From 3974067915d2943eb3119300906aa0dcbc3d1517 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Sat, 26 Feb 2022 02:43:01 +0900 Subject: [PATCH 1/9] Unstable 0.17.0.0 --- .../BarotraumaClient/ClientSource/Camera.cs | 12 +- .../Characters/AI/EnemyAIController.cs | 27 +- .../Characters/AI/HumanAIController.cs | 4 +- .../Characters/AI/Objectives/AIObjective.cs | 12 +- .../Characters/AI/Wreck/WreckAI.cs | 4 +- .../Characters/Animation/Ragdoll.cs | 12 +- .../ClientSource/Characters/Attack.cs | 8 +- .../ClientSource/Characters/Character.cs | 68 +- .../ClientSource/Characters/CharacterHUD.cs | 86 +- .../ClientSource/Characters/CharacterInfo.cs | 242 +-- .../Characters/CharacterNetworking.cs | 41 +- .../ClientSource/Characters/CharacterSound.cs | 10 +- .../ClientSource/Characters/HUDProgressBar.cs | 14 +- .../Characters/Health/AfflictionHusk.cs | 6 +- .../Characters/Health/AfflictionPsychosis.cs | 8 +- .../Characters/Health/CharacterHealth.cs | 180 +- .../Characters/Health/DamageModifier.cs | 4 +- .../ClientSource/Characters/Jobs/JobPrefab.cs | 22 +- .../ClientSource/Characters/Limb.cs | 82 +- .../ContentPackage/ModProject.cs | 167 ++ .../ContentPackageManager.cs | 53 + .../ClientSource/DebugConsole.cs | 465 ++-- .../Events/EventActions/ConversationAction.cs | 20 +- .../ClientSource/Events/EventManager.cs | 118 +- .../Missions/AbandonedOutpostMission.cs | 2 +- .../Events/Missions/CargoMission.cs | 24 +- .../Events/Missions/CombatMission.cs | 2 +- .../Events/Missions/MineralMission.cs | 2 +- .../ClientSource/Events/Missions/Mission.cs | 47 +- .../Events/Missions/MissionMode.cs | 6 +- .../Events/Missions/MissionPrefab.cs | 11 +- .../ClientSource/Fonts/ScalableFont.cs | 163 +- .../ClientSource/GUI/ChatBox.cs | 40 +- .../ClientSource/GUI/ComponentStyle.cs | 56 +- .../ClientSource/GUI/CrewManagement.cs | 83 +- .../ClientSource/GUI/FileSelection.cs | 115 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 505 +++-- .../ClientSource/GUI/GUIButton.cs | 41 +- .../ClientSource/GUI/GUIComponent.cs | 131 +- .../ClientSource/GUI/GUIContextMenu.cs | 29 +- .../ClientSource/GUI/GUIDropDown.cs | 35 +- .../ClientSource/GUI/GUIImage.cs | 3 +- .../ClientSource/GUI/GUILayoutGroup.cs | 105 +- .../ClientSource/GUI/GUIListBox.cs | 10 +- .../ClientSource/GUI/GUIMessageBox.cs | 58 +- .../ClientSource/GUI/GUINumberInput.cs | 6 +- .../ClientSource/GUI/GUIPrefab.cs | 395 ++++ .../ClientSource/GUI/GUIProgressBar.cs | 18 +- .../ClientSource/GUI/GUIScrollBar.cs | 7 +- .../ClientSource/GUI/GUIStyle.cs | 554 +---- .../ClientSource/GUI/GUITextBlock.cs | 184 +- .../ClientSource/GUI/GUITextBox.cs | 200 +- .../ClientSource/GUI/GUITickBox.cs | 26 +- .../ClientSource/GUI/HUDLayoutSettings.cs | 5 +- .../ClientSource/GUI/LoadingScreen.cs | 142 +- .../ClientSource/GUI/MedicalClinicUI.cs | 70 +- .../ClientSource/GUI/Store.cs | 238 +- .../ClientSource/GUI/SubmarineSelection.cs | 76 +- .../ClientSource/GUI/TabMenu.cs | 232 +- .../ClientSource/GUI/UISprite.cs | 10 +- .../ClientSource/GUI/UpgradeStore.cs | 124 +- .../ClientSource/GUI/VideoPlayer.cs | 39 +- .../ClientSource/GUI/VotingInterface.cs | 68 +- .../ClientSource/GUI/Widget.cs | 6 +- .../GameAnalytics/GameAnalyticsManager.cs | 6 +- .../BarotraumaClient/ClientSource/GameMain.cs | 414 ++-- .../ClientSource/GameSession/CargoManager.cs | 4 +- .../ClientSource/GameSession/CrewManager.cs | 687 +++--- .../GameSession/GameModes/CampaignMode.cs | 32 +- .../GameModes/Data/CampaignMetadata.cs | 27 +- .../GameModes/MultiPlayerCampaign.cs | 49 +- .../GameModes/SinglePlayerCampaign.cs | 16 +- .../GameSession/GameModes/TestGameMode.cs | 6 +- .../GameModes/Tutorials/BasicTutorial.cs | 683 ------ .../GameModes/Tutorials/CaptainTutorial.cs | 105 +- .../GameModes/Tutorials/ContextualTutorial.cs | 520 ----- .../GameModes/Tutorials/DoctorTutorial.cs | 134 +- .../GameModes/Tutorials/EditorTutorial.cs | 44 - .../GameModes/Tutorials/EngineerTutorial.cs | 99 +- .../GameModes/Tutorials/MechanicTutorial.cs | 198 +- .../GameModes/Tutorials/OfficerTutorial.cs | 134 +- .../GameModes/Tutorials/ScenarioTutorial.cs | 95 +- .../GameModes/Tutorials/Tutorial.cs | 391 ++-- .../GameModes/Tutorials/TutorialMode.cs | 6 - .../ClientSource/GameSession/GameSession.cs | 8 +- .../ClientSource/GameSession/HintManager.cs | 164 +- .../ClientSource/GameSession/ReadyCheck.cs | 22 +- .../ClientSource/GameSession/RoundSummary.cs | 122 +- .../ClientSource/GameSettings.cs | 1932 ----------------- .../ClientSource/Items/CharacterInventory.cs | 30 +- .../ClientSource/Items/Components/Door.cs | 2 +- .../Components/EntitySpawnerComponent.cs | 12 +- .../Items/Components/GeneticMaterial.cs | 28 +- .../ClientSource/Items/Components/Growable.cs | 30 +- .../Items/Components/Holdable/Holdable.cs | 2 +- .../Items/Components/Holdable/IdCard.cs | 115 +- .../Items/Components/Holdable/RangedWeapon.cs | 15 +- .../Items/Components/Holdable/Sprayer.cs | 6 +- .../Items/Components/ItemComponent.cs | 36 +- .../Items/Components/ItemContainer.cs | 40 +- .../Items/Components/ItemLabel.cs | 27 +- .../ClientSource/Items/Components/Ladder.cs | 2 +- .../Items/Components/LightComponent.cs | 15 +- .../Components/Machines/Deconstructor.cs | 37 +- .../Items/Components/Machines/Engine.cs | 22 +- .../Items/Components/Machines/Fabricator.cs | 144 +- .../Items/Components/Machines/MiniMap.cs | 111 +- .../Items/Components/Machines/Pump.cs | 22 +- .../Items/Components/Machines/Reactor.cs | 42 +- .../Items/Components/Machines/Sonar.cs | 66 +- .../Items/Components/Machines/Steering.cs | 81 +- .../Items/Components/Power/PowerContainer.cs | 26 +- .../Items/Components/Power/PowerTransfer.cs | 51 +- .../Items/Components/Power/Powered.cs | 6 +- .../Items/Components/Projectile.cs | 4 +- .../ClientSource/Items/Components/Quality.cs | 6 +- .../Items/Components/RepairTool.cs | 16 +- .../Items/Components/Repairable.cs | 44 +- .../ClientSource/Items/Components/Rope.cs | 12 +- .../ClientSource/Items/Components/Scanner.cs | 2 +- .../Items/Components/Signal/ButtonTerminal.cs | 6 +- .../Items/Components/Signal/Connection.cs | 38 +- .../Components/Signal/ConnectionPanel.cs | 2 +- .../Components/Signal/CustomInterface.cs | 8 +- .../Items/Components/Signal/Terminal.cs | 2 +- .../Items/Components/Signal/Wire.cs | 10 +- .../Items/Components/StatusHUD.cs | 67 +- .../ClientSource/Items/Components/Turret.cs | 54 +- .../ClientSource/Items/Components/Wearable.cs | 26 +- .../ClientSource/Items/Inventory.cs | 113 +- .../ClientSource/Items/Item.cs | 127 +- .../ClientSource/Items/ItemPrefab.cs | 198 +- .../Map/Creatures/BallastFloraBehavior.cs | 173 +- .../BarotraumaClient/ClientSource/Map/Gap.cs | 4 +- .../BarotraumaClient/ClientSource/Map/Hull.cs | 29 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 24 +- .../BackgroundCreatures/BackgroundCreature.cs | 2 +- .../BackgroundCreatureManager.cs | 31 +- .../BackgroundCreaturePrefab.cs | 45 +- .../Map/Levels/LevelObjects/LevelObject.cs | 6 +- .../Levels/LevelObjects/LevelObjectManager.cs | 4 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 22 +- .../ClientSource/Map/Levels/LevelRenderer.cs | 2 +- .../ClientSource/Map/Lights/ConvexHull.cs | 4 +- .../ClientSource/Map/Lights/LightManager.cs | 17 +- .../ClientSource/Map/Lights/LightSource.cs | 42 +- .../ClientSource/Map/LinkedSubmarine.cs | 23 +- .../ClientSource/Map/Map/Map.cs | 83 +- .../ClientSource/Map/Map/Radiation.cs | 6 +- .../ClientSource/Map/MapEntity.cs | 14 +- .../ClientSource/Map/MapEntityPrefab.cs | 10 +- .../ClientSource/Map/RoundSound.cs | 137 ++ .../ClientSource/Map/Structure.cs | 42 +- .../ClientSource/Map/StructurePrefab.cs | 25 +- .../ClientSource/Map/Submarine.cs | 170 +- .../ClientSource/Map/SubmarineInfo.cs | 24 +- .../ClientSource/Map/SubmarinePreview.cs | 77 +- .../ClientSource/Map/WayPoint.cs | 44 +- .../ClientSource/Networking/BanList.cs | 4 +- .../ClientSource/Networking/ChatMessage.cs | 48 +- .../ClientSource/Networking/Client.cs | 4 +- .../ClientSource/Networking/EntitySpawner.cs | 4 +- .../Networking/FileTransfer/FileReceiver.cs | 36 +- .../Networking/FileTransfer/ModReceiver.cs | 8 + .../ClientSource/Networking/GameClient.cs | 331 +-- .../ClientSource/Networking/KarmaManager.cs | 14 +- .../ClientEntityEventManager.cs | 14 +- .../ClientSource/Networking/NetStats.cs | 18 +- .../Networking/Primitives/Peers/ClientPeer.cs | 149 +- .../Primitives/Peers/LidgrenClientPeer.cs | 8 +- .../Primitives/Peers/SteamP2PClientPeer.cs | 4 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 4 +- .../ClientSource/Networking/RespawnManager.cs | 2 +- .../ClientSource/Networking/ServerInfo.cs | 30 +- .../ClientSource/Networking/ServerLog.cs | 19 +- .../ClientSource/Networking/ServerSettings.cs | 84 +- .../ClientSource/Networking/SteamManager.cs | 1740 --------------- .../Networking/Voip/VoipCapture.cs | 33 +- .../Networking/Voip/VoipClient.cs | 14 +- .../ClientSource/Networking/Voting.cs | 4 +- .../ClientSource/Particles/ParticleEmitter.cs | 72 +- .../ClientSource/Particles/ParticleManager.cs | 63 +- .../ClientSource/Particles/ParticlePrefab.cs | 115 +- .../ClientSource/Physics/PhysicsBody.cs | 2 +- .../ClientSource/PlayerInput.cs | 94 +- .../BarotraumaClient/ClientSource/Program.cs | 49 +- .../ClientSource/Screens/CampaignEndScreen.cs | 16 +- .../MultiPlayerCampaignSetupUI.cs | 41 +- .../SinglePlayerCampaignSetupUI.cs | 99 +- .../ClientSource/Screens/CampaignUI.cs | 52 +- .../CharacterEditor/CharacterEditorScreen.cs | 382 ++-- .../Screens/CharacterEditor/Wizard.cs | 103 +- .../ClientSource/Screens/CreditsPlayer.cs | 6 +- .../ClientSource/Screens/EditorImage.cs | 18 +- .../ClientSource/Screens/EditorScreen.cs | 11 +- .../Screens/EventEditor/EditorNode.cs | 26 +- .../Screens/EventEditor/EventEditorScreen.cs | 86 +- .../Screens/EventEditor/NodeConnection.cs | 31 +- .../ClientSource/Screens/GameScreen.cs | 10 +- .../ClientSource/Screens/LevelEditorScreen.cs | 267 +-- .../ClientSource/Screens/MainMenuScreen.cs | 402 ++-- .../ClientSource/Screens/ModDownloadScreen.cs | 296 +++ .../ClientSource/Screens/NetLobbyScreen.cs | 321 +-- .../Screens/ParticleEditorScreen.cs | 19 +- .../Screens/RoundSummaryScreen.cs | 8 +- .../ClientSource/Screens/Screen.cs | 3 +- .../ClientSource/Screens/ServerListScreen.cs | 504 +---- .../Screens/SpriteEditorScreen.cs | 178 +- .../Screens/SteamWorkshopScreen.cs | 1923 ---------------- .../ClientSource/Screens/SubEditorScreen.cs | 930 ++++---- .../ClientSource/Screens/TestScreen.cs | 14 +- .../Serialization/SerializableEntityEditor.cs | 221 +- .../Settings/CompletedTutorials.cs | 45 + .../Settings/DebugConsoleMapping.cs | 58 + .../ClientSource/Settings/IgnoredHints.cs | 42 + .../Settings/MultiplayerPreferences.cs | 112 + .../Settings/ServerListFilters.cs | 66 + .../ClientSource/Settings/SettingsMenu.cs | 752 +++++++ .../ClientSource/Sounds/OpenAL/Alc.cs | 13 +- .../ClientSource/Sounds/SoundChannel.cs | 2 +- .../ClientSource/Sounds/SoundManager.cs | 33 +- .../ClientSource/Sounds/SoundPlayer.cs | 555 +---- .../ClientSource/Sounds/SoundPrefab.cs | 267 +++ .../ClientSource/Sounds/VoipSound.cs | 6 +- .../ClientSource/Sprite/DecorativeSprite.cs | 37 +- .../DeformAnimations/CustomDeformation.cs | 4 +- .../Sprite/DeformAnimations/Inflate.cs | 4 +- .../DeformAnimations/NoiseDeformation.cs | 6 +- .../DeformAnimations/PositionalDeformation.cs | 8 +- .../DeformAnimations/SpriteDeformation.cs | 22 +- .../ClientSource/Sprite/DeformableSprite.cs | 4 +- .../ClientSource/Sprite/Sprite.cs | 102 +- .../StatusEffects/StatusEffect.cs | 6 +- .../ClientSource/Steam/AuthTicket.cs | 27 + .../ClientSource/Steam/BBCode.cs | 187 ++ .../ClientSource/Steam/ItemList.cs | 694 ++++++ .../ClientSource/Steam/Lobby.cs | 422 ++++ .../ClientSource/Steam/PublishTab.cs | 531 +++++ .../ClientSource/Steam/SteamManager.cs | 143 ++ .../ClientSource/Steam/UiUtil.cs | 109 + .../ClientSource/Steam/Workshop.cs | 285 +++ .../ClientSource/Steam/WorkshopMenu.cs | 442 ++++ .../ClientSource/SubEditorCommands.cs | 36 +- .../Text/LocalizedString/LimitLString.cs | 34 + .../Text/LocalizedString/WrappedLString.cs | 26 + .../Traitors/TraitorMissionPrefab.cs | 32 +- .../Traitors/TraitorMissionResult.cs | 2 +- .../ClientSource/Upgrades/UpgradePrefab.cs | 4 +- .../ClientSource/Utils/HttpEncoder.cs | 165 +- .../Utils/LocalizationCSVtoXML.cs | 16 +- .../ClientSource/Utils/SpreadsheetExport.cs | 36 +- .../ClientSource/Utils/SpriteRecorder.cs | 4 +- .../ClientSource/Utils/TextureLoader.cs | 2 +- .../ClientSource/Utils/ToolBox.cs | 24 + .../BarotraumaClient/LinuxClient.csproj | 18 +- Barotrauma/BarotraumaClient/MacClient.csproj | 10 +- .../Properties/launchSettings.json | 8 + .../BarotraumaClient/WindowsClient.csproj | 12 +- .../BarotraumaServer/LinuxServer.csproj | 14 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/Character.cs | 4 +- .../ServerSource/Characters/CharacterInfo.cs | 41 +- .../Characters/CharacterNetworking.cs | 74 +- .../ServerSource/DebugConsole.cs | 36 +- .../Events/EventActions/ConversationAction.cs | 2 +- .../Missions/AbandonedOutpostMission.cs | 4 +- .../Events/Missions/CargoMission.cs | 3 +- .../Events/Missions/CombatMission.cs | 4 +- .../Events/Missions/EscortMission.cs | 2 +- .../Events/Missions/MineralMission.cs | 4 +- .../ServerSource/Events/Missions/Mission.cs | 8 +- .../Events/Missions/NestMission.cs | 2 +- .../Events/Missions/PirateMission.cs | 2 +- .../Events/Missions/SalvageMission.cs | 3 +- .../Events/Missions/ScanMission.cs | 3 +- .../BarotraumaServer/ServerSource/GameMain.cs | 82 +- .../ServerSource/GameSession/CargoManager.cs | 4 +- .../ServerSource/GameSession/CrewManager.cs | 6 +- .../GameSession/GameModes/CampaignMode.cs | 4 +- .../GameModes/CharacterCampaignData.cs | 2 +- .../GameSession/GameModes/MissionMode.cs | 4 +- .../GameModes/MultiPlayerCampaign.cs | 10 +- .../Items/Components/GeneticMaterial.cs | 4 +- .../ServerSource/Items/Components/Growable.cs | 4 +- .../Items/Components/ItemComponent.cs | 2 +- .../Items/Components/ItemLabel.cs | 10 +- .../Items/Components/Machines/Fabricator.cs | 16 +- .../Items/Components/Repairable.cs | 7 +- .../ServerSource/Items/Inventory.cs | 2 +- .../ServerSource/Items/Item.cs | 23 +- .../Map/Creatures/BallastFloraBehavior.cs | 29 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 7 +- .../ServerSource/Networking/ChatMessage.cs | 52 +- .../ServerSource/Networking/Client.cs | 8 +- .../ServerSource/Networking/EntitySpawner.cs | 23 +- .../Networking/FileTransfer/FileSender.cs | 131 +- .../Networking/FileTransfer/ModSender.cs | 55 + .../ServerSource/Networking/GameServer.cs | 245 ++- .../ServerSource/Networking/KarmaManager.cs | 14 +- .../ServerEntityEventManager.cs | 10 +- .../Peers/Server/LidgrenServerPeer.cs | 8 +- .../Primitives/Peers/Server/ServerPeer.cs | 10 +- .../Peers/Server/SteamP2PServerPeer.cs | 8 +- .../ServerSource/Networking/RespawnManager.cs | 18 +- .../ServerSource/Networking/ServerSettings.cs | 18 +- .../ServerSource/Networking/Voting.cs | 2 +- .../BarotraumaServer/ServerSource/Program.cs | 12 +- .../ServerSource/Screens/NetLobbyScreen.cs | 4 +- .../{Networking => Steam}/SteamManager.cs | 26 +- .../ServerSource/Traitors/Goals/Goal.cs | 3 +- .../Traitors/Goals/GoalDestroyItemsWithTag.cs | 4 +- .../Goals/GoalEntityTransformation.cs | 12 +- .../Traitors/Goals/GoalFindItem.cs | 18 +- .../Traitors/Goals/GoalFloodPercentOfSub.cs | 2 +- .../Goals/GoalKeepTransformedAlive.cs | 8 +- .../Traitors/Goals/GoalKillTarget.cs | 6 +- .../Traitors/Goals/GoalReplaceInventory.cs | 8 +- .../Traitors/Goals/GoalSabotageItems.cs | 7 +- .../Traitors/Goals/GoalUnwiring.cs | 2 +- .../Goals/Modifiers/GoalHasDuration.cs | 6 +- .../Goals/Modifiers/GoalHasTimeLimit.cs | 2 +- .../Goals/Modifiers/GoalIsOptional.cs | 4 +- .../Traitors/Goals/Modifiers/Modifier.cs | 2 +- .../ServerSource/Traitors/Objective.cs | 27 +- .../ServerSource/Traitors/Traitor.cs | 18 +- .../ServerSource/Traitors/TraitorManager.cs | 3 +- .../ServerSource/Traitors/TraitorMission.cs | 8 +- .../Traitors/TraitorMissionPrefab.cs | 60 +- .../BarotraumaServer/WindowsServer.csproj | 4 +- .../Data/ContentPackages/Vanilla 0.9.xml | 324 --- .../LocalMods/PowerTestSub/PowerTestSub.sub | Bin 0 -> 9564 bytes .../LocalMods/PowerTestSub/filelist.xml | 4 + .../{Mods => LocalMods}/info.txt | 3 +- .../Mods/ExampleMod/Humpback2.sub | Bin 340685 -> 0 bytes .../Mods/ExampleMod/PreviewImage.png | Bin 337280 -> 0 bytes .../Redcrawler/Animations/RedcrawlerRun.xml | 23 - .../Animations/RedcrawlerSwimFast.xml | 20 - .../Animations/RedcrawlerSwimSlow.xml | 20 - .../Redcrawler/Animations/RedcrawlerWalk.xml | 23 - .../Ragdolls/RedcrawlerDefaultRagdoll.xml | 116 - .../Mods/ExampleMod/Redcrawler/Redcrawler.xml | 70 - .../Mods/ExampleMod/Redcrawler/crawler.png | Bin 161093 -> 0 bytes .../Mods/ExampleMod/filelist.xml | 5 - .../SharedSource/Characters/AI/AITarget.cs | 14 +- .../Characters/AI/EnemyAIController.cs | 45 +- .../Characters/AI/HumanAIController.cs | 69 +- .../Characters/AI/IndoorsSteeringManager.cs | 2 +- .../Characters/AI/MentalStateManager.cs | 10 +- .../Characters/AI/NPCConversation.cs | 256 +-- .../Characters/AI/Objectives/AIObjective.cs | 10 +- .../Objectives/AIObjectiveChargeBatteries.cs | 6 +- .../AI/Objectives/AIObjectiveCleanupItem.cs | 36 +- .../AI/Objectives/AIObjectiveCleanupItems.cs | 10 +- .../AI/Objectives/AIObjectiveCombat.cs | 38 +- .../AI/Objectives/AIObjectiveContainItem.cs | 49 +- .../AI/Objectives/AIObjectiveDecontainItem.cs | 4 +- .../Objectives/AIObjectiveEscapeHandcuffs.cs | 4 +- .../Objectives/AIObjectiveExtinguishFire.cs | 16 +- .../Objectives/AIObjectiveExtinguishFires.cs | 4 +- .../Objectives/AIObjectiveFightIntruders.cs | 6 +- .../Objectives/AIObjectiveFindDivingGear.cs | 26 +- .../AI/Objectives/AIObjectiveFindSafety.cs | 6 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 18 +- .../AI/Objectives/AIObjectiveFixLeaks.cs | 2 +- .../AI/Objectives/AIObjectiveGetItem.cs | 51 +- .../AI/Objectives/AIObjectiveGetItems.cs | 10 +- .../AI/Objectives/AIObjectiveGoTo.cs | 66 +- .../AI/Objectives/AIObjectiveIdle.cs | 6 +- .../AI/Objectives/AIObjectiveLoadItem.cs | 22 +- .../AI/Objectives/AIObjectiveLoadItems.cs | 14 +- .../AI/Objectives/AIObjectiveLoop.cs | 2 +- .../AI/Objectives/AIObjectiveManager.cs | 115 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 8 +- .../AI/Objectives/AIObjectivePrepare.cs | 12 +- .../AI/Objectives/AIObjectivePumpWater.cs | 6 +- .../AI/Objectives/AIObjectiveRepairItem.cs | 12 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 8 +- .../AI/Objectives/AIObjectiveRescue.cs | 69 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 7 +- .../AI/Objectives/AIObjectiveReturn.cs | 10 +- .../SharedSource/Characters/AI/Order.cs | 839 +++---- .../SharedSource/Characters/AI/PetBehavior.cs | 38 +- .../AI/ShipCommand/ShipIssueWorker.cs | 29 +- .../AI/ShipCommand/ShipIssueWorkerItem.cs | 6 +- .../ShipIssueWorkerOperateWeapons.cs | 2 +- .../ShipIssueWorkerPowerUpReactor.cs | 4 +- .../AI/ShipCommand/ShipIssueWorkerSteer.cs | 2 +- .../Characters/AI/ShipCommandManager.cs | 27 +- .../Characters/AI/Wreck/WreckAI.cs | 36 +- .../Characters/AI/Wreck/WreckAIConfig.cs | 109 +- .../SharedSource/Characters/AICharacter.cs | 4 +- .../SharedSource/Characters/AIChatMessage.cs | 4 +- .../Animation/FishAnimController.cs | 11 +- .../Animation/HumanoidAnimController.cs | 28 +- .../Characters/Animation/Ragdoll.cs | 10 +- .../SharedSource/Characters/Attack.cs | 108 +- .../SharedSource/Characters/Character.cs | 397 ++-- .../SharedSource/Characters/CharacterInfo.cs | 1031 ++++----- .../Characters/CharacterPrefab.cs | 140 +- .../SharedSource/Characters/CorpsePrefab.cs | 98 +- .../Health/Afflictions/Affliction.cs | 19 +- .../Health/Afflictions/AfflictionHusk.cs | 40 +- .../Health/Afflictions/AfflictionPrefab.cs | 531 ++--- .../Afflictions/AfflictionSpaceHerpes.cs | 2 +- .../Characters/Health/CharacterHealth.cs | 136 +- .../Characters/Health/DamageModifier.cs | 81 +- .../SharedSource/Characters/HumanPrefab.cs | 78 +- .../SharedSource/Characters/Jobs/Job.cs | 89 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 229 +- .../SharedSource/Characters/Jobs/Skill.cs | 70 +- .../Characters/Jobs/SkillPrefab.cs | 6 +- .../SharedSource/Characters/Limb.cs | 13 +- .../Characters/NPCPersonalityTrait.cs | 36 +- .../Params/Animation/AnimationParams.cs | 109 +- .../Params/Animation/FishAnimations.cs | 57 +- .../Params/Animation/HumanoidAnimations.cs | 48 +- .../Characters/Params/CharacterParams.cs | 413 ++-- .../Characters/Params/EditableParams.cs | 53 +- .../Params/Ragdoll/RagdollParams.cs | 426 ++-- .../SharedSource/Characters/SkillSettings.cs | 63 +- .../AbilityConditionals/AbilityCondition.cs | 2 +- .../AbilityConditionAffliction.cs | 2 +- .../AbilityConditionAttackData.cs | 8 +- .../AbilityConditionAttackResult.cs | 11 +- .../AbilityConditionCharacter.cs | 4 +- .../AbilityConditionData.cs | 2 +- .../AbilityConditionEvasiveManeuvers.cs | 2 +- .../AbilityConditionGeneHarvester.cs | 2 +- .../AbilityConditionIsAiming.cs | 2 +- .../AbilityConditionItem.cs | 2 +- .../AbilityConditionItemInSubmarine.cs | 2 +- .../AbilityConditionLocation.cs | 9 +- .../AbilityConditionMission.cs | 2 +- .../AbilityConditionReduceAffliction.cs | 7 +- .../AbilityConditionSkill.cs | 6 +- .../AbilityConditionStatusEffectIdentifier.cs | 2 +- .../AbilityConditionAboveVitality.cs | 2 +- .../AbilityConditionAlliesAboveVitality.cs | 2 +- .../AbilityConditionCoauthor.cs | 2 +- .../AbilityConditionCrouched.cs | 2 +- .../AbilityConditionDataless.cs | 2 +- .../AbilityConditionHasAffliction.cs | 2 +- .../AbilityConditionHasDifferentJobs.cs | 2 +- .../AbilityConditionHasItem.cs | 7 +- .../AbilityConditionHasPermanentStat.cs | 8 +- .../AbilityConditionHasSkill.cs | 2 +- .../AbilityConditionHasStatusTag.cs | 2 +- .../AbilityConditionHasVelocity.cs | 2 +- .../AbilityConditionInFriendlySubmarine.cs | 2 +- .../AbilityConditionInHull.cs | 2 +- .../AbilityConditionInWater.cs | 2 +- .../AbilityConditionLevelsBehindHighest.cs | 2 +- .../AbilityConditionNoCrewDied.cs | 2 +- .../AbilityConditionOnMission.cs | 2 +- .../AbilityConditionRagdolled.cs | 2 +- .../AbilityConditionRunning.cs | 2 +- .../AbilityConditionServerRandom.cs | 2 +- .../AbilityConditionShipFlooded.cs | 2 +- .../Talents/Abilities/AbilityInterfaces.cs | 2 +- .../Talents/Abilities/AbilityObjects.cs | 2 +- .../Talents/Abilities/CharacterAbility.cs | 4 +- .../Abilities/CharacterAbilityApplyForce.cs | 2 +- .../CharacterAbilityApplyStatusEffects.cs | 2 +- ...racterAbilityApplyStatusEffectsToAllies.cs | 2 +- ...cterAbilityApplyStatusEffectsToAttacker.cs | 2 +- ...pplyStatusEffectsToLastOrderedCharacter.cs | 5 +- ...rAbilityApplyStatusEffectsToNearestAlly.cs | 2 +- ...erAbilityApplyStatusEffectsToRandomAlly.cs | 33 +- .../CharacterAbilityGainSimultaneousSkill.cs | 6 +- .../CharacterAbilityGiveAffliction.cs | 10 +- .../Abilities/CharacterAbilityGiveFlag.cs | 2 +- .../Abilities/CharacterAbilityGiveMoney.cs | 10 +- .../CharacterAbilityGivePermanentStat.cs | 2 +- .../CharacterAbilityGiveResistance.cs | 8 +- .../Abilities/CharacterAbilityGiveStat.cs | 2 +- .../CharacterAbilityGiveTalentPoints.cs | 2 +- .../CharacterAbilityIncreaseSkill.cs | 12 +- .../CharacterAbilityModifyAffliction.cs | 2 +- .../CharacterAbilityModifyAttackData.cs | 4 +- .../Abilities/CharacterAbilityModifyFlag.cs | 2 +- .../CharacterAbilityModifyResistance.cs | 8 +- .../Abilities/CharacterAbilityModifyStat.cs | 2 +- .../CharacterAbilityModifyStatToFlooding.cs | 2 +- .../CharacterAbilityModifyStatToLevel.cs | 2 +- .../CharacterAbilityModifyStatToSkill.cs | 2 +- .../Abilities/CharacterAbilityModifyValue.cs | 2 +- .../Abilities/CharacterAbilityPutItem.cs | 12 +- .../CharacterAbilityResetPermanentStat.cs | 2 +- .../Abilities/CharacterAbilityRevive.cs | 2 +- .../CharacterAbilitySpawnItemsToContainer.cs | 2 +- .../Abilities/CharacterAbilityUnlockTree.cs | 25 +- .../CharacterAbilityAlienHoarder.cs | 2 +- .../CharacterAbilityApprenticeship.cs | 2 +- .../CharacterAbilityAtmosMachine.cs | 8 +- .../CharacterAbilityBountyHunter.cs | 4 +- .../CharacterAbilityByTheBook.cs | 4 +- .../CharacterAbilityInsurancePolicy.cs | 4 +- .../CharacterAbilityMultitasker.cs | 6 +- .../CharacterAbilityPsychoClown.cs | 2 +- .../CharacterAbilityRegenerateLoot.cs | 2 +- .../CharacterAbilityTandemFire.cs | 2 +- .../AbilityGroups/CharacterAbilityGroup.cs | 28 +- .../CharacterAbilityGroupEffect.cs | 2 +- .../CharacterAbilityGroupInterval.cs | 2 +- .../Characters/Talents/CharacterTalent.cs | 12 +- .../Characters/Talents/TalentPrefab.cs | 121 +- .../Characters/Talents/TalentTree.cs | 156 +- .../ContentFile/AfflictionsFile.cs | 113 + .../BackgroundCreaturePrefabsFile.cs | 9 + .../ContentFile/BallastFloraFile.cs | 19 + .../ContentFile/BeaconStationFile.cs | 8 + .../CaveGenerationParametersFile.cs | 18 + .../ContentFile/CharacterFile.cs | 100 + .../ContentFile/ContentFile.cs | 117 + .../ContentFile/CorpsesFile.cs | 18 + .../ContentFile/DecalsFile.cs | 22 + .../ContentFile/EnemySubmarineFile.cs | 8 + .../ContentFile/EventManagerSettingsFile.cs | 17 + .../ContentFile/FactionsFile.cs | 18 + .../ContentFile/GenericPrefabFile.cs | 72 + .../ContentFile/HashlessFile.cs | 10 + .../ContentFile/ItemAssemblyFile.cs | 17 + .../ContentManagement/ContentFile/ItemFile.cs | 18 + .../ContentManagement/ContentFile/JobsFile.cs | 60 + .../LevelGenerationParametersFile.cs | 68 + .../ContentFile/LevelObjectPrefabsFile.cs | 18 + .../ContentFile/LocationTypesFile.cs | 18 + .../MapGenerationParametersFile.cs | 34 + .../ContentFile/MissionsFile.cs | 32 + .../ContentFile/NPCConversationsFile.cs | 44 + .../ContentFile/NPCSetsFile.cs | 18 + .../ContentFile/OrdersFile.cs | 70 + .../ContentFile/OtherFile.cs | 16 + .../ContentFile/OutpostConfigFile.cs | 18 + .../ContentFile/OutpostFile.cs | 7 + .../ContentFile/OutpostModuleFIle.cs | 7 + .../ContentFile/ParticlesFile.cs | 31 + .../ContentFile/RandomEventsFile.cs | 91 + .../ContentFile/RuinConfigFile.cs | 19 + .../ContentFile/SkillSettingsFile.cs | 33 + .../ContentFile/SoundsFile.cs | 38 + .../ContentFile/StructureFile.cs | 18 + .../ContentFile/SubmarineFile.cs | 38 + .../ContentFile/TalentTreesFile.cs | 18 + .../ContentFile/TalentsFile.cs | 18 + .../ContentManagement/ContentFile/TextFile.cs | 48 + .../ContentFile/TraitorMissionsFile.cs | 26 + .../ContentFile/UIStyleFile.cs | 100 + .../ContentFile/UpgradeModulesFile.cs | 31 + .../ContentFile/WreckAIConfigFile.cs | 18 + .../ContentFile/WreckFile.cs | 7 + .../ContentPackage/ContentPackage.cs | 306 +++ .../ContentPackage/CorePackage.cs | 52 + .../ContentPackage/RegularPackage.cs | 19 + .../ContentPackageManager.cs | 461 ++++ .../ContentManagement/ContentPath.cs | 149 ++ .../ContentManagement/ContentXElement.cs | 130 ++ .../ContentManagement/Identifier.cs | 160 ++ .../MissingContentPackageException.cs | 17 + .../ContentManagement/ModProject.cs | 0 .../SharedSource/ContentPackage.cs | 875 -------- .../SharedSource/DebugConsole.cs | 153 +- .../SharedSource/Decals/DecalManager.cs | 112 +- .../SharedSource/Decals/DecalPrefab.cs | 36 +- .../SharedSource/Events/ArtifactEvent.cs | 6 +- .../Events/EventActions/AfflictionAction.cs | 40 +- .../Events/EventActions/BinaryOptionAction.cs | 4 +- .../EventActions/CheckAfflictionAction.cs | 18 +- .../Events/EventActions/CheckDataAction.cs | 25 +- .../Events/EventActions/CheckItemAction.cs | 18 +- .../Events/EventActions/CheckMoneyAction.cs | 4 +- .../EventActions/CheckReputationAction.cs | 8 +- .../Events/EventActions/ClearTagAction.cs | 8 +- .../Events/EventActions/CombatAction.cs | 20 +- .../Events/EventActions/ConversationAction.cs | 47 +- .../Events/EventActions/EventAction.cs | 10 +- .../Events/EventActions/FireAction.cs | 8 +- .../Events/EventActions/GiveSkillExpAction.cs | 16 +- .../SharedSource/Events/EventActions/GoTo.cs | 4 +- .../Events/EventActions/GodModeAction.cs | 8 +- .../SharedSource/Events/EventActions/Label.cs | 4 +- .../Events/EventActions/MissionAction.cs | 34 +- .../Events/EventActions/MoneyAction.cs | 6 +- .../EventActions/NPCChangeTeamAction.cs | 10 +- .../Events/EventActions/NPCFollowAction.cs | 12 +- .../Events/EventActions/NPCWaitAction.cs | 8 +- .../Events/EventActions/RNGAction.cs | 4 +- .../Events/EventActions/RemoveItemAction.cs | 24 +- .../Events/EventActions/ReputationAction.cs | 12 +- .../Events/EventActions/SetDataAction.cs | 12 +- .../EventActions/SetPriceMultiplierAction.cs | 8 +- .../Events/EventActions/SkillCheckAction.cs | 20 +- .../Events/EventActions/SpawnAction.cs | 82 +- .../Events/EventActions/StatusEffectAction.cs | 10 +- .../Events/EventActions/TagAction.cs | 85 +- .../Events/EventActions/TriggerAction.cs | 49 +- .../Events/EventActions/TriggerEventAction.cs | 6 +- .../Events/EventActions/UnlockPathAction.cs | 4 +- .../Events/EventActions/WaitAction.cs | 4 +- .../SharedSource/Events/EventManager.cs | 162 +- .../Events/EventManagerSettings.cs | 83 +- .../SharedSource/Events/EventPrefab.cs | 29 +- .../SharedSource/Events/EventSet.cs | 342 ++- .../SharedSource/Events/MalfunctionEvent.cs | 8 +- .../Missions/AbandonedOutpostMission.cs | 28 +- .../Events/Missions/AlienRuinMission.cs | 21 +- .../Events/Missions/BeaconMission.cs | 76 +- .../Events/Missions/CargoMission.cs | 21 +- .../Events/Missions/CombatMission.cs | 24 +- .../Events/Missions/EscortMission.cs | 13 +- .../Events/Missions/MineralMission.cs | 41 +- .../SharedSource/Events/Missions/Mission.cs | 93 +- .../Events/Missions/MissionPrefab.cs | 229 +- .../Events/Missions/MonsterMission.cs | 12 +- .../Events/Missions/NestMission.cs | 18 +- .../Events/Missions/PirateMission.cs | 16 +- .../Events/Missions/SalvageMission.cs | 13 +- .../Events/Missions/ScanMission.cs | 11 +- .../SharedSource/Events/MonsterEvent.cs | 60 +- .../SharedSource/Events/ScriptedEvent.cs | 20 +- .../Extensions/IEnumerableExtensions.cs | 125 +- .../Extensions/StringExtensions.cs | 22 + .../Extensions/StringFormatter.cs | 4 +- .../GameAnalytics/GameAnalyticsConsent.cs | 9 +- .../GameAnalytics/GameAnalyticsManager.cs | 41 +- .../GameSession/AutoItemPlacer.cs | 74 +- .../SharedSource/GameSession/CargoManager.cs | 11 +- .../SharedSource/GameSession/CrewManager.cs | 91 +- .../GameSession/Data/CampaignMetadata.cs | 47 +- .../SharedSource/GameSession/Data/Factions.cs | 77 +- .../GameSession/Data/Reputation.cs | 46 +- .../GameSession/GameModes/CampaignMode.cs | 27 +- .../GameSession/GameModes/GameMode.cs | 2 +- .../GameSession/GameModes/GameModePreset.cs | 26 +- .../GameModes/MultiPlayerCampaign.cs | 4 +- .../SharedSource/GameSession/GameSession.cs | 219 +- .../SharedSource/GameSession/HireManager.cs | 4 +- .../SharedSource/GameSession/MedicalClinic.cs | 34 +- .../GameSession/UpgradeManager.cs | 41 +- .../SharedSource/GameSettings.cs | 1516 ------------- .../SharedSource/Items/CharacterInventory.cs | 31 +- .../Items/Components/DockingPort.cs | 52 +- .../SharedSource/Items/Components/Door.cs | 44 +- .../Items/Components/ElectricalDischarger.cs | 27 +- .../Components/EntitySpawnerComponent.cs | 54 +- .../Items/Components/GeneticMaterial.cs | 45 +- .../SharedSource/Items/Components/Growable.cs | 94 +- .../Items/Components/Holdable/Holdable.cs | 65 +- .../Items/Components/Holdable/IdCard.cs | 145 +- .../Components/Holdable/LevelResource.cs | 10 +- .../Items/Components/Holdable/MeleeWeapon.cs | 22 +- .../Items/Components/Holdable/Pickable.cs | 6 +- .../Items/Components/Holdable/Propulsion.cs | 8 +- .../Items/Components/Holdable/RangedWeapon.cs | 18 +- .../Items/Components/Holdable/RepairTool.cs | 64 +- .../Items/Components/Holdable/Sprayer.cs | 16 +- .../Items/Components/Holdable/Throwable.cs | 4 +- .../Items/Components/ItemComponent.cs | 122 +- .../Items/Components/ItemContainer.cs | 78 +- .../SharedSource/Items/Components/Ladder.cs | 4 +- .../Items/Components/Machines/Controller.cs | 16 +- .../Components/Machines/Deconstructor.cs | 39 +- .../Items/Components/Machines/Engine.cs | 55 +- .../Items/Components/Machines/Fabricator.cs | 106 +- .../Items/Components/Machines/MiniMap.cs | 30 +- .../Components/Machines/OutpostTerminal.cs | 2 +- .../Components/Machines/OxygenGenerator.cs | 37 +- .../Items/Components/Machines/Pump.cs | 42 +- .../Items/Components/Machines/Reactor.cs | 178 +- .../Items/Components/Machines/Sonar.cs | 43 +- .../Components/Machines/SonarTransducer.cs | 14 +- .../Items/Components/Machines/Steering.cs | 60 +- .../Items/Components/Machines/Vent.cs | 2 +- .../SharedSource/Items/Components/NameTag.cs | 4 +- .../SharedSource/Items/Components/Planter.cs | 14 +- .../Items/Components/Power/PowerContainer.cs | 251 ++- .../Items/Components/Power/PowerTransfer.cs | 77 +- .../Items/Components/Power/Powered.cs | 661 +++++- .../Items/Components/Projectile.cs | 104 +- .../SharedSource/Items/Components/Quality.cs | 6 +- .../Items/Components/RemoteController.cs | 10 +- .../Items/Components/Repairable.cs | 58 +- .../SharedSource/Items/Components/Rope.cs | 20 +- .../SharedSource/Items/Components/Scanner.cs | 10 +- .../Items/Components/Signal/AdderComponent.cs | 2 +- .../Items/Components/Signal/AndComponent.cs | 10 +- .../Components/Signal/ArithmeticComponent.cs | 8 +- .../Items/Components/Signal/ButtonTerminal.cs | 10 +- .../Items/Components/Signal/ColorComponent.cs | 4 +- .../Components/Signal/ConcatComponent.cs | 6 +- .../Items/Components/Signal/Connection.cs | 99 +- .../Components/Signal/ConnectionPanel.cs | 15 +- .../Components/Signal/CustomInterface.cs | 37 +- .../Items/Components/Signal/DelayComponent.cs | 8 +- .../Components/Signal/DivideComponent.cs | 2 +- .../Components/Signal/EqualsComponent.cs | 10 +- .../Signal/ExponentiationComponent.cs | 4 +- .../Components/Signal/FunctionComponent.cs | 4 +- .../Components/Signal/GreaterComponent.cs | 2 +- .../Items/Components/Signal/LightComponent.cs | 28 +- .../Components/Signal/MemoryComponent.cs | 8 +- .../Components/Signal/ModuloComponent.cs | 4 +- .../Items/Components/Signal/MotionSensor.cs | 28 +- .../Components/Signal/MultiplyComponent.cs | 2 +- .../Items/Components/Signal/NotComponent.cs | 4 +- .../Items/Components/Signal/OrComponent.cs | 2 +- .../Components/Signal/OscillatorComponent.cs | 6 +- .../Items/Components/Signal/OxygenDetector.cs | 12 +- .../Components/Signal/RegExFindComponent.cs | 14 +- .../Items/Components/Signal/RelayComponent.cs | 289 ++- .../Components/Signal/SignalCheckComponent.cs | 10 +- .../Items/Components/Signal/SmokeDetector.cs | 8 +- .../Components/Signal/StringComponent.cs | 4 +- .../Components/Signal/SubtractComponent.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 18 +- .../Signal/TrigonometricFunctionComponent.cs | 6 +- .../Items/Components/Signal/WaterDetector.cs | 8 +- .../Items/Components/Signal/WifiComponent.cs | 20 +- .../Items/Components/Signal/Wire.cs | 14 +- .../Items/Components/Signal/XorComponent.cs | 2 +- .../Items/Components/StatusHUD.cs | 2 +- .../Items/Components/TriggerComponent.cs | 6 +- .../SharedSource/Items/Components/Turret.cs | 149 +- .../SharedSource/Items/Components/Wearable.cs | 94 +- .../SharedSource/Items/Inventory.cs | 24 +- .../SharedSource/Items/Item.cs | 344 +-- .../SharedSource/Items/ItemPrefab.cs | 1430 +++++------- .../SharedSource/Items/RelatedItem.cs | 77 +- .../SharedSource/Map/CoreEntityPrefab.cs | 80 +- .../Map/Creatures/BallastFloraBehavior.cs | 411 ++-- .../Map/Creatures/BallastFloraPrefab.cs | 113 +- .../Creatures/State/DefendWithPumpState.cs | 2 +- .../Map/Creatures/State/GrowIdleState.cs | 39 +- .../Map/Creatures/State/GrowToTargetState.cs | 2 +- .../SharedSource/Map/Entity.cs | 34 + .../SharedSource/Map/Explosion.cs | 8 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 8 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 90 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 135 +- .../SharedSource/Map/Levels/Biome.cs | 50 + .../Map/Levels/CaveGenerationParams.cs | 125 +- .../SharedSource/Map/Levels/CaveGenerator.cs | 4 +- .../SharedSource/Map/Levels/Level.cs | 301 +-- .../SharedSource/Map/Levels/LevelData.cs | 26 +- .../Map/Levels/LevelGenerationParams.cs | 367 +--- .../Map/Levels/LevelObjects/LevelObject.cs | 8 +- .../Levels/LevelObjects/LevelObjectManager.cs | 47 +- .../Levels/LevelObjects/LevelObjectPrefab.cs | 194 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 18 +- .../Map/Levels/Ruins/RuinGenerationParams.cs | 81 +- .../Map/Levels/Ruins/RuinGenerator.cs | 2 +- .../SharedSource/Map/LinkedSubmarine.cs | 39 +- .../SharedSource/Map/Map/Location.cs | 77 +- .../SharedSource/Map/Map/LocationType.cs | 155 +- .../Map/Map/LocationTypeChange.cs | 56 +- .../SharedSource/Map/Map/Map.cs | 73 +- .../Map/Map/MapGenerationParams.cs | 183 +- .../SharedSource/Map/Map/Radiation.cs | 8 +- .../SharedSource/Map/Map/RadiationParams.cs | 24 +- .../SharedSource/Map/MapEntity.cs | 55 +- .../SharedSource/Map/MapEntityPrefab.cs | 371 ++-- .../SharedSource/Map/Md5Hash.cs | 354 ++- .../SharedSource/Map/Outposts/NPCSet.cs | 78 +- .../Map/Outposts/OutpostGenerationParams.cs | 203 +- .../Map/Outposts/OutpostGenerator.cs | 164 +- .../Map/Outposts/OutpostModuleInfo.cs | 70 +- .../SharedSource/Map/PriceInfo.cs | 48 +- .../SharedSource/Map/Structure.cs | 72 +- .../SharedSource/Map/StructurePrefab.cs | 453 ++-- .../SharedSource/Map/Submarine.cs | 31 +- .../SharedSource/Map/SubmarineBody.cs | 8 +- .../SharedSource/Map/SubmarineInfo.cs | 66 +- .../SharedSource/Map/WayPoint.cs | 49 +- .../SharedSource/Networking/ChatMessage.cs | 4 +- .../SharedSource/Networking/Client.cs | 6 +- .../Networking/ClientPermissions.cs | 10 +- .../SharedSource/Networking/EntitySpawner.cs | 45 +- .../Networking/FileTransfer/FileTransfer.cs | 2 +- .../Networking/INetSerializableStruct.cs | 84 +- .../SharedSource/Networking/KarmaManager.cs | 68 +- .../SharedSource/Networking/NetConfig.cs | 2 - .../Networking/OrderChatMessage.cs | 94 +- .../Primitives/Message/IReadMessage.cs | 1 + .../Primitives/Message/IWriteMessage.cs | 3 +- .../Networking/Primitives/Message/Message.cs | 27 +- .../NetworkConnection/NetworkConnection.cs | 2 +- .../SharedSource/Networking/RespawnManager.cs | 10 +- .../SharedSource/Networking/ServerLog.cs | 15 +- .../SharedSource/Networking/ServerSettings.cs | 175 +- .../SharedSource/Physics/PhysicsBody.cs | 4 +- .../SharedSource/PlayerInput.cs | 2 +- .../Prefabs/IImplementsVariants.cs | 119 + .../SharedSource/Prefabs/IPrefab.cs | 39 - .../SharedSource/Prefabs/Prefab.cs | 63 + .../SharedSource/Prefabs/PrefabCollection.cs | 407 +++- .../SharedSource/Prefabs/PrefabSelector.cs | 170 ++ .../Prefabs/PrefabWithUintIdentifier.cs | 20 + .../SharedSource/Screens/GameScreen.cs | 33 +- .../SharedSource/Screens/Screen.cs | 30 +- .../Serialization/ISerializableEntity.cs | 4 +- .../Serialization/SerializableProperty.cs | 143 +- .../Serialization/StructSerialization.cs | 233 ++ .../Serialization/XMLExtensions.cs | 317 ++- .../SharedSource/Settings/CreatureMetrics.cs | 13 + .../SharedSource/Settings/GameSettings.cs | 562 +++++ .../SharedSource/Sprite/ConditionalSprite.cs | 4 +- .../SharedSource/Sprite/DeformableSprite.cs | 2 +- .../SharedSource/Sprite/Sprite.cs | 61 +- .../SharedSource/Sprite/SpriteSheet.cs | 4 +- .../StatusEffects/DelayedEffect.cs | 2 +- .../StatusEffects/PropertyConditional.cs | 99 +- .../StatusEffects/StatusEffect.cs | 268 ++- .../SharedSource/Steam/AuthTicket.cs | 18 + .../{Networking => Steam}/SteamManager.cs | 120 +- .../SharedSource/Steam/Workshop.cs | 441 ++++ .../SharedSource/SteamAchievementManager.cs | 113 +- .../AddedPunctuationLString.cs | 35 + .../Text/LocalizedString/CapitalizeLString.cs | 25 + .../Text/LocalizedString/ConcatLString.cs | 21 + .../Text/LocalizedString/FallbackLString.cs | 44 + .../Text/LocalizedString/FormattedLString.cs | 25 + .../Text/LocalizedString/InputTypeLString.cs | 33 + .../Text/LocalizedString/JoinLString.cs | 24 + .../Text/LocalizedString/LocalizedString.cs | 177 ++ .../Text/LocalizedString/LowerLString.cs | 20 + .../Text/LocalizedString/RawLString.cs | 13 + .../Text/LocalizedString/ReplaceLString.cs | 75 + .../Text/LocalizedString/ServerMsgLString.cs | 185 ++ .../Text/LocalizedString/SplitLString.cs | 93 + .../Text/LocalizedString/TagLString.cs | 71 + .../Text/LocalizedString/TrimLString.cs | 28 + .../Text/LocalizedString/UpperLString.cs | 25 + .../SharedSource/Text/RichString.cs | 199 ++ .../SharedSource/Text/TextManager.cs | 404 ++++ .../SharedSource/Text/TextPack.cs | 174 ++ .../SharedSource/TextManager.cs | 951 -------- .../BarotraumaShared/SharedSource/TextPack.cs | 207 -- .../BarotraumaShared/SharedSource/Timing.cs | 17 +- .../Traitors/TraitorMissionResult.cs | 2 +- .../SharedSource/Upgrades/Upgrade.cs | 31 +- .../SharedSource/Upgrades/UpgradePrefab.cs | 371 ++-- .../SharedSource/Utils/CollectionConcat.cs | 124 ++ .../SharedSource/Utils/CursedDictionary.cs | 231 ++ .../SharedSource/Utils/Homoglyphs.cs | 2 +- .../SharedSource/Utils/HttpUtility.cs | 6 +- .../SharedSource/Utils/IdRemap.cs | 4 +- .../SharedSource/Utils/LinkedPairSet.cs | 79 + .../SharedSource/Utils/ListDictionary.cs | 55 + .../SharedSource/Utils/MathUtils.cs | 2 +- .../SharedSource/Utils/Option/None.cs | 9 + .../SharedSource/Utils/Option/Option.cs | 14 + .../SharedSource/Utils/Option/Some.cs | 17 + .../SharedSource/Utils/Rand.cs | 83 +- .../Utils/ReadOnlyListExtensions.cs | 5 +- .../SharedSource/Utils/ReflectionUtils.cs | 15 + .../SharedSource/Utils/Result.cs | 43 + .../SharedSource/Utils/RichTextData.cs | 12 +- .../SharedSource/Utils/SafeIO.cs | 108 +- .../SharedSource/Utils/SaveUtil.cs | 6 +- .../SharedSource/Utils/ToolBox.cs | 77 +- .../BarotraumaShared/Submarines/Azimuth.sub | Bin 232755 -> 0 bytes .../BarotraumaShared/Submarines/Barsuk.sub | Bin 227145 -> 0 bytes .../BarotraumaShared/Submarines/Berilia.sub | Bin 317309 -> 0 bytes .../BarotraumaShared/Submarines/Dugong.sub | Bin 210134 -> 0 bytes .../BarotraumaShared/Submarines/Hemulen.sub | Bin 229261 -> 0 bytes .../BarotraumaShared/Submarines/Herja.sub | Bin 245733 -> 0 bytes .../BarotraumaShared/Submarines/Humpback.sub | Bin 210282 -> 0 bytes .../BarotraumaShared/Submarines/Kastrull.sub | Bin 270838 -> 0 bytes .../Submarines/KastrullDrone.sub | Bin 227231 -> 0 bytes .../BarotraumaShared/Submarines/Orca.sub | Bin 231715 -> 0 bytes .../BarotraumaShared/Submarines/Orca2.sub | Bin 241429 -> 0 bytes .../Submarines/PowerTestSub.sub | Bin 0 -> 8754 bytes .../BarotraumaShared/Submarines/R-29.sub | Bin 226863 -> 0 bytes .../BarotraumaShared/Submarines/Remora.sub | Bin 281036 -> 0 bytes .../Submarines/RemoraDrone.sub | Bin 253496 -> 0 bytes .../BarotraumaShared/Submarines/Selkie.sub | Bin 222929 -> 0 bytes .../BarotraumaShared/Submarines/Typhon.sub | Bin 297793 -> 0 bytes .../BarotraumaShared/Submarines/Typhon2.sub | Bin 292127 -> 0 bytes .../BarotraumaShared/Submarines/Venture.sub | Bin 292049 -> 0 bytes .../Submarines/Winterhalter.sub | Bin 270761 -> 0 bytes Barotrauma/BarotraumaShared/changelog.txt | 73 + Barotrauma/BarotraumaShared/config.xml | 54 - .../Facepunch.Steamworks/Classes/Dispatch.cs | 20 +- .../Generated/Interfaces/ISteamAppList.cs | 6 +- .../Generated/Interfaces/ISteamApps.cs | 12 +- .../Generated/Interfaces/ISteamGameSearch.cs | 3 +- .../Generated/Interfaces/ISteamInventory.cs | 6 +- .../Generated/Interfaces/ISteamMatchmaking.cs | 6 +- .../Interfaces/ISteamNetworkingSockets.cs | 6 +- .../Interfaces/ISteamNetworkingUtils.cs | 9 +- .../Generated/Interfaces/ISteamParties.cs | 6 +- .../Generated/Interfaces/ISteamUGC.cs | 24 +- .../Generated/Interfaces/ISteamUser.cs | 3 +- .../Generated/Interfaces/ISteamUserStats.cs | 6 +- .../Generated/Interfaces/ISteamUtils.cs | 6 +- .../Generated/Interfaces/ISteamVideo.cs | 3 +- .../Networking/NetAddress.cs | 2 +- .../Facepunch.Steamworks/SteamFriends.cs | 2 +- .../Facepunch.Steamworks/SteamMatchmaking.cs | 2 +- Libraries/Facepunch.Steamworks/SteamUgc.cs | 40 +- .../Facepunch.Steamworks/Structs/UgcEditor.cs | 38 +- .../Facepunch.Steamworks/Structs/UgcItem.cs | 70 +- .../Facepunch.Steamworks/Structs/UgcQuery.cs | 15 - .../Facepunch.Steamworks/Utility/Helpers.cs | 53 +- .../Graphics/GraphicsResource.cs | 38 +- .../Graphics/SpriteBatch.cs | 12 +- .../Graphics/TextureCollection.cs | 2 +- .../MonoGame.Framework/Input/Keyboard.SDL.cs | 38 + .../Src/MonoGame.Framework/SDL/SDL2.cs | 4 + Libraries/XNATypes/Point.cs | 1 + Libraries/XNATypes/Vector2.cs | 1 + Libraries/XNATypes/Vector3.cs | 1 + Libraries/XNATypes/Vector4.cs | 1 + 913 files changed, 32472 insertions(+), 32364 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs rename Barotrauma/BarotraumaServer/ServerSource/{Networking => Steam}/SteamManager.cs (86%) delete mode 100644 Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml create mode 100644 Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub create mode 100644 Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml rename Barotrauma/BarotraumaShared/{Mods => LocalMods}/info.txt (97%) delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerRun.xml delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png delete mode 100644 Barotrauma/BarotraumaShared/Mods/ExampleMod/filelist.xml create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DecalsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MapGenerationParametersFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SkillSettingsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UIStyleFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Extensions/StringExtensions.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs rename Barotrauma/BarotraumaShared/SharedSource/{Networking => Steam}/SteamManager.cs (71%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/TextManager.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/TextPack.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Azimuth.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Barsuk.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Berilia.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Dugong.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Hemulen.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Herja.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Humpback.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Kastrull.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Orca.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Orca2.sub create mode 100644 Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/R-29.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Remora.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Selkie.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Typhon.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Typhon2.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Venture.sub delete mode 100644 Barotrauma/BarotraumaShared/Submarines/Winterhalter.sub delete mode 100644 Barotrauma/BarotraumaShared/config.xml diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index f0182f806..e8d9f99f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -13,7 +13,7 @@ namespace Barotrauma private float? defaultZoom; public float DefaultZoom { - get { return defaultZoom ?? (GameMain.Config == null || GameMain.Config.EnableMouseLook ? 1.3f : 1.0f); } + get { return defaultZoom ?? (GameSettings.CurrentConfig.EnableMouseLook ? 1.3f : 1.0f); } set { defaultZoom = MathHelper.Clamp(value, 0.5f, 2.0f); @@ -269,10 +269,10 @@ namespace Barotrauma if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } - if (GameMain.Config.KeyBind(InputType.Left).IsDown()) { moveInput.X -= 1.0f; } - if (GameMain.Config.KeyBind(InputType.Right).IsDown()) { moveInput.X += 1.0f; } - if (GameMain.Config.KeyBind(InputType.Down).IsDown()) { moveInput.Y -= 1.0f; } - if (GameMain.Config.KeyBind(InputType.Up).IsDown()) { moveInput.Y += 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Left].IsDown()) { moveInput.X -= 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Right].IsDown()) { moveInput.X += 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down].IsDown()) { moveInput.Y -= 1.0f; } + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up].IsDown()) { moveInput.Y += 1.0f; } } velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); @@ -346,7 +346,7 @@ namespace Barotrauma float scaledZoom = MathHelper.Lerp(DefaultZoom, MinZoom, zoomOutAmount) * globalZoomScale; //zoom in further if zoomOutAmount is low and resolution is lower than reference float newZoom = scaledZoom * (MathHelper.Lerp(0.3f * (1f - Math.Min(globalZoomScale, 1f)), 0f, - (GameMain.Config == null || GameMain.Config.EnableMouseLook) ? (float)Math.Sqrt(offsetUnscaledLen) : 0.3f) + 1f); + (GameSettings.CurrentConfig.EnableMouseLook) ? (float)Math.Sqrt(offsetUnscaledLen) : 0.3f) + 1f); Zoom += (newZoom - zoom) / ZoomSmoothness; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index eacea79b3..9b31bf9bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -37,7 +37,8 @@ namespace Barotrauma targetPos = attackWorldPos; } targetPos.Y = -targetPos.Y; - GUI.DrawLine(spriteBatch, pos, targetPos, GUI.Style.Red * 0.5f, 0, 4); + + GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; @@ -46,19 +47,19 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUI.Style.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity} ({GetTargetMemory(SelectedAiTarget, false)?.Priority.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", GUIStyle.Red, Color.Black); } - /*GUI.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUI.Style.Red); - GUI.Font.DrawString(spriteBatch, "updatetargets: " + MathUtils.Round(updateTargetsTimer, 0.1f), pos - Vector2.UnitY * 100.0f, GUI.Style.Red); - GUI.Font.DrawString(spriteBatch, "cooldown: " + MathUtils.Round(coolDownTimer, 0.1f), pos - Vector2.UnitY * 120.0f, GUI.Style.Red);*/ + /*GUIStyle.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, GUIStyle.Red); + GUIStyle.Font.DrawString(spriteBatch, "updatetargets: " + MathUtils.Round(updateTargetsTimer, 0.1f), pos - Vector2.UnitY * 100.0f, GUIStyle.Red); + GUIStyle.Font.DrawString(spriteBatch, "cooldown: " + MathUtils.Round(coolDownTimer, 0.1f), pos - Vector2.UnitY * 120.0f, GUIStyle.Red);*/ Color stateColor = Color.White; switch (State) { case AIState.Attack: - stateColor = IsCoolDownRunning ? Color.Orange : GUI.Style.Red; + stateColor = IsCoolDownRunning ? Color.Orange : GUIStyle.Red; break; case AIState.Escape: stateColor = Color.LightBlue; @@ -78,13 +79,13 @@ namespace Barotrauma { GUI.DrawLine(spriteBatch, ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorA.X, -attachJoint.WorldAnchorA.Y)), - ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorB.X, -attachJoint.WorldAnchorB.Y)), GUI.Style.Green, 0, 4); + ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorB.X, -attachJoint.WorldAnchorB.Y)), GUIStyle.Green, 0, 4); } if (LatchOntoAI.AttachPos.HasValue) { GUI.DrawLine(spriteBatch, pos, - ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.AttachPos.Value.X, -LatchOntoAI.AttachPos.Value.Y)), GUI.Style.Green, 0, 3); + ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.AttachPos.Value.X, -LatchOntoAI.AttachPos.Value.Y)), GUIStyle.Green, 0, 3); } } @@ -108,12 +109,12 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(currentNode.DrawPosition.X, -currentNode.DrawPosition.Y), new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), - GUI.Style.Red * 0.5f, 0, 3); + GUIStyle.Red * 0.5f, 0, 3); - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, currentNode.ID.ToString(), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), - GUI.Style.Red); + GUIStyle.Red); } } } @@ -124,7 +125,7 @@ namespace Barotrauma Vector2 hitPos = ConvertUnits.ToDisplayUnits(steeringManager.AvoidRayCastHitPosition); hitPos.Y = -hitPos.Y; - GUI.DrawLine(spriteBatch, hitPos, hitPos + new Vector2(steeringManager.AvoidDir.X, -steeringManager.AvoidDir.Y) * 100, GUI.Style.Red, width: 5); + GUI.DrawLine(spriteBatch, hitPos, hitPos + new Vector2(steeringManager.AvoidDir.X, -steeringManager.AvoidDir.Y) * 100, GUIStyle.Red, width: 5); //GUI.DrawLine(spriteBatch, pos, ConvertUnits.ToDisplayUnits(steeringManager.AvoidLookAheadPos.X, -steeringManager.AvoidLookAheadPos.Y), Color.Orange, width: 4); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 42ffa3a40..4be093613 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -18,7 +18,7 @@ namespace Barotrauma if (SelectedAiTarget?.Entity != null) { - //GUI.DrawLine(spriteBatch, pos, new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), GUI.Style.Red); + //GUI.DrawLine(spriteBatch, pos, new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), GUIStyle.Red); //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } @@ -87,7 +87,7 @@ namespace Barotrauma new Vector2(previousNode.DrawPosition.X, -previousNode.DrawPosition.Y), Color.Blue * 0.5f, 0, 3); - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, currentNode.ID.ToString(), new Vector2(currentNode.DrawPosition.X - 10, -currentNode.DrawPosition.Y - 30), Color.Blue); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs index 0c21b2956..a1c712e7c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Objectives/AIObjective.cs @@ -6,16 +6,16 @@ namespace Barotrauma { public static Color ObjectiveIconColor => Color.LightGray; - public static Sprite GetSprite(string identifier, string option, Entity targetEntity) + public static Sprite GetSprite(Identifier identifier, Identifier option, Entity targetEntity) { - if (string.IsNullOrEmpty(identifier)) + if (identifier == Identifier.Empty) { return null; } - identifier = identifier.RemoveWhitespace(); - if (Order.Prefabs.TryGetValue(identifier, out Order orderPrefab)) + if (OrderPrefab.Prefabs.ContainsKey(identifier)) { - if (!string.IsNullOrEmpty(option) && orderPrefab.OptionSprites.TryGetValue(option, out var optionSprite)) + OrderPrefab orderPrefab = OrderPrefab.Prefabs[identifier]; + if (option != Identifier.Empty && orderPrefab.OptionSprites.TryGetValue(option, out var optionSprite)) { return optionSprite; } @@ -25,7 +25,7 @@ namespace Barotrauma } return orderPrefab.SymbolSprite; } - return GUI.Style.GetComponentStyle($"{identifier}objectiveicon")?.GetDefaultSprite(); + return GUIStyle.GetComponentStyle($"{identifier}objectiveicon")?.GetDefaultSprite(); } public Sprite GetSprite() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index f674cc079..6904554bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -28,13 +28,13 @@ namespace Barotrauma { if (item.Prefab.BrokenSprites.None()) { - Color c = item.prefab.SpriteColor; + Color c = item.Prefab.SpriteColor; item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } } foreach (var structure in thalamusStructures) { - Color c = structure.prefab.SpriteColor; + Color c = structure.Prefab.SpriteColor; structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); } yield return CoroutineStatus.Running; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 9d3900b80..5e315ee8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -469,7 +469,7 @@ namespace Barotrauma Color? color = null; if (character.ExternalHighlight) { - color = Color.Lerp(Color.White, GUI.Style.Orange, (float)Math.Sin(Timing.TotalTime * 3.5f)); + color = Color.Lerp(Color.White, GUIStyle.Orange, (float)Math.Sin(Timing.TotalTime * 3.5f)); } float depthOffset = GetDepthOffset(); @@ -564,7 +564,7 @@ namespace Barotrauma Vector2 pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorB); if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; pos.Y = -pos.Y; - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)pos.Y, 5, 5), GUI.Style.Red, true, 0.01f); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)pos.Y, 5, 5), GUIStyle.Red, true, 0.01f); pos = ConvertUnits.ToDisplayUnits(limb.PullJointWorldAnchorA); if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; @@ -575,8 +575,8 @@ namespace Barotrauma limb.body.DebugDraw(spriteBatch, inWater ? (currentHull == null ? Color.Blue : Color.Cyan) : Color.White); } - Collider.DebugDraw(spriteBatch, frozen ? GUI.Style.Red : (inWater ? Color.SkyBlue : Color.Gray)); - GUI.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); + Collider.DebugDraw(spriteBatch, frozen ? GUIStyle.Red : (inWater ? Color.SkyBlue : Color.Gray)); + GUIStyle.Font.DrawString(spriteBatch, Collider.LinearVelocity.X.FormatSingleDecimal(), new Vector2(Collider.DrawPosition.X, -Collider.DrawPosition.Y), Color.Orange); foreach (var joint in LimbJoints) { @@ -607,10 +607,10 @@ namespace Barotrauma { Vector2 pos = ConvertUnits.ToDisplayUnits(humanoid.RightHandIKPos); if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUIStyle.Green, true); pos = ConvertUnits.ToDisplayUnits(humanoid.LeftHandIKPos); if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.DrawPosition; } - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUIStyle.Green, true); Vector2 aimPos = humanoid.AimSourceWorldPos; aimPos.Y = -aimPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 480a5fd26..2c7bc106e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -6,14 +6,14 @@ namespace Barotrauma { partial class Attack { - [Serialize("StructureBlunt", true), Editable()] + [Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()] public string StructureSoundType { get; private set; } private RoundSound sound; private ParticleEmitter particleEmitter; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { if (element.Attribute("sound") != null) { @@ -21,7 +21,7 @@ namespace Barotrauma return; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -29,7 +29,7 @@ namespace Barotrauma particleEmitter = new ParticleEmitter(subElement); break; case "sound": - sound = Submarine.LoadRoundSound(subElement); + sound = RoundSound.Load(subElement); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 89ca9a2f7..4bd56bce4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -131,7 +131,7 @@ namespace Barotrauma private class GUIMessage { public string RawText; - public string Identifier; + public Identifier Identifier; public string Text; private int _value; @@ -142,7 +142,7 @@ namespace Barotrauma { _value = value; Text = RawText.Replace("[value]", _value.ToString()); - Size = GUI.Font.MeasureString(Text); + Size = GUIStyle.Font.MeasureString(Text); } } @@ -154,7 +154,7 @@ namespace Barotrauma public bool PlaySound; - public GUIMessage(string rawText, Color color, float delay, string identifier = null, int? value = null, float lifeTime = 3.0f) + public GUIMessage(string rawText, Color color, float delay, Identifier identifier = default, int? value = null, float lifeTime = 3.0f) { RawText = Text = rawText; if (value.HasValue) @@ -163,7 +163,7 @@ namespace Barotrauma Value = value.Value; } Timer = -delay; - Size = GUI.Font.MeasureString(Text); + Size = GUIStyle.Font.MeasureString(Text); Color = color; Identifier = identifier; Lifetime = lifeTime; @@ -202,14 +202,14 @@ namespace Barotrauma get { return activeObjectiveEntities; } } - partial void InitProjSpecific(XElement mainElement) + partial void InitProjSpecific(ContentXElement mainElement) { soundTimer = Rand.Range(0.0f, Params.SoundInterval); sounds = new List(); Params.Sounds.ForEach(s => sounds.Add(new CharacterSound(s))); - foreach (XElement subElement in mainElement.Elements()) + foreach (var subElement in mainElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -267,11 +267,11 @@ namespace Barotrauma //and the fire key is the same as Select or Use, reset the key to prevent accidentally selecting/using items if (wasFiring && !keys[(int)InputType.Shoot].Held) { - if (GameMain.Config.KeyBind(InputType.Shoot).Equals(GameMain.Config.KeyBind(InputType.Select))) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Shoot] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select]) { keys[(int)InputType.Select].Reset(); } - if (GameMain.Config.KeyBind(InputType.Shoot).Equals(GameMain.Config.KeyBind(InputType.Use))) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Shoot] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use]) { keys[(int)InputType.Use].Reset(); } @@ -331,7 +331,7 @@ namespace Barotrauma Position + PlayerInput.MouseSpeed.ClampLength(10.0f); //apply a little bit of movement to the cursor pos to prevent AFK kicking } - else if (!GameMain.Config.EnableMouseLook) + else if (!GameSettings.CurrentConfig.EnableMouseLook) { cam.OffsetAmount = targetOffsetAmount = 0.0f; } @@ -446,15 +446,15 @@ namespace Barotrauma if (GameMain.NetworkMember != null && controlled == this) { - string chatMessage = CauseOfDeath.Type == CauseOfDeathType.Affliction ? + LocalizedString chatMessage = CauseOfDeath.Type == CauseOfDeathType.Affliction ? CauseOfDeath.Affliction.SelfCauseOfDeathDescription : - TextManager.Get("Self_CauseOfDeathDescription." + CauseOfDeath.Type.ToString(), fallBackTag: "Self_CauseOfDeathDescription.Damage"); + TextManager.Get("Self_CauseOfDeathDescription." + CauseOfDeath.Type.ToString(), "Self_CauseOfDeathDescription.Damage"); if (GameMain.Client != null) { chatMessage += " " + TextManager.Get("DeathChatNotification"); } GameMain.NetworkMember.RespawnManager?.ShowRespawnPromptIfNeeded(); - GameMain.NetworkMember.AddChatMessage(chatMessage, ChatMessageType.Dead); + GameMain.NetworkMember.AddChatMessage(chatMessage.Value, ChatMessageType.Dead); GameMain.LightManager.LosEnabled = false; controlled = null; if (!(Screen.Selected?.Cam is null)) @@ -726,9 +726,9 @@ namespace Barotrauma } } - partial void SetOrderProjSpecific(Order order, string orderOption, int priority) + partial void SetOrderProjSpecific(Order order) { - GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption, priority); + GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order); } public static void AddAllToGUIUpdateList() @@ -812,7 +812,7 @@ namespace Barotrauma Controlled != this && Submarine != null && Controlled.Submarine == Submarine && - GameMain.Config.LosMode != LosMode.None) + GameSettings.CurrentConfig.Graphics.LosMode != LosMode.None) { float yPos = Controlled.AnimController.FloorY - 1.5f; @@ -854,7 +854,7 @@ namespace Barotrauma if (speechBubbleTimer > 0.0f) { - GUI.SpeechBubbleIcon.Draw(spriteBatch, pos - Vector2.UnitY * 5, + GUIStyle.SpeechBubbleIcon.Value.Sprite.Draw(spriteBatch, pos - Vector2.UnitY * 5, speechBubbleColor * Math.Min(speechBubbleTimer, 1.0f), 0.0f, Math.Min(speechBubbleTimer, 1.0f)); } @@ -880,7 +880,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, cursorPos, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), - ToolBox.GradientLerp(dist, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green), width: 2); + ToolBox.GradientLerp(dist, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green), width: 2); } } return; @@ -899,10 +899,10 @@ namespace Barotrauma { if (info != null) { - string name = Info.DisplayName; + LocalizedString name = Info.DisplayName; if (controlled == null && name != Info.Name) { name += " " + TextManager.Get("Disguised"); } - Vector2 nameSize = GUI.Font.MeasureString(name); + Vector2 nameSize = GUIStyle.Font.MeasureString(name); Vector2 namePos = new Vector2(pos.X, pos.Y - 10.0f - (5.0f / cam.Zoom)) - nameSize * 0.5f / cam.Zoom; Color nameColor = GetNameColor(); @@ -916,7 +916,7 @@ namespace Barotrauma if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { - var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); + var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; @@ -929,11 +929,11 @@ namespace Barotrauma } } - GUI.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); - GUI.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos + new Vector2(1.0f / cam.Zoom, 1.0f / cam.Zoom), Color.Black, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.001f); + GUIStyle.Font.DrawString(spriteBatch, name, namePos, nameColor * hudInfoAlpha, 0.0f, Vector2.Zero, 1.0f / cam.Zoom, SpriteEffects.None, 0.0f); if (GameMain.DebugDraw) { - GUI.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); + GUIStyle.Font.DrawString(spriteBatch, ID.ToString(), namePos - new Vector2(0.0f, 20.0f), Color.White); } } @@ -941,7 +941,7 @@ namespace Barotrauma if (petBehavior != null && !IsDead && !IsUnconscious) { var petStatus = petBehavior.GetCurrentStatusIndicatorType(); - var iconStyle = GUI.Style.GetComponentStyle("PetIcon." + petStatus); + var iconStyle = GUIStyle.GetComponentStyle("PetIcon." + petStatus); if (iconStyle != null) { Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; @@ -963,7 +963,7 @@ namespace Barotrauma Vector2 healthBarPos = new Vector2(pos.X - 50, -pos.Y); GUI.DrawProgressBar(spriteBatch, healthBarPos, new Vector2(100.0f, 15.0f), CharacterHealth.DisplayedVitality / MaxVitality, - Color.Lerp(GUI.Style.Red, GUI.Style.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, + Color.Lerp(GUIStyle.Red, GUIStyle.Green, CharacterHealth.DisplayedVitality / MaxVitality) * 0.8f * hudInfoAlpha, new Color(0.5f, 0.57f, 0.6f, 1.0f) * hudInfoAlpha); } } @@ -987,7 +987,7 @@ namespace Barotrauma } } - Color nameColor = GUI.Style.TextColor; + Color nameColor = GUIStyle.TextColorNormal; if (Controlled != null && team != Controlled.TeamID) { if (TeamID == CharacterTeamType.FriendlyNPC) @@ -996,13 +996,13 @@ namespace Barotrauma } else { - nameColor = GUI.Style.Red; + nameColor = GUIStyle.Red; } } return nameColor; } - public void AddMessage(string rawText, Color color, bool playSound, string identifier = null, int? value = null, float lifetime = 3.0f) + public void AddMessage(string rawText, Color color, bool playSound, Identifier identifier = default, int? value = null, float lifetime = 3.0f) { GUIMessage existingMessage = null; @@ -1089,12 +1089,12 @@ namespace Barotrauma matchingSounds.Clear(); foreach (var s in sounds) { - if (s.Type == soundType && (s.Gender == Gender.None || (info != null && info.Gender == s.Gender))) + if (s.Type == soundType && (s.TagSet.None() || (info != null && s.TagSet.IsSubsetOf(info.Head.Preset.TagSet)))) { matchingSounds.Add(s); } } - var selectedSound = matchingSounds.GetRandom(); + var selectedSound = matchingSounds.GetRandomUnsynced(); if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull, ignoreMuffling: selectedSound.IgnoreMuffling); soundTimer = Params.SoundInterval; @@ -1117,7 +1117,7 @@ namespace Barotrauma /// /// Note that when a predicate is provided, the random option uses Linq.Where() extension method, which creates a new collection. /// - public CharacterSound GetSound(Func predicate = null, bool random = false) => random ? sounds.GetRandom(predicate) : sounds.FirstOrDefault(predicate); + public CharacterSound GetSound(Func predicate = null, bool random = false) => random ? sounds.GetRandomUnsynced(predicate) : sounds.FirstOrDefault(predicate); partial void ImplodeFX() { @@ -1156,14 +1156,14 @@ namespace Barotrauma if (newAmount > prevAmount) { int increase = newAmount - prevAmount; - AddMessage("+" + TextManager.GetWithVariable("currencyformat", "[credits]", "[value]"), - GUI.Style.Yellow, playSound: this == Controlled, "money", increase); + AddMessage("+" + TextManager.GetWithVariable("currencyformat", "[credits]", "[value]").Value, + GUIStyle.Yellow, playSound: this == Controlled, "money".ToIdentifier(), increase); } } partial void OnTalentGiven(TalentPrefab talentPrefab) { - AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier), GUI.Style.Yellow, playSound: this == Controlled); + AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier).Value, GUIStyle.Yellow, playSound: this == Controlled); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 0455691f9..a911b5c70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -35,23 +35,23 @@ namespace Barotrauma MinSize = new Point(100, 50), RelativeOffset = new Vector2(0.0f, 0.01f) }, isHorizontal: false, childAnchor: Anchor.TopCenter); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUI.Style.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), TopContainer.RectTransform), character.DisplayName, textAlignment: Alignment.Center, textColor: GUIStyle.Red); TopHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.6f), TopContainer.RectTransform) { MinSize = new Point(100, HUDLayoutSettings.HealthBarArea.Size.Y) }, barSize: 0.0f, style: "CharacterHealthBarCentered") { - Color = GUI.Style.Red + Color = GUIStyle.Red }; SideContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), bossHealthContainer.RectTransform) { MinSize = new Point(80, 60) }, isHorizontal: false, childAnchor: Anchor.TopRight); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUI.Style.Red); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), SideContainer.RectTransform), character.DisplayName, textAlignment: Alignment.CenterRight, textColor: GUIStyle.Red); SideHealthBar = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.7f), SideContainer.RectTransform), barSize: 0.0f, style: "CharacterHealthBar") { - Color = GUI.Style.Red + Color = GUIStyle.Red }; TopContainer.Visible = SideContainer.Visible = false; @@ -72,7 +72,7 @@ namespace Barotrauma private static readonly List bossHealthBars = new List(); - private static readonly Dictionary cachedHudTexts = new Dictionary(); + private static readonly Dictionary cachedHudTexts = new Dictionary(); private static GUILayoutGroup bossHealthContainer; @@ -119,14 +119,12 @@ namespace Barotrauma !ConversationAction.FadeScreenToBlack; } - public static string GetCachedHudText(string textTag, string keyBind) + public static LocalizedString GetCachedHudText(string textTag, InputType keyBind) { - if (cachedHudTexts.TryGetValue(textTag + keyBind, out string text)) - { - return text; - } - text = TextManager.GetWithVariable(textTag, "[key]", keyBind); - cachedHudTexts.Add(textTag + keyBind, text); + Identifier key = (textTag + keyBind).ToIdentifier(); + if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } + text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; + cachedHudTexts.Add(key, text); return text; } @@ -256,24 +254,24 @@ namespace Barotrauma if (GameMain.GameSession?.CrewManager != null) { orderIndicatorCount.Clear(); - foreach (Pair activeOrder in GameMain.GameSession.CrewManager.ActiveOrders) + foreach (CrewManager.ActiveOrder activeOrder in GameMain.GameSession.CrewManager.ActiveOrders) { - if (!DrawIcon(activeOrder.First)) { continue; } + if (!DrawIcon(activeOrder.Order)) { continue; } - if (activeOrder.Second.HasValue) + if (activeOrder.FadeOutTime.HasValue) { - DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, iconAlpha: MathHelper.Clamp(activeOrder.Second.Value / 10.0f, 0.2f, 1.0f)); + DrawOrderIndicator(spriteBatch, cam, character, activeOrder.Order, iconAlpha: MathHelper.Clamp(activeOrder.FadeOutTime.Value / 10.0f, 0.2f, 1.0f)); } else { - float iconAlpha = GetDistanceBasedIconAlpha(activeOrder.First.TargetSpatialEntity, maxDistance: 450.0f); + float iconAlpha = GetDistanceBasedIconAlpha(activeOrder.Order.TargetSpatialEntity, maxDistance: 450.0f); if (iconAlpha <= 0.0f) { continue; } - DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, + DrawOrderIndicator(spriteBatch, cam, character, activeOrder.Order, iconAlpha: iconAlpha, createOffset: false, scaleMultiplier: 0.5f, overrideAlpha: true); } } - if (character.GetCurrentOrderWithTopPriority()?.Order is Order currentOrder && DrawIcon(currentOrder)) + if (character.GetCurrentOrderWithTopPriority() is Order currentOrder && DrawIcon(currentOrder)) { DrawOrderIndicator(spriteBatch, cam, character, currentOrder, 1.0f); } @@ -310,8 +308,8 @@ namespace Barotrauma if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); if (alpha <= 0.0f) continue; - GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUI.BrokenIcon, - Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); + GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite, + Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f) @@ -344,12 +342,12 @@ namespace Barotrauma circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); if (circleSize > 0.0f) { - Vector2 scale = new Vector2(circleSize / GUI.Style.FocusIndicator.FrameSize.X); - GUI.Style.FocusIndicator.Draw(spriteBatch, - (int)((focusedItemOverlayTimer - 1.0f) * GUI.Style.FocusIndicator.FrameCount * 3.0f), + Vector2 scale = new Vector2(circleSize / GUIStyle.FocusIndicator.FrameSize.X); + GUIStyle.FocusIndicator.Draw(spriteBatch, + (int)((focusedItemOverlayTimer - 1.0f) * GUIStyle.FocusIndicator.FrameCount * 3.0f), circlePos, Color.LightBlue * 0.3f, - origin: GUI.Style.FocusIndicator.FrameSize.ToVector2() / 2, + origin: GUIStyle.FocusIndicator.FrameSize.ToVector2() / 2, rotate: (float)Timing.TotalTime, scale: scale); } @@ -367,8 +365,8 @@ namespace Barotrauma int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); - Vector2 textSize = GUI.Font.MeasureString(hudTexts.First().Text); - Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(hudTexts.First().Text); + Vector2 textSize = GUIStyle.Font.MeasureString(hudTexts.First().Text); + Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(hudTexts.First().Text); Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); startPos.Y -= (hudTexts.Count + 1) * textSize.Y; @@ -383,14 +381,14 @@ namespace Barotrauma float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); - GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUIStyle.SubHeadingFont, ForceUpperCase.No); startPos.X += dir * 10.0f * GUI.Scale; textPos.X += dir * 10.0f * GUI.Scale; textPos.Y += largeTextSize.Y; foreach (ColoredText coloredText in hudTexts.Skip(1)) { - if (dir == -1) textPos.X = (int)(startPos.X - GUI.SmallFont.MeasureString(coloredText.Text).X); - GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUI.SmallFont); + if (dir == -1) textPos.X = (int)(startPos.X - GUIStyle.SmallFont.MeasureString(coloredText.Text).X); + GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } } @@ -405,7 +403,7 @@ namespace Barotrauma { if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; } - var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); + var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType); if (iconStyle == null) { continue; } Range visibleRange = new Range(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) @@ -491,7 +489,7 @@ namespace Barotrauma mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); if (mouseOnPortrait) { - GUI.UIGlow.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea, GUI.Style.Green * 0.5f); + GUIStyle.UIGlow.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea, GUIStyle.Green * 0.5f); } } if (ShouldDrawInventory(character)) @@ -555,28 +553,28 @@ namespace Barotrauma string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; Vector2 textPos = startPos; - Vector2 textSize = GUI.Font.MeasureString(focusName); - Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusName); + Vector2 textSize = GUIStyle.Font.MeasureString(focusName); + Vector2 largeTextSize = GUIStyle.SubHeadingFont.MeasureString(focusName); textPos -= new Vector2(textSize.X / 2, textSize.Y); Color nameColor = character.FocusedCharacter.GetNameColor(); - GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); textPos.X += 10.0f * GUI.Scale; - textPos.Y += GUI.SubHeadingFont.MeasureString(focusName).Y; + textPos.Y += GUIStyle.SubHeadingFont.MeasureString(focusName).Y; if (!character.FocusedCharacter.IsIncapacitated && character.FocusedCharacter.IsPet) { - GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", GameMain.Config.KeyBindText(InputType.Use)), - GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, GetCachedHudText("PlayHint", InputType.Use), + GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += largeTextSize.Y; } if (character.FocusedCharacter.CanBeDragged) { string text = character.CanEat ? "EatHint" : "GrabHint"; - GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, GameMain.Config.KeyBindText(InputType.Grab)), - GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, GetCachedHudText(text, InputType.Grab), + GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += largeTextSize.Y; } @@ -585,13 +583,13 @@ namespace Barotrauma character.FocusedCharacter.CharacterHealth.UseHealthWindow && character.CanInteractWith(character.FocusedCharacter, 160f, false)) { - GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", GameMain.Config.KeyBindText(InputType.Health)), - GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, GetCachedHudText("HealHint", InputType.Health), + GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } - if (!string.IsNullOrEmpty(character.FocusedCharacter.customInteractHUDText) && character.FocusedCharacter.AllowCustomInteract) + if (!character.FocusedCharacter.CustomInteractHUDText.IsNullOrEmpty() && character.FocusedCharacter.AllowCustomInteract) { - GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.customInteractHUDText, GUI.Style.Green, Color.Black, 2, GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos, character.FocusedCharacter.CustomInteractHUDText, GUIStyle.Green, Color.Black, 2, GUIStyle.SmallFont); textPos.Y += textSize.Y; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 025aadcd0..5d2358563 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Graphics; using System.Xml.Linq; using Barotrauma.IO; using Barotrauma.Items.Components; +using System.Collections.Immutable; namespace Barotrauma { @@ -34,17 +35,17 @@ namespace Barotrauma public static void Init() { - infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); + infoAreaPortraitBG = GUIStyle.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } - partial void LoadHeadSpriteProjectSpecific(XElement limbElement) + partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement) { - XElement maskElement = limbElement.Element("tintmask"); + ContentXElement maskElement = limbElement.GetChildElement("tintmask"); if (maskElement != null) { - string tintMaskPath = maskElement.GetAttributeString("texture", ""); - if (!string.IsNullOrWhiteSpace(tintMaskPath)) + ContentPath tintMaskPath = maskElement.GetAttributeContentPath("texture"); + if (!tintMaskPath.IsNullOrEmpty()) { tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); @@ -66,7 +67,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); - ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; + GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font; var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { @@ -77,9 +78,9 @@ namespace Barotrauma Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUI.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUI.Font) + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUIStyle.Font) { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero }; @@ -98,9 +99,11 @@ namespace Barotrauma }; } - if (personalityTrait != null) + if (PersonalityTrait != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), + TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + PersonalityTrait.Name.Replace(" ".ToIdentifier(), "".ToIdentifier()))), + font: font) { Padding = Vector4.Zero }; @@ -148,10 +151,10 @@ namespace Barotrauma Stretch = true }; - string deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? + LocalizedString deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + Character.CauseOfDeath.Type.ToString()))); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUI.Style.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUIStyle.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; } if (returnParent) @@ -182,13 +185,13 @@ namespace Barotrauma Color? textColor = null; if (Job != null) { textColor = Job.Prefab.UIColor; } - GUITextBlock textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(40, 0) }, text, textColor: textColor, font: GUI.SmallFont); + GUITextBlock textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(40, 0) }, text, textColor: textColor, font: GUIStyle.SmallFont); new GUICustomComponent(new RectTransform(new Point(frame.Rect.Height, frame.Rect.Height), frame.RectTransform, Anchor.CenterLeft) { IsFixedSize = false }, onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); return frame; } - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel) + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } @@ -199,9 +202,10 @@ namespace Barotrauma if ((int)newLevel > (int)prevLevel) { int increase = Math.Max((int)newLevel - (int)prevLevel, 1); + Character?.AddMessage( - "+[value] "+ TextManager.Get("SkillName." + skillIdentifier), - specialIncrease ? GUI.Style.Orange : GUI.Style.Green, + "+[value] "+ TextManager.Get("SkillName." + skillIdentifier).Value, + specialIncrease ? GUIStyle.Orange : GUIStyle.Green, playSound: Character == Character.Controlled, skillIdentifier, increase); } } @@ -216,8 +220,8 @@ namespace Barotrauma { int increase = newAmount - prevAmount; Character?.AddMessage( - "+[value] " + TextManager.Get("experienceshort"), - GUI.Style.Blue, playSound: Character == Character.Controlled, "exp", increase); + "+[value] " + TextManager.Get("experienceshort").Value, + GUIStyle.Blue, playSound: Character == Character.Controlled, "exp".ToIdentifier(), increase); } } @@ -227,9 +231,13 @@ namespace Barotrauma if (idCard.StoredOwnerAppearance.JobPrefab == null || idCard.StoredOwnerAppearance.Portrait == null) { - string[] readTags = idCard.Item.Tags.Split(','); + var readTags = idCard.Item.Tags.Split(',') + .Where(s => s.Contains(':')) + .Select(s => s.Split(':')) + .Select(s => (s[0].ToIdentifier(),s[1])) + .ToImmutableDictionary(); - if (readTags.Length == 0) { return; } + if (readTags.None()) { return; } if (idCard.StoredOwnerAppearance.JobPrefab == null) { @@ -238,7 +246,7 @@ namespace Barotrauma if (idCard.StoredOwnerAppearance.Portrait == null) { - idCard.StoredOwnerAppearance.ExtractAppearance(this, readTags); + idCard.StoredOwnerAppearance.ExtractAppearance(this, idCard); } } @@ -267,17 +275,17 @@ namespace Barotrauma { LoadHeadAttachments(); } - FaceAttachment?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.FaceAttachment))); - BeardElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); - MoustacheElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); - HairElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); + Head.FaceAttachment?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.FaceAttachment))); + 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?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + JobPrefab.NoJobElement?.GetChildElement("PortraitClothing")?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); } else { - Job?.Prefab.ClothingElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + Job?.Prefab.ClothingElement?.GetChildElements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); } } @@ -288,7 +296,7 @@ namespace Barotrauma { if (sprite == null) { return; } if (Head.SheetIndex == null) { return; } - Point location = CalculateOffset(sprite, Head.SheetIndex.Value.ToPoint()); + Point location = CalculateOffset(sprite, Head.SheetIndex.ToPoint()); sprite.SourceRect = new Rectangle(location, sprite.SourceRect.Size); } @@ -414,19 +422,16 @@ namespace Barotrauma { var currEffect = spriteBatch.GetCurrentEffect(); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); - if (Head.SheetIndex.HasValue) - { - headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.Value.ToPoint()), headSprite.SourceRect.Size); - } + headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); SetHeadEffect(spriteBatch); - headSprite.Draw(spriteBatch, screenPos, scale: scale, color: SkinColor); + headSprite.Draw(spriteBatch, screenPos, scale: scale, color: Head.SkinColor); if (AttachmentSprites != null) { float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { SetAttachmentEffect(spriteBatch, attachment); - DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, HairColor, FacialHairColor)); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, Head.HairColor, Head.FacialHairColor)); depthStep += depthStep; } } @@ -479,14 +484,17 @@ namespace Barotrauma attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } - public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) + public static CharacterInfo ClientRead(Identifier speciesName, IReadMessage inc) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); string originalName = inc.ReadString(); - int gender = inc.ReadByte(); - int race = inc.ReadByte(); - int headSpriteID = inc.ReadByte(); + int tagCount = inc.ReadByte(); + HashSet tagSet = new HashSet(); + for (int i = 0; i < tagCount; i++) + { + tagSet.Add(inc.ReadIdentifier()); + } int hairIndex = inc.ReadByte(); int beardIndex = inc.ReadByte(); int moustacheIndex = inc.ReadByte(); @@ -500,14 +508,14 @@ namespace Barotrauma int variant = inc.ReadByte(); JobPrefab jobPrefab = null; - Dictionary skillLevels = new Dictionary(); + Dictionary skillLevels = new Dictionary(); if (!string.IsNullOrEmpty(jobIdentifier)) { jobPrefab = JobPrefab.Get(jobIdentifier); byte skillCount = inc.ReadByte(); for (int i = 0; i < skillCount; i++) { - string skillIdentifier = inc.ReadString(); + Identifier skillIdentifier = inc.ReadIdentifier(); float skillLevel = inc.ReadSingle(); skillLevels.Add(skillIdentifier, skillLevel); } @@ -518,14 +526,14 @@ namespace Barotrauma { ID = infoID, }; - ch.RecreateHead(headSpriteID,(Race)race, (Gender)gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - ch.SkinColor = skinColor; - ch.HairColor = hairColor; - ch.FacialHairColor = facialHairColor; + ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + ch.Head.SkinColor = skinColor; + ch.Head.HairColor = hairColor; + ch.Head.FacialHairColor = facialHairColor; ch.SetPersonalityTrait(); if (ch.Job != null) { - foreach (KeyValuePair skill in skillLevels) + foreach (KeyValuePair skill in skillLevels) { Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key); if (matchingSkill == null) @@ -538,17 +546,8 @@ namespace Barotrauma ch.Job.Skills.RemoveAll(s => !skillLevels.ContainsKey(s.Identifier)); } - byte savedStatValueCount = inc.ReadByte(); - for (int i = 0; i < savedStatValueCount; i++) - { - int statType = inc.ReadByte(); - string statIdentifier = inc.ReadString(); - float statValue = inc.ReadSingle(); - bool removeOnDeath = inc.ReadBoolean(); - ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath); - } ch.ExperiencePoints = inc.ReadUInt16(); - ch.AdditionalTalentPoints = inc.ReadUInt16(); + ch.AdditionalTalentPoints = inc.ReadRangedInteger(0, MaxAdditionalTalentPoints); return ch; } @@ -605,12 +604,12 @@ namespace Barotrauma { RelativeOffset = new Vector2(-0.01f, 0.0f) }); } - RectTransform createItemRectTransform(string labelTag, float width = 0.6f) + RectTransform createItemRectTransform(Identifier labelTag, float width = 0.6f) { var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.166f), content.RectTransform)); var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), - TextManager.Get(labelTag), font: GUI.SubHeadingFont); + TextManager.Get(labelTag), font: GUIStyle.SubHeadingFont); var bottomItem = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), style: null); @@ -618,43 +617,40 @@ namespace Barotrauma return new RectTransform(new Vector2(width, 1.0f), bottomItem.RectTransform, Anchor.Center); } - RectTransform genderItemRT = createItemRectTransform("Gender", 1.0f); + RectTransform menuCategoryRT = createItemRectTransform(info.Prefab.MenuCategoryVar, 1.0f); - GUILayoutGroup genderContainer = - new GUILayoutGroup(genderItemRT, isHorizontal: true) + GUILayoutGroup menuCategoryContainer = + new GUILayoutGroup(menuCategoryRT, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; - void createGenderButton(Gender gender) + void createMenuCategoryButton(Identifier tag) { - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), - TextManager.Get(gender.ToString()), style: "ListBoxElement") + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), menuCategoryContainer.RectTransform), + TextManager.Get(tag), style: "ListBoxElement") { - UserData = gender, + UserData = tag, OnClicked = OpenHeadSelection, - Selected = info.Gender == gender + Selected = info.Head.Preset.TagSet.Contains(tag) }; } - createGenderButton(Gender.Male); - createGenderButton(Gender.Female); - - int countAttachmentsOfType(WearableType wearableType) - => info.FilterByTypeAndHeadID( - info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), - wearableType, info.HeadSpriteId).Count(); + foreach (var tag in info.Prefab.VarTags[info.Prefab.MenuCategoryVar].OrderBy(t => t.Value).Reverse()) + { + createMenuCategoryButton(tag); + } List attachmentSliders = new List(); void createAttachmentSlider(int initialValue, WearableType wearableType) { - int attachmentCount = countAttachmentsOfType(wearableType); + int attachmentCount = info.CountValidAttachmentsOfType(wearableType); if (attachmentCount > 0) { var labelTag = wearableType == WearableType.FaceAttachment - ? "FaceAttachment.Accessories" - : $"FaceAttachment.{wearableType}"; + ? "FaceAttachment.Accessories".ToIdentifier() + : $"FaceAttachment.{wearableType}".ToIdentifier(); var sliderItemRT = createItemRectTransform(labelTag); var slider = new GUIScrollBar(sliderItemRT, style: "GUISlider") @@ -670,12 +666,12 @@ namespace Barotrauma } } - createAttachmentSlider(info.HairIndex, WearableType.Hair); - createAttachmentSlider(info.BeardIndex, WearableType.Beard); - createAttachmentSlider(info.MoustacheIndex, WearableType.Moustache); - createAttachmentSlider(info.FaceAttachmentIndex, WearableType.FaceAttachment); + createAttachmentSlider(info.Head.HairIndex, WearableType.Hair); + createAttachmentSlider(info.Head.BeardIndex, WearableType.Beard); + createAttachmentSlider(info.Head.MoustacheIndex, WearableType.Moustache); + createAttachmentSlider(info.Head.FaceAttachmentIndex, WearableType.FaceAttachment); - void createColorSelector(string labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, + void createColorSelector(Identifier labelTag, IEnumerable<(Color Color, float Commonness)> options, Func getter, Action setter) { var selectorItemRT = createItemRectTransform(labelTag, 0.4f); @@ -757,21 +753,21 @@ namespace Barotrauma }; } - if (countAttachmentsOfType(WearableType.Hair) > 0) + if (info.CountValidAttachmentsOfType(WearableType.Hair) > 0) { - createColorSelector($"Customization.{nameof(info.HairColor)}", info.HairColors, - () => info.HairColor, (color) => info.HairColor = color); + createColorSelector($"Customization.{nameof(info.Head.HairColor)}".ToIdentifier(), info.HairColors, + () => info.Head.HairColor, (color) => info.Head.HairColor = color); } - if (countAttachmentsOfType(WearableType.Moustache) > 0 || - countAttachmentsOfType(WearableType.Beard) > 0) + if (info.CountValidAttachmentsOfType(WearableType.Moustache) > 0 || + info.CountValidAttachmentsOfType(WearableType.Beard) > 0) { - createColorSelector($"Customization.{nameof(info.FacialHairColor)}", info.FacialHairColors, - () => info.FacialHairColor, (color) => info.FacialHairColor = color); + createColorSelector($"Customization.{nameof(info.Head.FacialHairColor)}".ToIdentifier(), info.FacialHairColors, + () => info.Head.FacialHairColor, (color) => info.Head.FacialHairColor = color); } - - createColorSelector($"Customization.{nameof(info.SkinColor)}", info.SkinColors, () => info.SkinColor, - (color) => info.SkinColor = color); + + createColorSelector($"Customization.{nameof(info.Head.SkinColor)}".ToIdentifier(), info.SkinColors, () => info.Head.SkinColor, + (color) => info.Head.SkinColor = color); RandomizeButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, parentComponent.RectTransform, @@ -780,10 +776,11 @@ namespace Barotrauma { OnClicked = (button, o) => { - info.Head = new HeadInfo(); - info.SetGenderAndRace(Rand.RandSync.Unsynced); - info.SetColors(); - + var headPreset = info.Prefab.Heads.GetRandom(Rand.RandSync.Unsynced); + info.Head = new HeadInfo(info, headPreset); + info.SetAttachments(Rand.RandSync.Unsynced); + info.SetColors(Rand.RandSync.Unsynced); + RecreateFrameContents(); info.RefreshHead(); OnHeadSwitch?.Invoke(this); @@ -801,7 +798,7 @@ namespace Barotrauma private bool OpenHeadSelection(GUIButton button, object userData) { - Gender selectedGender = (Gender)userData; + Identifier selectedCategory = (Identifier)userData; var info = CharacterInfo; @@ -842,36 +839,27 @@ namespace Barotrauma GUILayoutGroup row = null; int itemsInRow = 0; - XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => + ContentXElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); - XElement headSpriteElement = headElement.Element("sprite"); + ContentXElement headSpriteElement = headElement.GetChildElement("sprite"); string spritePathWithTags = headSpriteElement.Attribute("texture").Value; var characterConfigElement = info.CharacterConfigElement; - var heads = info.Heads; + var heads = info.Prefab.Heads; if (heads != null) { row = null; itemsInRow = 0; - foreach (var kvp in heads.Where(kv => kv.Key.Gender == selectedGender)) + foreach (var head in heads.Where(h => h.TagSet.Contains(selectedCategory))) { - var headPreset = kvp.Key; - Race race = headPreset.Race; - int headIndex = headPreset.ID; + string spritePath = info.Prefab.ReplaceVars(spritePathWithTags, head); - string spritePath = spritePathWithTags - .Replace("[GENDER]", selectedGender.ToString().ToLowerInvariant()) - .Replace("[RACE]", race.ToString().ToLowerInvariant()); - - if (!File.Exists(spritePath)) - { - continue; - } + if (!File.Exists(spritePath)) { continue; } Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); headSprite.SourceRect = - new Rectangle(CalculateOffset(headSprite, kvp.Value.ToPoint()), + new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.SheetIndex.ToPoint()), headSprite.SourceRect.Size); characterSprites.Add(headSprite); @@ -881,7 +869,7 @@ namespace Barotrauma new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) { - UserData = selectedGender, + UserData = head.MenuCategory, Visible = true }; itemsInRow = 0; @@ -892,9 +880,9 @@ namespace Barotrauma { OutlineColor = Color.White * 0.5f, PressedColor = Color.White * 0.5f, - UserData = new Tuple(selectedGender, race, headIndex), + UserData = head, OnClicked = SwitchHead, - Selected = selectedGender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, + Selected = info.Head.Preset == head, Visible = true }; @@ -909,12 +897,18 @@ namespace Barotrauma private bool SwitchHead(GUIButton button, object obj) { var info = CharacterInfo; - Gender gender = ((Tuple)obj).Item1; - Race race = ((Tuple)obj).Item2; - int id = ((Tuple)obj).Item3; - info.Gender = gender; - info.Race = race; - info.Head.HeadSpriteId = id; + var headPreset = obj as HeadPreset; + if (info.Head.Preset != headPreset) + { + info.Head = new HeadInfo(info, headPreset) + { + SkinColor = info.Head.SkinColor, + HairColor = info.Head.HairColor, + FacialHairColor = info.Head.FacialHairColor + }; + info.ReloadHeadAttachments(); + } + RecreateFrameContents(); OnHeadSwitch?.Invoke(this); return true; @@ -927,16 +921,16 @@ namespace Barotrauma switch (type) { case WearableType.Beard: - info.BeardIndex = index; + info.Head.BeardIndex = index; break; case WearableType.FaceAttachment: - info.FaceAttachmentIndex = index; + info.Head.FaceAttachmentIndex = index; break; case WearableType.Hair: - info.HairIndex = index; + info.Head.HairIndex = index; break; case WearableType.Moustache: - info.MoustacheIndex = index; + info.Head.MoustacheIndex = index; break; default: DebugConsole.ThrowError($"Wearable type not implemented: {type}"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 946301454..a3dba23b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -134,7 +134,7 @@ namespace Barotrauma msg.Write((ushort)characterTalents.Count); foreach (var unlockedTalent in characterTalents) { - msg.Write(unlockedTalent.Prefab.UIntIdentifier); + msg.Write(unlockedTalent.Prefab.UintIdentifier); } break; } @@ -307,7 +307,7 @@ namespace Barotrauma { string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); //read anyway to prevent messing up reading the rest of the message _ = msg.ReadUInt16(); @@ -357,7 +357,7 @@ namespace Barotrauma int skillCount = msg.ReadByte(); for (int i = 0; i < skillCount; i++) { - string skillIdentifier = msg.ReadString(); + Identifier skillIdentifier = msg.ReadIdentifier(); float skillLevel = msg.ReadSingle(); info?.SetSkillLevel(skillIdentifier, skillLevel); } @@ -419,9 +419,9 @@ namespace Barotrauma if (!validData) { break; } if (msgType == 1) { - int orderIndex = msg.ReadRangedInteger(0, Order.PrefabList.Count); - var orderPrefab = Order.PrefabList[orderIndex]; - string option = null; + UInt32 orderPrefabUintIdentifier = msg.ReadUInt32(); + var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); + Identifier option = Identifier.Empty; if (orderPrefab.HasOptions) { int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); @@ -434,8 +434,8 @@ namespace Barotrauma } else if (msgType == 2) { - string identifier = msg.ReadString(); - string option = msg.ReadString(); + Identifier identifier = msg.ReadIdentifier(); + Identifier option = msg.ReadIdentifier(); ushort objectiveTargetEntityId = msg.ReadUInt16(); var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); @@ -486,8 +486,8 @@ namespace Barotrauma break; case 13: //NetEntityEvent.Type.UpdatePermanentStats: byte savedStatValueCount = msg.ReadByte(); - StatTypes statType = (StatTypes)msg.ReadByte(); - info?.ClearSavedStatValues(statType); + StatTypes statType = (StatTypes)msg.ReadByte(); + info?.ClearSavedStatValues(statType); for (int i = 0; i < savedStatValueCount; i++) { string statIdentifier = msg.ReadString(); @@ -544,7 +544,7 @@ namespace Barotrauma int ownerId = hasOwner ? inc.ReadByte() : -1; byte teamID = inc.ReadByte(); bool hasAi = inc.ReadBoolean(); - string infoSpeciesName = inc.ReadString(); + Identifier infoSpeciesName = inc.ReadIdentifier(); CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); try @@ -567,7 +567,7 @@ namespace Barotrauma int orderCount = inc.ReadByte(); for (int i = 0; i < orderCount; i++) { - int orderPrefabIndex = inc.ReadByte(); + UInt32 orderPrefabUintIdentifier = inc.ReadUInt32(); Entity targetEntity = FindEntityByID(inc.ReadUInt16()); Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; int orderOptionIndex = inc.ReadByte(); @@ -581,18 +581,23 @@ namespace Barotrauma targetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); } - if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) + OrderPrefab orderPrefab = + OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); + if (orderPrefab != null) { - var orderPrefab = Order.PrefabList[orderPrefabIndex]; var component = orderPrefab.GetTargetItemComponent(targetEntity as Item); if (!orderPrefab.MustSetTarget || (targetEntity != null && component != null) || targetPosition != null) { var order = targetPosition == null ? new Order(orderPrefab, targetEntity, component, orderGiver: orderGiver) : new Order(orderPrefab, targetPosition, orderGiver: orderGiver); - character.SetOrder(order, - orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, - orderPriority, orderGiver, speak: false, force: true); + order = order.WithOption( + orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length + ? orderPrefab.Options[orderOptionIndex] + : Identifier.Empty) + .WithManualPriority(orderPriority) + .WithOrderGiver(orderGiver); + character.SetOrder(order, speak: false, force: true); } else { @@ -601,7 +606,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabIndex + ") out of bounds."); + DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabUintIdentifier + ") out of bounds."); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index 3cea60b80..5f3574971 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -1,4 +1,5 @@ using Barotrauma.Sounds; +using System.Collections.Immutable; namespace Barotrauma { @@ -13,20 +14,17 @@ namespace Barotrauma public readonly CharacterParams.SoundParams Params; public SoundType Type => Params.State; - public Gender Gender => Params.Gender; + public ImmutableHashSet TagSet => Params.TagSet; public float Volume => roundSound == null ? 0.0f : roundSound.Volume; public float Range => roundSound == null ? 0.0f : roundSound.Range; public Sound Sound => roundSound?.Sound; - public bool IgnoreMuffling - { - get { return roundSound?.IgnoreMuffling ?? false; } - } + public bool IgnoreMuffling => roundSound?.IgnoreMuffling ?? false; public CharacterSound(CharacterParams.SoundParams soundParams) { Params = soundParams; - roundSound = Submarine.LoadRoundSound(soundParams.Element); + roundSound = RoundSound.Load(soundParams.Element); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs index 5052e74fe..7738ca9ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs @@ -39,7 +39,7 @@ namespace Barotrauma public Vector2 Size; private readonly Submarine parentSub; - public string Text + public LocalizedString Text { get; private set; @@ -58,7 +58,7 @@ namespace Barotrauma } public HUDProgressBar(Vector2 worldPosition, string textTag, Submarine parentSubmarine = null) - : this(worldPosition, parentSubmarine, GUI.Style.Red, GUI.Style.Green, textTag) + : this(worldPosition, parentSubmarine, GUIStyle.Red, GUIStyle.Green, textTag) { } @@ -73,7 +73,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(textTag)) { this.textTag = textTag; - Text = TextManager.Get(textTag); + Text = TextManager.Get(textTag).Fallback(textTag); } } @@ -101,12 +101,12 @@ namespace Barotrauma color * a, Color.White * a * 0.8f); - if (!string.IsNullOrEmpty(Text)) + if (!Text.IsNullOrEmpty()) { - Vector2 textSize = GUI.SmallFont.MeasureString(Text); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(Text); Vector2 textPos = new Vector2(pos.X + (Size.X - textSize.X) / 2, pos.Y - textSize.Y * 1.2f); - GUI.DrawString(spriteBatch, textPos - Vector2.One, Text, Color.Black * a, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, textPos, Text, Color.White * a, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, textPos - Vector2.One, Text, Color.Black * a, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, textPos, Text, Color.White * a, font: GUIStyle.SmallFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index e5bd89ebb..f89b83bdf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -16,15 +16,15 @@ { return; } - GUI.AddMessage(TextManager.Get("HuskDormant"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); break; case InfectionState.Transition: - GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); break; case InfectionState.Active: if (character.Params.UseHuskAppendage) { - GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameMain.Config.KeyBindText(InputType.Attack)), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red); } break; case InfectionState.Final: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index 20249f49b..77672ac7e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -75,7 +75,7 @@ namespace Barotrauma case FloodType.Minor: currentFloodState += deltaTime; //lerp the water surface in all hulls 15 units above the floor within 10 seconds - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = hull.FakeFireSources.Count - 1; i >= 0; i--) { @@ -87,7 +87,7 @@ namespace Barotrauma case FloodType.Major: currentFloodState += deltaTime; //create a full flood in 10 seconds - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = hull.FakeFireSources.Count - 1; i >= 0; i--) { @@ -98,7 +98,7 @@ namespace Barotrauma break; case FloodType.HideFlooding: //hide water inside hulls (the player can't see which hulls are flooded) - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.DrawSurface = hull.Rect.Y - hull.Rect.Height; } @@ -140,7 +140,7 @@ namespace Barotrauma character.Submarine != null && createFireSourceTimer > MathHelper.Lerp(MaxFakeFireSourceInterval, MinFakeFireSourceInterval, Strength / 100.0f)) { - Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); + Hull fireHull = Hull.HullList.GetRandomUnsynced(h => h.Submarine == character.Submarine); if (fireHull != null) { var fakeFire = new DummyFireSource(Vector2.One * 500.0f, new Vector2(Rand.Range(fireHull.WorldRect.X, fireHull.WorldRect.Right), fireHull.WorldPosition.Y + 1), fireHull, isNetworkMessage: true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d3773294f..c84fe8eab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -14,11 +14,31 @@ namespace Barotrauma { private static bool toggledThisFrame; - public static Sprite DamageOverlay; + public class DamageOverlayPrefab : Prefab + { + public readonly static PrefabSelector Prefabs = new PrefabSelector(); - public static string DamageOverlayFile; + public readonly Sprite DamageOverlay; - private static string[] strengthTexts; + public DamageOverlayPrefab(ContentXElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier()) + { + DamageOverlay = new Sprite(element); + } + + public override void Dispose() + { + DamageOverlay.Remove(); + } + } + + public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay; + + private readonly static LocalizedString[] strengthTexts = new LocalizedString[] + { + TextManager.Get("AfflictionStrengthLow"), + TextManager.Get("AfflictionStrengthMedium"), + TextManager.Get("AfflictionStrengthHigh") + }; private Point screenResolution; @@ -134,7 +154,7 @@ namespace Barotrauma Character.Controlled.ResetInteract = true; if (openHealthWindow != null) { - if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner")) + if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier())) { openHealthWindow.characterName.Text = value.Character.Name; } @@ -173,20 +193,10 @@ namespace Barotrauma private GUIFrame healthBarHolder; - partial void InitProjSpecific(XElement element, Character character) + partial void InitProjSpecific(ContentXElement element, Character character) { DisplayedVitality = MaxVitality; - if (strengthTexts == null) - { - strengthTexts = new string[] - { - TextManager.Get("AfflictionStrengthLow"), - TextManager.Get("AfflictionStrengthMedium"), - TextManager.Get("AfflictionStrengthHigh") - }; - } - character.OnAttacked += OnAttacked; healthWindow = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.6f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox"); @@ -200,13 +210,13 @@ namespace Barotrauma { Stretch = true }; - + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => { character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, character != Character.Controlled); }); - characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) + characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -220,12 +230,12 @@ namespace Barotrauma var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + barSize: 1.0f, color: GUIStyle.Green, style: "GUIHealthBar") { IsHorizontal = true }; healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + barSize: 1.0f, color: GUIStyle.Green, style: "GUIHealthBar") { IsHorizontal = true }; @@ -311,7 +321,7 @@ namespace Barotrauma } ); deadIndicator = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.1f), limbSelection.RectTransform, Anchor.Center), - text: TextManager.Get("Deceased"), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "GUIToolTip") + text: TextManager.Get("Deceased"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: "GUIToolTip") { Visible = false, CanBeFocused = false @@ -328,7 +338,7 @@ namespace Barotrauma afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), - TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); + TextManager.Get("SuitableTreatments"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), healthWindowVerticalLayout.RectTransform), true) { @@ -366,10 +376,10 @@ namespace Barotrauma healthShadowSize = 1.0f; healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: "CharacterHealthBar") + barSize: 1.0f, color: GUIStyle.HealthBarColorHigh, style: "CharacterHealthBar") { HoverCursor = CursorState.Hand, - ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), + ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)), Enabled = true }; @@ -406,7 +416,7 @@ namespace Barotrauma if (element != null) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -501,7 +511,17 @@ namespace Barotrauma { if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && Character.Controlled == Character) { - SoundPlayer.PlaySound(Character.Info != null && Character.Info.Gender == Gender.Female ? "drownfemale" : "drownmale"); + string soundName; + if (Character.Info != null) + { + soundName = Character.Info.ReplaceVars($"drown[{Character.Info.Prefab.MenuCategoryVar}]"); + } + else + { + var charInfoPrefab = CharacterPrefab.HumanPrefab.CharacterInfoPrefab; + soundName = charInfoPrefab.ReplaceVars($"drown[{charInfoPrefab.MenuCategoryVar}]", charInfoPrefab.Heads.First()); + } + SoundPlayer.PlaySound(soundName); } if (Character == Character.Controlled && !IsUnconscious && !Character.IsDead && OxygenAmount < LowOxygenThreshold) @@ -715,11 +735,11 @@ namespace Barotrauma if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { - afflictionIcon.Flash(GUI.Style.Red); + afflictionIcon.Flash(GUIStyle.Red); } else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { - afflictionIcon.Flash(GUI.Style.Green); + afflictionIcon.Flash(GUIStyle.Green); } } @@ -754,7 +774,7 @@ namespace Barotrauma var labelContainer = afflictionTooltip.Content.GetChildByUserData("label"); - labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, (int)(GUI.LargeFont.Size * 1.5f))); + labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f))); } } else @@ -799,7 +819,7 @@ namespace Barotrauma { var treatmentButton = component.GetChild(); if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } - var matchingItem = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + var matchingItem = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); treatmentButton.Enabled = matchingItem != null; if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) { @@ -809,16 +829,18 @@ namespace Barotrauma if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length && Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f) { - Character.Controlled.Inventory.visualSlots[index].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + Character.Controlled.Inventory.visualSlots[index].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f); } } - if (matchingItem != null && !string.IsNullOrEmpty(treatmentButton.ToolTip)) { continue; } - treatmentButton.ToolTip = $"‖color:255,255,255,255‖{itemPrefab.Name}‖color:end‖" + '\n' + itemPrefab.Description; + if (matchingItem != null && !treatmentButton.ToolTip.IsNullOrEmpty()) { continue; } + treatmentButton.ToolTip = RichString.Rich($"‖color:255,255,255,255‖{itemPrefab.Name}‖color:end‖" + '\n' + itemPrefab.Description); if (treatmentButton.Enabled) { treatmentButton.ToolTip = - $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] {TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' - + treatmentButton.RawToolTip; + RichString.Rich( + $"‖color:gui.green‖[{TextManager.Get(PlayerInput.MouseButtonsSwapped() ? "input.rightmouse" : "input.leftmouse")}] " + + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + + treatmentButton.ToolTip.NestedStr); } foreach (GUIComponent child in treatmentButton.Children) { @@ -834,7 +856,7 @@ namespace Barotrauma } else { - healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUI.Style.HealthBarColorLow, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorHigh); + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUIStyle.HealthBarColorLow, GUIStyle.HealthBarColorMedium, GUIStyle.HealthBarColorHigh); healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; healthBar.BarSize = healthWindowHealthBar.BarSize = (DisplayedVitality > 0.0f) ? @@ -1007,14 +1029,14 @@ namespace Barotrauma DrawStatusHUD(spriteBatch); } - private (Affliction affliction, string text)? highlightedAfflictionIcon; + private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; public void DrawStatusHUD(SpriteBatch spriteBatch) { highlightedAfflictionIcon = null; //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - List<(Affliction affliction, string text)> statusIcons = new List<(Affliction affliction, string text)>(); + var statusIcons = new List<(Affliction Affliction, LocalizedString Warning)>(); if (Character.InPressure) { statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); @@ -1045,7 +1067,7 @@ namespace Barotrauma foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.affliction; + Affliction affliction = statusIcon.Affliction; AfflictionPrefab afflictionPrefab = affliction.Prefab; Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); @@ -1059,10 +1081,10 @@ namespace Barotrauma { Rectangle glowRect = afflictionIconRect; glowRect.Inflate((int)(20 * GUI.Scale), (int)(20 * GUI.Scale)); - var glow = GUI.Style.GetComponentStyle("OuterGlowCircular"); + var glow = GUIStyle.GetComponentStyle("OuterGlowCircular"); glow.Sprites[GUIComponent.ComponentState.None][0].Draw( spriteBatch, glowRect, - GUI.Style.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); + GUIStyle.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); } float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; @@ -1082,8 +1104,8 @@ namespace Barotrauma if (highlightedAfflictionIcon != null) { - string nameTooltip = highlightedAfflictionIcon.Value.text; - Vector2 offset = GUI.Font.MeasureString(nameTooltip); + LocalizedString nameTooltip = highlightedAfflictionIcon.Value.NameToolTip; + Vector2 offset = GUIStyle.Font.MeasureString(nameTooltip); GUI.DrawString(spriteBatch, alignment == Alignment.Left ? highlightedIconPos + offset : highlightedIconPos - offset, @@ -1096,7 +1118,7 @@ namespace Barotrauma float currHealth = healthBar.BarSize; Color prevColor = healthBar.Color; healthBarShadow.BarSize = healthShadowSize; - healthBarShadow.Color = Color.Lerp(GUI.Style.Red, Color.Black, 0.5f); + healthBarShadow.Color = Color.Lerp(GUIStyle.Red, Color.Black, 0.5f); healthBarShadow.Visible = true; healthBar.BarSize = currHealth; healthBar.Color = prevColor; @@ -1113,7 +1135,7 @@ namespace Barotrauma float currHealth = healthWindowHealthBar.BarSize; Color prevColor = healthWindowHealthBar.Color; healthWindowHealthBarShadow.BarSize = healthShadowSize; - healthWindowHealthBarShadow.Color = GUI.Style.Red; + healthWindowHealthBarShadow.Color = GUIStyle.Red; healthWindowHealthBarShadow.Visible = true; healthWindowHealthBar.BarSize = currHealth; healthWindowHealthBar.Color = prevColor; @@ -1137,10 +1159,10 @@ namespace Barotrauma { if (prefab.IsBuff) { - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); } - return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); } return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); @@ -1213,7 +1235,7 @@ namespace Barotrauma CanBeFocused = false }; - var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUI.Style.Green, style: "GUIAfflictionBar") + var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUIStyle.Green, style: "GUIAfflictionBar") { UserData = "afflictionstrengthprediction", CanBeFocused = false @@ -1240,9 +1262,9 @@ namespace Barotrauma afflictionIcon.PressedColor = afflictionIcon.Color; afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f); afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f); - + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content.RectTransform), - affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.BottomCenter) + affliction.Prefab.Name, font: GUIStyle.SmallFont, textAlignment: Alignment.BottomCenter) { CanBeFocused = false }; @@ -1274,13 +1296,13 @@ namespace Barotrauma //key = item identifier //float = suitability - Dictionary treatmentSuitability = new Dictionary(); + Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, normalize: true, ignoreHiddenAfflictions: true, limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); - foreach (string treatment in treatmentSuitability.Keys.ToList()) + foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) { //prefer suggestions for items the player has if (Character.Controlled.Inventory.FindItemByIdentifier(treatment, recursive: true) != null) @@ -1304,10 +1326,10 @@ namespace Barotrauma recommendedTreatmentContainer.AutoHideScrollBar = true; } - List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); + List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); int count = 0; - foreach (KeyValuePair treatment in treatmentSuitabilities) + foreach (KeyValuePair treatment in treatmentSuitabilities) { count++; if (count > 5) { break; } @@ -1326,7 +1348,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (!(userdata is ItemPrefab itemPrefab)) { return false; } - var item = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + var item = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); @@ -1337,15 +1359,15 @@ namespace Barotrauma new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow") { CanBeFocused = false, - Color = GUI.Style.Green, + Color = GUIStyle.Green, HoverColor = Color.White, PressedColor = Color.DarkGray, SelectedColor = Color.Transparent, DisabledColor = Color.Transparent }; - Sprite itemSprite = item.InventoryIcon ?? item.sprite; - Color itemColor = itemSprite == item.sprite ? item.SpriteColor : item.InventoryIconColor; + Sprite itemSprite = item.InventoryIcon ?? item.Sprite; + Color itemColor = itemSprite == item.Sprite ? item.SpriteColor : item.InventoryIconColor; var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), innerFrame.RectTransform, Anchor.Center), itemSprite, scaleToFit: true) { @@ -1397,12 +1419,12 @@ namespace Barotrauma CanBeFocused = false }; - var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) + var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false, AutoScaleHorizontal = true }; - var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUI.SubHeadingFont) + var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { UserData = "strength", CanBeFocused = false @@ -1423,21 +1445,21 @@ namespace Barotrauma if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height) { - description.Font = GUI.SmallFont; + description.Font = GUIStyle.SmallFont; } - Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUI.LargeFont.Size * 1.5f)); + Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f)); afflictionStrength.Text = strengthTexts[ MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; - Vector2 strengthDims = GUI.SubHeadingFont.MeasureString(afflictionStrength.Text); + Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text); labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, nameDims.Y)); afflictionName.RectTransform.Resize(new Point((int)(labelContainer.Rect.Width - strengthDims.X * 0.99f), nameDims.Y)); afflictionStrength.RectTransform.Resize(new Point(labelContainer.Rect.Width - afflictionName.Rect.Width, nameDims.Y)); - afflictionStrength.TextColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, + afflictionStrength.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); @@ -1451,8 +1473,8 @@ namespace Barotrauma { vitality.Visible = true; vitality.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; - vitality.TextColor = vitalityDecrease <= 0 ? GUI.Style.Green : - Color.Lerp(GUI.Style.Orange, GUI.Style.Red, affliction.Strength / affliction.Prefab.MaxStrength); + vitality.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : + Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); } vitality.AutoDraw = true; @@ -1478,7 +1500,7 @@ namespace Barotrauma var potentialTreatment = Inventory.DraggingItems.FirstOrDefault(); if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab) { - potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.prefab == itemPrefab, recursive: true); + potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); } potentialTreatment ??= Inventory.SelectedSlot?.Item; @@ -1488,11 +1510,11 @@ namespace Barotrauma Color afflictionEffectColor = Color.White; if (afflictionVitalityDecrease > 0.0f) { - afflictionEffectColor = GUI.Style.Red; + afflictionEffectColor = GUIStyle.Red; } else if (afflictionVitalityDecrease < 0.0f) { - afflictionEffectColor = GUI.Style.Green; + afflictionEffectColor = GUIStyle.Green; } var child = afflictionIconContainer.Content.FindChild(affliction); @@ -1510,7 +1532,7 @@ namespace Barotrauma if (afflictionStrengthPrediction < affliction.Strength) { afflictionStrengthBar.Color = afflictionEffectColor; - afflictionStrengthPredictionBar.Color = GUI.Style.Blue * t; + afflictionStrengthPredictionBar.Color = GUIStyle.Blue * t; afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize; afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; } @@ -1541,8 +1563,8 @@ namespace Barotrauma { foreach (var reduceAffliction in effect.ReduceAffliction) { - if (reduceAffliction.affliction != affliction.Identifier && reduceAffliction.affliction != affliction.Prefab.AfflictionType) { continue; } - strength -= reduceAffliction.amount * (effect.Duration > 0 ? effect.Duration : 1.0f); + if (reduceAffliction.AfflictionIdentifier != affliction.Identifier && reduceAffliction.AfflictionIdentifier != affliction.Prefab.AfflictionType) { continue; } + strength -= reduceAffliction.ReduceAmount * (effect.Duration > 0 ? effect.Duration : 1.0f); } foreach (var addAffliction in effect.Afflictions) { @@ -1563,7 +1585,7 @@ namespace Barotrauma strengthText.Text = strengthTexts[ MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; - strengthText.TextColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, + strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock; @@ -1576,8 +1598,8 @@ namespace Barotrauma { vitalityText.Visible = true; vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; - vitalityText.TextColor = vitalityDecrease <= 0 ? GUI.Style.Green : - Color.Lerp(GUI.Style.Orange, GUI.Style.Red, affliction.Strength / affliction.Prefab.MaxStrength); + vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : + Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); } } @@ -1807,9 +1829,9 @@ namespace Barotrauma if (afflictionsDisplayedOnLimb.Count() > 1) { string additionalAfflictionCount = $"+{afflictionsDisplayedOnLimb.Count() - 1}"; - Vector2 displace = GUI.SubHeadingFont.MeasureString(additionalAfflictionCount); - GUI.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f); - GUI.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White); + Vector2 displace = GUIStyle.SubHeadingFont.MeasureString(additionalAfflictionCount); + GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f); + GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White); } i++; @@ -1885,7 +1907,7 @@ namespace Barotrauma for (int i = 0; i < afflictionCount; i++) { uint afflictionID = inc.ReadUInt32(); - AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UIntIdentifier == afflictionID); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionID); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); @@ -1913,7 +1935,7 @@ namespace Barotrauma { int limbIndex = inc.ReadRangedInteger(0, limbHealths.Count - 1); uint afflictionID = inc.ReadUInt32(); - AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UIntIdentifier == afflictionID); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionID); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs index 30833a339..f6030fad6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/DamageModifier.cs @@ -2,14 +2,14 @@ { partial class DamageModifier { - [Serialize("", false), Editable] + [Serialize("", IsPropertySaveable.No), Editable] public string DamageSound { get; private set; } - [Serialize("", false), Editable] + [Serialize("", IsPropertySaveable.No), Editable] public string DamageParticle { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index 6e041eeac..19640dbc7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Barotrauma { - partial class JobPrefab : IPrefab, IDisposable + partial class JobPrefab : PrefabWithUintIdentifier { public GUIButton CreateInfoFrame(out GUIComponent buttonContainer) { @@ -18,20 +18,20 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), frameHolder.RectTransform, Anchor.Center)); GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUIStyle.LargeFont); var descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.15f) }, - Description, font: GUI.SmallFont, wrap: true); + Description, font: GUIStyle.SmallFont, wrap: true); var skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.5f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.2f + descriptionBlock.RectTransform.RelativeSize.Y) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform), - TextManager.Get("Skills"), font: GUI.LargeFont); + TextManager.Get("Skills"), font: GUIStyle.LargeFont); foreach (SkillPrefab skill in Skills) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), (int)skill.LevelRange.Start + " - " + (int)skill.LevelRange.End), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); } buttonContainer = paddedFrame; @@ -43,14 +43,14 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), - TextManager.Get("Items", fallBackTag: "mapentitycategory.equipment"), font: GUI.LargeFont); + TextManager.Get("Items", "mapentitycategory.equipment"), font: GUIStyle.LargeFont); foreach (string identifier in itemIdentifiers.Distinct()) { if (!(MapEntityPrefab.Find(name: null, identifier: identifier) is ItemPrefab itemPrefab)) { continue; } int count = itemIdentifiers.Count(i => i == identifier); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), itemContainer.RectTransform), " - " + (count == 1 ? itemPrefab.Name : itemPrefab.Name + " x" + count), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); }*/ return frameHolder; @@ -76,12 +76,12 @@ namespace Barotrauma } } - public List GetJobOutfitSprites(Gender gender, bool useInventoryIcon, out Vector2 maxDimensions) + public List GetJobOutfitSprites(CharacterInfoPrefab charInfoPrefab, bool useInventoryIcon, out Vector2 maxDimensions) { List outfitPreviews = new List(); maxDimensions = Vector2.One; - var equipIdentifiers = Element.GetChildElements("ItemSet").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeString("identifier", "")); + var equipIdentifiers = Element.GetChildElements("ItemSet").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeIdentifier("identifier", "")); List outfitPrefabs = new List(); foreach (var equipIdentifier in equipIdentifiers) @@ -114,8 +114,8 @@ namespace Barotrauma var children = previewElement.Elements().ToList(); for (int n = 0; n < children.Count; n++) { - XElement spriteElement = children[n]; - string spriteTexture = spriteElement.GetAttributeString("texture", "").Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male"); + var spriteElement = children[n]; + string spriteTexture = charInfoPrefab.ReplaceVars(spriteElement.GetAttributeString("texture", ""), charInfoPrefab.Heads.First()); var sprite = new Sprite(spriteElement, file: spriteTexture); sprite.size = new Vector2(sprite.SourceRect.Width, sprite.SourceRect.Height); outfitPreview.AddSprite(sprite, children[n].GetAttributeVector2("offset", Vector2.Zero)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 15bd29bf4..4d924f605 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -78,7 +78,7 @@ namespace Barotrauma //{ // var pos = ConvertUnits.ToDisplayUnits(mouthPos.Value); // pos.Y = -pos.Y; - // ShapeExtensions.DrawPoint(spriteBatch, pos, GUI.Style.Red, size: 5); + // ShapeExtensions.DrawPoint(spriteBatch, pos, GUIStyle.Red, size: 5); //} // A debug visualisation on the bezier curve between limbs. @@ -95,7 +95,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, start, end, Color.White); GUI.DrawLine(spriteBatch, start, control, Color.Black); GUI.DrawLine(spriteBatch, control, end, Color.Black); - GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUI.Style.Red);*/ + GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUIStyle.Red);*/ } } @@ -274,7 +274,7 @@ namespace Barotrauma } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { for (int i = 0; i < Params.decorativeSpriteParams.Count; i++) { @@ -290,7 +290,7 @@ namespace Barotrauma spriteAnimState.Add(decorativeSprite, new SpriteState()); } TintMask = null; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -317,7 +317,7 @@ namespace Barotrauma NonConditionalDeformations.AddRange(deformations); break; case "randomcolor": - randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandom(); + randomColor = subElement.GetAttributeColorArray("colors", null)?.GetRandomUnsynced(); if (randomColor.HasValue) { Params.GetSprite().Color = randomColor.Value; @@ -337,8 +337,8 @@ namespace Barotrauma InitialLightSpriteAlpha = LightSource.OverrideLightSpriteAlpha; break; case "tintmask": - string tintMaskPath = subElement.GetAttributeString("texture", ""); - if (!string.IsNullOrWhiteSpace(tintMaskPath)) + ContentPath tintMaskPath = subElement.GetAttributeContentPath("texture"); + if (!tintMaskPath.IsNullOrWhiteSpace()) { TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath)); TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f); @@ -346,8 +346,8 @@ namespace Barotrauma } break; case "huskmask": - string huskMaskPath = subElement.GetAttributeString("texture", ""); - if (!string.IsNullOrWhiteSpace(huskMaskPath)) + ContentPath huskMaskPath = subElement.GetAttributeContentPath("texture"); + if (!huskMaskPath.IsNullOrWhiteSpace()) { HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath)); } @@ -387,7 +387,7 @@ namespace Barotrauma } if (deformation == null) { - deformation = SpriteDeformation.Load(animationElement, character.SpeciesName); + deformation = SpriteDeformation.Load(animationElement, character.SpeciesName.Value); if (deformation != null) { ragdoll.SpriteDeformations.Add(deformation); @@ -472,19 +472,23 @@ namespace Barotrauma } private string _texturePath; - private string GetSpritePath(XElement element, SpriteParams spriteParams) + private string GetSpritePath(ContentXElement element, SpriteParams spriteParams) { if (_texturePath == null) { if (spriteParams != null) { - string texturePath = character.Params.VariantFile?.Root?.GetAttributeString("texture", null) ?? spriteParams.GetTexturePath(); + ContentPath texturePath = + character.Params.VariantFile?.Root?.GetAttributeContentPath("texture", character.Prefab.ContentPackage) + ?? ContentPath.FromRaw(character.Prefab.ContentPackage, spriteParams.GetTexturePath()); _texturePath = GetSpritePath(texturePath); } else { - string texturePath = element.GetAttributeString("texture", null); - texturePath = string.IsNullOrWhiteSpace(texturePath) ? ragdoll.RagdollParams.Texture : texturePath; + ContentPath texturePath = element.GetAttributeContentPath("texture"); + texturePath = texturePath.IsNullOrWhiteSpace() + ? ContentPath.FromRaw(character.Prefab.ContentPackage, ragdoll.RagdollParams.Texture) + : texturePath; _texturePath = GetSpritePath(texturePath); } } @@ -494,20 +498,18 @@ namespace Barotrauma /// /// Get the full path of a limb sprite, taking into account tags, gender and head id /// - public static string GetSpritePath(string texturePath, CharacterInfo characterInfo) + public static string GetSpritePath(ContentPath texturePath, CharacterInfo characterInfo) { - string spritePath = texturePath; + string spritePath = texturePath.Value; string spritePathWithTags = spritePath; if (characterInfo != null) { - spritePath = spritePath.Replace("[GENDER]", (characterInfo.Gender == Gender.Female) ? "female" : "male"); - spritePath = spritePath.Replace("[RACE]", characterInfo.Race.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", characterInfo.HeadSpriteId.ToString()); + spritePath = characterInfo.ReplaceVars(spritePath); if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any()) { string tags = ""; - characterInfo.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); + characterInfo.SpriteTags.ForEach(tag => tags += $"[{tag}]"); spritePathWithTags = Path.Combine( Path.GetDirectoryName(spritePath), @@ -518,9 +520,9 @@ namespace Barotrauma } - private string GetSpritePath(string texturePath) + private string GetSpritePath(ContentPath texturePath) { - if (!character.IsHumanoid) { return texturePath; } + if (!character.IsHumanoid) { return texturePath.Value; } return GetSpritePath(texturePath, character?.Info); } @@ -696,7 +698,7 @@ namespace Barotrauma clr = clr.Multiply(ragdoll.RagdollParams.Color); if (character.Info != null) { - clr = clr.Multiply(character.Info.SkinColor); + clr = clr.Multiply(character.Info.Head.SkinColor); } if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head) { @@ -929,7 +931,7 @@ namespace Barotrauma if (pullJoint != null) { Vector2 pos = ConvertUnits.ToDisplayUnits(pullJoint.WorldAnchorB); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 5, 5), GUIStyle.Red, true); } var bodyDrawPos = body.DrawPosition; bodyDrawPos.Y = -bodyDrawPos.Y; @@ -943,11 +945,11 @@ namespace Barotrauma var front = ConvertUnits.ToDisplayUnits(body.FarseerBody.GetWorldPoint(localFront)); front.Y = -front.Y; GUI.DrawLine(spriteBatch, bodyDrawPos, front, Color.Yellow, width: 2); - GUI.DrawLine(spriteBatch, from, to, GUI.Style.Red, width: 1); + GUI.DrawLine(spriteBatch, from, to, GUIStyle.Red, width: 1); GUI.DrawRectangle(spriteBatch, new Rectangle((int)from.X, (int)from.Y, 12, 12), Color.White, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 12, 12), Color.White, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)from.X, (int)from.Y, 10, 10), Color.Blue, true); - GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 10, 10), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)to.X, (int)to.Y, 10, 10), GUIStyle.Red, true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)front.X, (int)front.Y, 10, 10), Color.Yellow, true); //Vector2 mainLimbFront = ConvertUnits.ToDisplayUnits(ragdoll.MainLimb.body.FarseerBody.GetWorldPoint(ragdoll.MainLimb.body.GetFrontLocal(MathHelper.ToRadians(limbParams.Orientation)))); @@ -1046,8 +1048,8 @@ namespace Barotrauma //{ // width = (int)Math.Round(width / cam.Zoom); //} - //GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(up) * size, GUI.Style.Red, width: width); - Color color = modifier.DamageMultiplier > 1 ? GUI.Style.Red : GUI.Style.Green; + //GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(up) * size, GUIStyle.Red, width: width); + Color color = modifier.DamageMultiplier > 1 ? GUIStyle.Red : GUIStyle.Green; float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); if (isScreenSpace) { @@ -1078,9 +1080,9 @@ namespace Barotrauma { wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, wearable.SheetIndex.Value), sprite.SourceRect.Size); } - else if (type == LimbType.Head && character.Info != null && character.Info.Head.SheetIndex.HasValue) + else if (type == LimbType.Head && character.Info != null) { - wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, character.Info.Head.SheetIndex.Value.ToPoint()), sprite.SourceRect.Size); + wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, character.Info.Head.SheetIndex.ToPoint()), sprite.SourceRect.Size); } else { @@ -1134,11 +1136,11 @@ namespace Barotrauma { if (wearable.Type == WearableType.Hair) { - wearableColor = character.Info.HairColor; + wearableColor = character.Info.Head.HairColor; } else if (wearable.Type == WearableType.Beard || wearable.Type == WearableType.Moustache) { - wearableColor = character.Info.FacialHairColor; + wearableColor = character.Info.Head.FacialHairColor; } } float scale = wearable.Scale; @@ -1164,22 +1166,22 @@ namespace Barotrauma wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); } - private WearableSprite GetWearableSprite(WearableType type, bool random = false) + private WearableSprite GetWearableSprite(WearableType type)//, bool random = false) { var info = character.Info; if (info == null) { return null; } - XElement element; - if (random) + ContentXElement element; + /*if (random) { - element = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Gender, info.Race), type, info.Head.HeadSpriteId)?.GetRandom(Rand.RandSync.ClientOnly); + element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet)?.GetRandom(Rand.RandSync.ClientOnly); } else - { - element = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Gender, info.Race), type, info.Head.HeadSpriteId)?.FirstOrDefault(); - } + {*/ + element = info.FilterElements(info.Wearables, info.Head.Preset.TagSet, type)?.FirstOrDefault(); + //} if (element != null) { - return new WearableSprite(element.Element("sprite"), type); + return new WearableSprite(element.GetChildElement("sprite"), type); } return null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs new file mode 100644 index 000000000..166979300 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackage/ModProject.cs @@ -0,0 +1,167 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; + +namespace Barotrauma +{ + public class ModProject + { + public class File + { + private File(string path, Type type) + { + Path = path.CleanUpPathCrossPlatform(correctFilenameCase: false); + Type = type switch + { + _ when !type.IsSubclassOf(typeof(ContentFile)) => throw new ArgumentException($"{type.Name} does not derive from {nameof(ContentFile)}"), + { IsAbstract: true } => throw new ArgumentException($"{type.Name} is abstract"), + _ => type + }; + } + + private File(ContentFile f) + { + Path = f.Path.RawValue ?? ""; + Type = f.GetType(); + } + + public static File FromContentFile(ContentFile file) + => new File(file); + + public static File FromPath(string path) where T : ContentFile + => new File(path, typeof(T)); + + /// + /// Prefer FromPath<T> when possible, this just exists + /// for cases where the type can only be decided at runtime + /// + public static File FromPath(string path, Type type) + => new File(path, type); + + public readonly string Path; + public readonly Type Type; + + public XElement ToXElement() + { + if (Type is null) { throw new InvalidOperationException("Type must be set before calling ToXElement"); } + if (Path.IsNullOrEmpty()) { throw new InvalidOperationException("Path must be set before calling ToXElement"); } + return new XElement(Type.Name.RemoveFromEnd("File"), new XAttribute("file", Path)); + } + } + + public ModProject() { } + + public ModProject(ContentPackage? contentPackage) + { + if (contentPackage is null) { return; } + Name = contentPackage.Name; + AltNames = contentPackage.AltNames.ToList(); + files = contentPackage.Files.Select(File.FromContentFile).ToList(); + ModVersion = IncrementModVersion(contentPackage.ModVersion); + IsCore = contentPackage is CorePackage; + SteamWorkshopId = contentPackage.SteamWorkshopId; + ExpectedHash = contentPackage.Hash; + InstallTime = contentPackage.InstallTime; + } + + private string name = ""; + public string Name + { + get => name; + set + { + var charsToRemove = Path.GetInvalidFileNameChars(); + name = string.Concat(value.Where(c => !charsToRemove.Contains(c))); + } + } + + public readonly List AltNames = new List(); + + private readonly List files = new List(); + public IReadOnlyList Files => files; + + public string ModVersion = ContentPackage.DefaultModVersion; + + public Md5Hash? ExpectedHash { get; private set; } + + public bool IsCore = false; + + public UInt64 SteamWorkshopId = 0; + + public DateTime? InstallTime = null; + + public bool HasFile(File file) + => Files.Any(f => + string.Equals(f.Path, file.Path, StringComparison.OrdinalIgnoreCase) + && f.Type == file.Type); + + public void AddFile(File file) + { + if (!HasFile(file)) + { + files.Add(file); + DiscardHashAndInstallTime(); + } + } + + public void DiscardHashAndInstallTime() + { + ExpectedHash = null; + InstallTime = null; + } + + public static string IncrementModVersion(string modVersion) + { + //look for an integer at the end of the string and increment it + int startIndex = modVersion.Length - 1; + while (char.IsDigit(modVersion[startIndex])) { startIndex--; } + startIndex++; + + if (startIndex >= modVersion.Length + || !char.IsDigit(modVersion[startIndex]) + || !int.TryParse( + modVersion[startIndex..], + NumberStyles.Any, + CultureInfo.InvariantCulture, + out int theFinalInteger)) + { + return modVersion; + } + + return $"{modVersion[..startIndex]}{(theFinalInteger + 1).ToString(CultureInfo.InvariantCulture)}"; + } + + public XDocument ToXDocument() + { + XDocument doc = new XDocument(); + XElement rootElement = new XElement("contentpackage"); + + void addRootAttribute(string name, T value) where T : notnull + => rootElement.Add(new XAttribute(name, value.ToString() ?? "")); + + addRootAttribute("name", Name); + addRootAttribute("modversion", ModVersion); + addRootAttribute("corepackage", IsCore); + if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); } + addRootAttribute("gameversion", GameMain.Version); + if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } + if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } + if (InstallTime != null) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value)); } + + files.ForEach(f => rootElement.Add(f.ToXElement())); + + doc.Add(rootElement); + return doc; + } + + public void Save(string path) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + ToXDocument().SaveSafe(path); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs new file mode 100644 index 000000000..3ae094982 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -0,0 +1,53 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.IO; +using Barotrauma.Steam; + +namespace Barotrauma +{ + public static partial class ContentPackageManager + { + public sealed partial class PackageSource : ICollection + { + public ContentPackage SaveAndEnableRegularMod(ModProject modProject) + { + if (modProject.IsCore) { throw new ArgumentException("ModProject must not be a core package"); } + + //save the content package + string fileListPath = Path.Combine(directory, ToolBox.RemoveInvalidFileNameChars(modProject.Name), ContentPackage.FileListFileName) + .CleanUpPathCrossPlatform(correctFilenameCase: false); + Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)!); + modProject.Save(fileListPath); + Refresh(); EnabledPackages.DisableRemovedMods(); + var newPackage = Regular.First(p => p.Path == fileListPath); + + //enable it + EnabledPackages.EnableRegular(newPackage); + + return newPackage; + } + } + + private static async Task> EnqueueWorkshopUpdates() + { + ISet subscribedItems = await SteamManager.Workshop.GetAllSubscribedItems(); + + var needInstalling = subscribedItems.Where(item + => !WorkshopPackages.Any(p + => item.Id == p.SteamWorkshopId + && p.InstallTime.HasValue + && item.LatestUpdateTime <= p.InstallTime)) + .ToArray(); + if (needInstalling.Any()) + { + await Task.WhenAll( + needInstalling.Select(SteamManager.Workshop.DownloadModThenEnqueueInstall)); + } + + return needInstalling; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index e4b05779f..22c5061a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -14,6 +14,7 @@ using FarseerPhysics; using Barotrauma.Extensions; using Barotrauma.Steam; using System.Threading.Tasks; +using Barotrauma.ClientSource.Settings; using Barotrauma.MapCreatures.Behavior; using static Barotrauma.FabricationRecipe; @@ -73,8 +74,6 @@ namespace Barotrauma private static readonly ChatManager chatManager = new ChatManager(true, 64); - public static Dictionary Keybinds = new Dictionary(); - public static void Init() { OpenAL.Alc.SetErrorReasonCallback((string msg) => NewMessage(msg, Color.Orange)); @@ -84,7 +83,7 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f }; - var toggleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform, Anchor.TopLeft), TextManager.Get("DebugConsoleHelpText"), Color.GreenYellow, GUI.SmallFont, Alignment.CenterLeft, style: null); + var toggleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform, Anchor.TopLeft), TextManager.Get("DebugConsoleHelpText"), Color.GreenYellow, GUIStyle.SmallFont, Alignment.CenterLeft, style: null); var closeButton = new GUIButton(new RectTransform(new Vector2(0.025f, 1.0f), toggleText.RectTransform, Anchor.TopRight), "X", style: null) { @@ -139,7 +138,7 @@ namespace Barotrauma var newMsg = queuedMessages.Dequeue(); AddMessage(newMsg); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(newMsg); if (unsavedMessages.Count >= messagesPerFile) @@ -153,9 +152,9 @@ namespace Barotrauma if (!IsOpen && GUI.KeyboardDispatcher.Subscriber == null) { - foreach (var (key, command) in Keybinds) + foreach (var (key, command) in DebugConsoleMapping.Instance.Bindings) { - if (PlayerInput.KeyHit(key)) + if (key.IsHit()) { ExecuteCommand(command); } @@ -275,7 +274,7 @@ namespace Barotrauma AddMessage(newMsg); } - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(newMsg); } @@ -313,7 +312,7 @@ namespace Barotrauma } }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, - msg.Text, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) + msg.Text, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -324,7 +323,7 @@ namespace Barotrauma else { var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - msg.Text, font: GUI.SmallFont, wrap: true) + msg.Text, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = msg.Color @@ -355,7 +354,7 @@ namespace Barotrauma CanBeFocused = false }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 170, 0), textContainer.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(20, 0) }, - command.help, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) + command.help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = Color.White @@ -398,17 +397,15 @@ namespace Barotrauma private static void InitProjectSpecific() { -#if WINDOWS commands.Add(new Command("copyitemnames", "", (string[] args) => { StringBuilder sb = new StringBuilder(); foreach (ItemPrefab mp in ItemPrefab.Prefabs) { - sb.AppendLine(mp.Name); + sb.AppendLine(mp.Name.Value); } Clipboard.SetText(sb.ToString()); })); -#endif commands.Add(new Command("autohull", "", (string[] args) => { @@ -534,8 +531,8 @@ namespace Barotrauma return; } - string subName = args.Length > 0 ? args[0] : ""; - if (string.IsNullOrWhiteSpace(subName)) + Identifier subName = (args.Length > 0 ? args[0] : "").ToIdentifier(); + if (subName.IsEmpty) { ThrowError("No submarine specified."); return; @@ -554,7 +551,7 @@ namespace Barotrauma levelGenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(p => p.Identifier == levelGenerationIdentifier); } - if (SubmarineInfo.SavedSubmarines.None(s => s.Name.ToLowerInvariant() == subName.ToLowerInvariant())) + if (SubmarineInfo.SavedSubmarines.None(s => s.Name == subName)) { ThrowError($"Cannot find a sub that matches the name \"{subName}\"."); return; @@ -566,8 +563,7 @@ namespace Barotrauma commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { - SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; - SteamManager.SetSteamworksNetworkingDebugLog(SteamManager.NetworkingDebugLog); + SteamManager.SetSteamworksNetworkingDebugLog(!SteamManager.NetworkingDebugLog); })); commands.Add(new Command("readycheck", "Commence a ready check in multiplayer.", (string[] args) => @@ -586,27 +582,26 @@ namespace Barotrauma string keyString = args[0]; string command = args[1]; - if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object outKey) && outKey is Keys key) + KeyOrMouse key = Enum.TryParse(keyString, ignoreCase: true, out var outKey) + ? outKey + : Enum.TryParse(keyString, ignoreCase: true, out var outMouseButton) + ? outMouseButton + : (KeyOrMouse)MouseButton.None; + + if (key.Key == Keys.None && key.MouseButton == MouseButton.None) { - if (Keybinds.ContainsKey(key)) - { - Keybinds[key] = command; - } - else - { - Keybinds.Add(key, command); - } - NewMessage($"\"{command}\" bound to {key}.", GUI.Style.Green); - - if (GameMain.Config.keyMapping.FirstOrDefault(bind => bind.Key != Keys.None && bind.Key == key) is { } existingBind) - { - AddWarning($"\"{key}\" has already been bound to {(InputType)GameMain.Config.keyMapping.IndexOf(existingBind)}. The keybind will perform both actions when pressed."); - } - + ThrowError($"Invalid key {keyString}."); return; } + + DebugConsoleMapping.Instance.Set(key, command); + NewMessage($"\"{command}\" bound to {key}.", GUIStyle.Green); + + if (GameSettings.CurrentConfig.KeyMap.Bindings.FirstOrDefault(bind => bind.Value.Key != Keys.None && bind.Value.Key == key) is { } existingBind && existingBind.Value != null) + { + AddWarning($"\"{key}\" has already been bound to {existingBind.Key}. The keybind will perform both actions when pressed."); + } - ThrowError($"Invalid key {keyString}."); }, isCheat: false, getValidArgs: () => new[] { Enum.GetNames(typeof(Keys)), new[] { "\"\"" } })); commands.Add(new Command("unbindkey", "unbindkey [key]: Unbinds a command.", (string[] args) => @@ -618,40 +613,42 @@ namespace Barotrauma } string keyString = args[0]; - if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object outKey) && outKey is Keys key) + + KeyOrMouse key = Enum.TryParse(keyString, ignoreCase: true, out var outKey) + ? outKey + : Enum.TryParse(keyString, ignoreCase: true, out var outMouseButton) + ? outMouseButton + : (KeyOrMouse)MouseButton.None; + + if (key.Key == Keys.None && key.MouseButton == MouseButton.None) { - if (Keybinds.ContainsKey(key)) - { - Keybinds.Remove(key); - } - NewMessage("Keybind unbound.", GUI.Style.Green); + ThrowError($"Invalid key {keyString}."); return; } - ThrowError($"Invalid key {keyString}."); - }, isCheat: false, getValidArgs: () => new[] { Keybinds.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); + DebugConsoleMapping.Instance.Remove(key); + NewMessage("Keybind unbound.", GUIStyle.Green); + return; + }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); commands.Add(new Command("savebinds", "savebinds: Writes current keybinds into the config file.", (string[] args) => { - ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({Keybinds.Count} keybind(s) assigned) Y/N", + ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({DebugConsoleMapping.Instance.Bindings.Count} keybind(s) assigned) Y/N", (option2) => { - if (option2.ToLowerInvariant() != "y") + if (option2.ToIdentifier() != "y") { - NewMessage("Aborted.", GUI.Style.Red); + NewMessage("Aborted.", GUIStyle.Red); return; } - GameSettings.ConsoleKeybinds = new Dictionary(Keybinds); - GameMain.Config.SaveNewPlayerConfig(); - - NewMessage($"{Keybinds.Count} keybind(s) written to the config file.", GUI.Style.Green); + GameSettings.SaveCurrentConfig(); }); }, isCheat: false)); commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) => { SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid; - NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUI.Style.Green); + NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUIStyle.Green); })); commands.Add(new Command("spreadsheetexport", "Export items in format recognized by the spreadsheet importer.", (string[] args) => @@ -801,7 +798,7 @@ namespace Barotrauma string colorString = string.Join(",", add ? args.SkipLast(1) : args); if (colorString.Equals("restore", StringComparison.OrdinalIgnoreCase)) { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.OriginalAmbientLight != null) { @@ -823,7 +820,7 @@ namespace Barotrauma GameMain.LightManager.AmbientLight = add ? GameMain.LightManager.AmbientLight.Add(color) : color; } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.OriginalAmbientLight ??= hull.AmbientLight; hull.AmbientLight = add ? hull.AmbientLight.Add(color) : color; @@ -987,14 +984,14 @@ namespace Barotrauma { if (entity is Item item) { - if (item.prefab.Identifier != args[0] && !item.Tags.Contains(args[0])) { continue; } + if (item.Prefab.Identifier != args[0] && !item.Tags.Contains(args[0])) { continue; } item.Reset(); if (MapEntity.SelectedList.Contains(item)) { item.CreateEditingHUD(); } entityFound = true; } else if (entity is Structure structure) { - if (structure.prefab.Identifier != args[0] && !structure.Tags.Contains(args[0])) { continue; } + if (structure.Prefab.Identifier != args[0] && !structure.Tags.Contains(args[0])) { continue; } structure.Reset(); if (MapEntity.SelectedList.Contains(structure)) { structure.CreateEditingHUD(); } entityFound = true; @@ -1018,7 +1015,7 @@ namespace Barotrauma { return new string[][] { - MapEntityPrefab.List.Select(me => me.Identifier).ToArray() + MapEntityPrefab.List.Select(me => me.Identifier.Value).ToArray() }; })); @@ -1103,11 +1100,6 @@ namespace Barotrauma } }, isCheat: true)); - commands.Add(new Command("tutorial", "", (string[] args) => - { - TutorialMode.StartTutorial(Tutorials.Tutorial.Tutorials[0]); - })); - commands.Add(new Command("save|savesub", "save [submarine name]: Save the currently loaded submarine using the specified name.", (string[] args) => { if (args.Length < 1) { return; } @@ -1196,7 +1188,7 @@ namespace Barotrauma var msgBox = new GUIMessageBox( args.Length > 0 ? args[0] : "", args.Length > 1 ? args[1] : "", - buttons: new string[] { "OK" }, + buttons: new LocalizedString[] { "OK" }, type: args.Length < 3 || args[2] == "default" ? GUIMessageBox.Type.Default : GUIMessageBox.Type.InGame); msgBox.Buttons[0].OnClicked = msgBox.Close; @@ -1217,10 +1209,12 @@ namespace Barotrauma { if (args.None() || !bool.TryParse(args[0], out bool state)) { - state = !GameMain.Config.DisableVoiceChatFilters; + state = !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; } - GameMain.Config.DisableVoiceChatFilters = state; - NewMessage("Voice chat filters " + (GameMain.Config.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + var config = GameSettings.CurrentConfig; + config.Audio.DisableVoiceChatFilters = state; + GameSettings.SetCurrentConfig(config); + NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); }); AssignRelayToServer("togglevoicechatfilters", false); @@ -1345,7 +1339,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - fabricableItems.AddRange(itemPrefab.FabricationRecipes); + fabricableItems.AddRange(itemPrefab.FabricationRecipes.Values); } foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { @@ -1439,14 +1433,14 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab iPrefab in ItemPrefab.Prefabs) { - fabricableItems.AddRange(iPrefab.FabricationRecipes); + fabricableItems.AddRange(iPrefab.FabricationRecipes.Values); } string itemNameOrId = args[0].ToLowerInvariant(); ItemPrefab itemPrefab = (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab; + MapEntityPrefab.Find(null, identifier: itemNameOrId.ToIdentifier(), showErrorMessages: false)) as ItemPrefab; if (itemPrefab == null) { @@ -1459,7 +1453,7 @@ namespace Barotrauma // omega nesting incoming if (fabricationRecipe != null) { - foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) + foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) { NewMessage(" If bought at " + itemLocationPrice.Key + " it costs " + itemLocationPrice.Value.Price); int totalPrice = 0; @@ -1473,7 +1467,7 @@ namespace Barotrauma totalPrice += defaultPrice; totalBestPrice += ingredientItemPrefab.GetMinPrice(); int basePrice = defaultPrice; - foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) + foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) { if (basePrice > ingredientItemLocationPrice.Value.Price) { @@ -1499,7 +1493,7 @@ namespace Barotrauma }, () => { - return new string[][] { ItemPrefab.Prefabs.SelectMany(p => p.Aliases).Concat(ItemPrefab.Prefabs.Select(p => p.Identifier)).ToArray() }; + return new string[][] { ItemPrefab.Prefabs.SelectMany(p => p.Aliases).Concat(ItemPrefab.Prefabs.Select(p => p.Identifier.Value)).ToArray() }; }, isCheat: false)); commands.Add(new Command("checkcraftingexploits", "checkcraftingexploits: Finds outright item exploits created by buying store-bought ingredients and constructing them into sellable items.", (string[] args) => @@ -1507,7 +1501,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - fabricableItems.AddRange(itemPrefab.FabricationRecipes); + fabricableItems.AddRange(itemPrefab.FabricationRecipes.Values); } List> costDifferences = new List>(); @@ -1550,7 +1544,7 @@ namespace Barotrauma if (costDifference > maximumAllowedCost || costDifference < 0f) { float ratio = (float)fabricationCostStore.Value / defaultCost.Value; - string message = "Fabricating \"" + itemPrefab.Name + "\" costs " + (int)(ratio * 100) + "% of the price of the item, or " + costDifference + " more. Item price: " + defaultCost.Value + ", ingredient prices: " + fabricationCostStore.Value; + string message = $"Fabricating \"{itemPrefab.Name}\" costs {(int)(ratio * 100)}% of the price of the item, or {costDifference} more. Item price: {defaultCost.Value}, ingredient prices: {fabricationCostStore.Value}"; costDifferences.Add(new Tuple(message, costDifference)); } } @@ -1570,7 +1564,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab iP in ItemPrefab.Prefabs) { - fabricableItems.AddRange(iP.FabricationRecipes); + fabricableItems.AddRange(iP.FabricationRecipes.Values); } if (args.Length < 2) { @@ -1617,7 +1611,7 @@ namespace Barotrauma List fabricableItems = new List(); foreach (ItemPrefab iP in ItemPrefab.Prefabs) { - fabricableItems.AddRange(iP.FabricationRecipes); + fabricableItems.AddRange(iP.FabricationRecipes.Values); } if (args.Length < 1) { @@ -1659,8 +1653,8 @@ namespace Barotrauma foreach (DeconstructItem deconstructItem in parentItem.DeconstructItems) { ItemPrefab itemPrefab = - (MapEntityPrefab.Find(deconstructItem.ItemIdentifier, identifier: null, showErrorMessages: false) ?? - MapEntityPrefab.Find(null, identifier: deconstructItem.ItemIdentifier, showErrorMessages: false)) as ItemPrefab; + (MapEntityPrefab.Find(deconstructItem.ItemIdentifier.Value, identifier: null, showErrorMessages: false) ?? + MapEntityPrefab.Find(null, identifier: deconstructItem.ItemIdentifier, showErrorMessages: false)) as ItemPrefab; if (itemPrefab == null) { ThrowError($" Couldn't find deconstruct product \"{deconstructItem.ItemIdentifier}\"!"); @@ -1684,7 +1678,7 @@ namespace Barotrauma if (!(me is ISerializableEntity serializableEntity)) { continue; } if (serializableEntity.SerializableProperties == null) { continue; } - if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) + if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToIdentifier(), out SerializableProperty property)) { propertyFound = true; object prevValue = property.GetValue(me); @@ -1701,7 +1695,7 @@ namespace Barotrauma { foreach (ItemComponent ic in item.Components) { - ic.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty componentProperty); + ic.SerializableProperties.TryGetValue(args[0].ToIdentifier(), out SerializableProperty componentProperty); if (componentProperty == null) { continue; } propertyFound = true; object prevValue = componentProperty.GetValue(ic); @@ -1723,7 +1717,7 @@ namespace Barotrauma }, () => { - List propertyList = new List(); + List propertyList = new List(); foreach (MapEntity me in MapEntity.SelectedList) { if (!(me is ISerializableEntity serializableEntity)) { continue; } @@ -1740,55 +1734,63 @@ namespace Barotrauma return new string[][] { - propertyList.Distinct().ToArray(), - new string[0] + propertyList.Distinct().Select(i => i.Value).ToArray(), + Array.Empty() }; })); commands.Add(new Command("checkmissingloca", "", (string[] args) => { - //key = text tag, value = list of languages the tag is missing from - Dictionary> missingTags = new Dictionary>(); - Dictionary> tags = new Dictionary>(); - foreach (string language in TextManager.AvailableLanguages) + void SwapLanguage(LanguageIdentifier language) { - TextManager.Language = language; - tags.Add(language, new HashSet(TextManager.GetAllTagTextPairs().Select(t => t.Key))); + var config = GameSettings.CurrentConfig; + config.Language = language; + GameSettings.SetCurrentConfig(config); + } + + //key = text tag, value = list of languages the tag is missing from + Dictionary> missingTags = new Dictionary>(); + Dictionary> tags = new Dictionary>(); + foreach (LanguageIdentifier language in TextManager.AvailableLanguages) + { + SwapLanguage(language); + tags.Add(language, new HashSet(TextManager.GetAllTagTextPairs().Select(t => t.Key))); } - foreach (string language in TextManager.AvailableLanguages) + foreach (LanguageIdentifier language in TextManager.AvailableLanguages) { //check missing mission texts - foreach (var missionPrefab in MissionPrefab.List) + foreach (var missionPrefab in MissionPrefab.Prefabs) { - string missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeString("textidentifier", string.Empty)); - string nameIdentifier = "missionname." + missionId; + Identifier missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); + Identifier nameIdentifier = $"missionname.{missionId}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - string descriptionIdentifier = "missiondescription." + missionId; + Identifier descriptionIdentifier = $"missiondescription.{missionId}".ToIdentifier(); if (!tags[language].Contains(descriptionIdentifier)) { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } missingTags[descriptionIdentifier].Add(language); } } foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { - if (sub.Type != SubmarineType.Player) { continue; } - string nameIdentifier = "submarine.name." + sub.Name.ToLowerInvariant(); + if (sub.Type != SubmarineType.Player || !sub.IsVanillaSubmarine()) { continue; } + + Identifier nameIdentifier = $"submarine.name.{sub.Name}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - string descriptionIdentifier = "submarine.description." + sub.Name.ToLowerInvariant(); + Identifier descriptionIdentifier = ("submarine.description." + sub.Name).ToIdentifier(); if (!tags[language].Contains(descriptionIdentifier)) { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } missingTags[descriptionIdentifier].Add(language); } } @@ -1803,18 +1805,18 @@ namespace Barotrauma continue; } - string afflictionId = affliction.TranslationOverride ?? affliction.Identifier; - string nameIdentifier = "afflictionname." + afflictionId; + Identifier afflictionId = affliction.TranslationIdentifier; + Identifier nameIdentifier = $"afflictionname.{afflictionId}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } - - string descriptionIdentifier = "afflictiondescription." + afflictionId; + + Identifier descriptionIdentifier = $"afflictiondescription.{afflictionId}".ToIdentifier(); if (!tags[language].Contains(descriptionIdentifier)) { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } missingTags[descriptionIdentifier].Add(language); } } @@ -1823,10 +1825,10 @@ namespace Barotrauma { foreach (var talentSubTree in talentTree.TalentSubTrees) { - string nameIdentifier = "talenttree." + talentSubTree.Identifier; + Identifier nameIdentifier = $"talenttree.{talentSubTree.Identifier}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } } @@ -1834,10 +1836,10 @@ namespace Barotrauma foreach (var talent in TalentPrefab.TalentPrefabs) { - string nameIdentifier = "talentname." + talent.Identifier; + Identifier nameIdentifier = $"talentname.{talent.Identifier}".ToIdentifier(); if (!tags[language].Contains(nameIdentifier)) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } } @@ -1845,43 +1847,138 @@ namespace Barotrauma //check missing entity names foreach (MapEntityPrefab me in MapEntityPrefab.List) { - string nameIdentifier = "entityname." + me.Identifier; + Identifier nameIdentifier = ("entityname." + me.Identifier).ToIdentifier(); if (tags[language].Contains(nameIdentifier)) { continue; } if (me is ItemPrefab itemPrefab) { - nameIdentifier = itemPrefab.ConfigElement?.GetAttributeString("nameidentifier", null) ?? nameIdentifier; + nameIdentifier = itemPrefab.ConfigElement?.GetAttributeIdentifier("nameidentifier", nameIdentifier) ?? nameIdentifier; if (nameIdentifier != null) { if (tags[language].Contains("entityname." + nameIdentifier)) { continue; } } } - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } + if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } missingTags[nameIdentifier].Add(language); } } - foreach (string englishTag in tags["English"]) + foreach (Identifier englishTag in tags[TextManager.DefaultLanguage]) { - foreach (string language in TextManager.AvailableLanguages) + foreach (LanguageIdentifier language in TextManager.AvailableLanguages) { - if (language == "English") { continue; } + if (language == TextManager.DefaultLanguage) { continue; } if (!tags[language].Contains(englishTag)) { - if (!missingTags.ContainsKey(englishTag)) { missingTags[englishTag] = new HashSet(); } + if (!missingTags.ContainsKey(englishTag)) { missingTags[englishTag] = new HashSet(); } missingTags[englishTag].Add(language); } } } - List lines = missingTags.Select(t => "\"" + t.Key + "\"\n missing from " + string.Join(", ", t.Value)).ToList(); + List lines = new List + { + "Missing from English:" + }; + Dictionary> missingByLanguages = new Dictionary>(); + List missingFromEnglish = new List(); + foreach (KeyValuePair> kvp in missingTags) + { + if (kvp.Value.Contains(TextManager.DefaultLanguage)) + { + missingFromEnglish.Add(kvp.Key.Value); + } + else + { + string languagesStr = string.Join(", ", kvp.Value.OrderBy(v => v.Value.Value)); + if (!missingByLanguages.ContainsKey(languagesStr)) + { + missingByLanguages.Add(languagesStr, new List()); + } + missingByLanguages[languagesStr].Add(kvp.Key.Value); + } + } + foreach (string text in missingFromEnglish.OrderBy(v => v)) + { + lines.Add(text); + } + + foreach (KeyValuePair> missingByLanguage in missingByLanguages) + { + lines.Add(string.Empty); + lines.Add($"Missing from {missingByLanguage.Key}"); + foreach (string text in missingByLanguage.Value.OrderBy(v => v)) + { + lines.Add(text); + } + } + string filePath = "missingloca.txt"; Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); - TextManager.Language = "English"; + SwapLanguage(TextManager.DefaultLanguage); + })); + + commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => + { + if (args.Length < 2) + { + ThrowError("Please specify two files two compare."); + return; + } + + XDocument doc1 = XMLExtensions.TryLoadXml(args[0]); + if (doc1?.Root == null) + { + ThrowError($"Could not load the file \"{args[0]}\""); + return; + } + XDocument doc2 = XMLExtensions.TryLoadXml(args[1]); + if (doc2?.Root == null) + { + ThrowError($"Could not load the file \"{args[1]}\""); + return; + } + + var content1 = getContent(doc1.Root); + var content2 = getContent(doc2.Root); + + foreach (KeyValuePair kvp in content1) + { + if (!content2.ContainsKey(kvp.Key)) + { + ThrowError($"File 2 doesn't contain the text tag \"{kvp.Key}\""); + } + else + { + if (content2[kvp.Key] != kvp.Value) + { + ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}"); + } + } + } + foreach (KeyValuePair kvp in content2) + { + if (!content1.ContainsKey(kvp.Key)) + { + ThrowError($"File 1 doesn't contain the text tag \"{kvp.Key}\""); + } + } + + static Dictionary getContent(XElement element) + { + Dictionary content = new Dictionary(); + foreach (XElement subElement in element.Elements()) + { + string key = subElement.Name.ToString().ToLowerInvariant(); + if (content.ContainsKey(key)) { continue; } + content.Add(key, subElement.ElementInnerText()); + } + return content; + } })); commands.Add(new Command("eventstats", "", (string[] args) => @@ -1959,7 +2056,7 @@ namespace Barotrauma commands.Add(new Command("showballastflorasprite", "", (string[] args) => { BallastFloraBehavior.AlwaysShowBallastFloraSprite = !BallastFloraBehavior.AlwaysShowBallastFloraSprite; - NewMessage("ok", GUI.Style.Green); + NewMessage("ok", GUIStyle.Green); })); commands.Add(new Command("printreceivertransfers", "", (string[] args) => @@ -2046,8 +2143,8 @@ namespace Barotrauma if (mapEntity is Item item) { item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, - (int)(item.Prefab.sprite.size.X * item.Prefab.Scale), - (int)(item.Prefab.sprite.size.Y * item.Prefab.Scale)); + (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale), + (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale)); } else if (mapEntity is Structure structure) { @@ -2196,8 +2293,8 @@ namespace Barotrauma List lines = new List(); foreach (MapEntityPrefab me in MapEntityPrefab.List) { - lines.Add("" + me.Name + ""); - lines.Add("" + me.Description + ""); + lines.Add($"{me.Name}"); + lines.Add($"{me.Description}"); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); @@ -2213,12 +2310,12 @@ namespace Barotrauma foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs()) { - if (string.IsNullOrEmpty(eventPrefab.Identifier)) + if (eventPrefab.Identifier.IsEmpty) { continue; } docs.Add(eventPrefab.ConfigElement.Document); - getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier); + getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier.Value); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); @@ -2250,7 +2347,7 @@ namespace Barotrauma } int i = 1; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -2326,7 +2423,7 @@ namespace Barotrauma - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property in properties) { object[] attributes = property.GetCustomAttributes(true); @@ -2347,10 +2444,10 @@ namespace Barotrauma } propertyTypeName = string.Join("/", valueNames); } - string defaultValueString = serialize.defaultValue?.ToString() ?? ""; + string defaultValueString = serialize.DefaultValue?.ToString() ?? ""; if (property.PropertyType == typeof(float)) { - defaultValueString = ((float)serialize.defaultValue).ToString(CultureInfo.InvariantCulture); + defaultValueString = ((float)serialize.DefaultValue).ToString(CultureInfo.InvariantCulture); } lines.Add(" [tr]"); @@ -2412,7 +2509,7 @@ namespace Barotrauma commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => { if (args.Length != 1) return; - TextManager.CheckForDuplicates(args[0]); + TextManager.CheckForDuplicates(args[0].ToIdentifier().ToLanguageIdentifier()); })); commands.Add(new Command("writetocsv|xmltocsv", "Writes the default language (English) to a .csv file.", (string[] args) => @@ -2470,8 +2567,8 @@ namespace Barotrauma { var property = allProperties[j].Second; string propertyName = (allProperties[j].First.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); - string displayName = TextManager.Get($"sp.{propertyName}.name", returnNull: true); - if (displayName == null) + LocalizedString displayName = TextManager.Get($"sp.{propertyName}.name"); + if (displayName.IsNullOrEmpty()) { displayName = property.Name.FormatCamelCaseWithSpaces(); @@ -2494,26 +2591,28 @@ namespace Barotrauma commands.Add(new Command("cleanbuild", "", (string[] args) => { - GameMain.Config.MusicVolume = 0.5f; - GameMain.Config.SoundVolume = 0.5f; - GameMain.Config.DynamicRangeCompressionEnabled = true; - GameMain.Config.VoipAttenuationEnabled = true; + /*GameSettings.CurrentConfig.MusicVolume = 0.5f; + GameSettings.CurrentConfig.SoundVolume = 0.5f; + GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true; + GameSettings.CurrentConfig.VoipAttenuationEnabled = true; NewMessage("Music and sound volume set to 0.5", Color.Green); - GameMain.Config.GraphicsWidth = 0; - GameMain.Config.GraphicsHeight = 0; - GameMain.Config.WindowMode = WindowMode.BorderlessWindowed; + GameSettings.CurrentConfig.GraphicsWidth = 0; + GameSettings.CurrentConfig.GraphicsHeight = 0; + GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed; NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green); NewMessage("Fullscreen enabled", Color.Green); - GameSettings.VerboseLogging = false; + GameSettings.CurrentConfig.VerboseLogging = false; - if (GameMain.Config.MasterServerUrl != "http://www.undertowgames.com/baromaster") + if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster") { - ThrowError("MasterServerUrl \"" + GameMain.Config.MasterServerUrl + "\"!"); + ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!"); } - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig();*/ + throw new NotImplementedException(); + #warning TODO: reimplement var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder); @@ -2606,10 +2705,11 @@ namespace Barotrauma return; } - GameMain.Config.SelectCorePackage(GameMain.Config.CurrentCorePackage, true); + ContentPackageManager.EnabledPackages.ReloadCore(); })); - commands.Add(new Command("ingamemodswap", "", (string[] args) => + #warning TODO: reimplement? + /*commands.Add(new Command("ingamemodswap", "", (string[] args) => { ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap; if (ContentPackage.IngameModSwap) @@ -2620,7 +2720,7 @@ namespace Barotrauma { NewMessage("Disabled ingame mod swapping"); } - })); + }));*/ AssignOnClientExecute( "giveperm", @@ -2820,7 +2920,7 @@ namespace Barotrauma ThrowError("Please give the location type after the command."); return; } - var locationType = LocationType.List.Find(lt => lt.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == args[0]); if (locationType == null) { ThrowError($"Could not find the location type \"{args[0]}\"."); @@ -2832,7 +2932,7 @@ namespace Barotrauma { return new string[][] { - LocationType.List.Select(lt => lt.Identifier).ToArray() + LocationType.Prefabs.Select(lt => lt.Identifier.Value).ToArray() }; })); #endif @@ -3028,67 +3128,6 @@ namespace Barotrauma if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); } }, isCheat: true)); - commands.Add(new Command("gender", "Set the gender of the controlled character. Allowed parameters: Male, Female, None.", args => - { - var character = Character.Controlled; - if (character == null) - { - ThrowError("Not controlling any character!"); - return; - } - if (args.Length == 0) - { - ThrowError("No parameters provided!"); - return; - } - if (Enum.TryParse(args[0], true, out Gender gender)) - { - character.Info.Gender = gender; - character.ReloadHead(); - foreach (var limb in character.AnimController.Limbs) - { - if (limb.type != LimbType.Head) - { - limb.RecreateSprites(); - } - foreach (var wearable in limb.WearingItems) - { - if (wearable.Gender != Gender.None && wearable.Gender != gender) - { - wearable.Gender = gender; - } - } - } - } - }, isCheat: true)); - - commands.Add(new Command("race", "Set race of the controlled character. Allowed parameters: White, Black, Asian, None.", args => - { - var character = Character.Controlled; - if (character == null) - { - ThrowError("Not controlling any character!"); - return; - } - if (args.Length == 0) - { - ThrowError("No parameters provided!"); - return; - } - if (Enum.TryParse(args[0], true, out Race race)) - { - character.Info.Race = race; - character.ReloadHead(); - foreach (var limb in character.AnimController.Limbs) - { - if (limb.type != LimbType.Head) - { - limb.RecreateSprites(); - } - } - } - }, isCheat: true)); - commands.Add(new Command("head", "Load the head sprite and the wearables (hair etc). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args => { var character = Character.Controlled; @@ -3188,7 +3227,7 @@ namespace Barotrauma { return new string[][] { - SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName).ToArray() + SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName.Value).ToArray() }; }, isCheat: true)); @@ -3276,7 +3315,7 @@ namespace Barotrauma } case "identifier": case "id": - sprites = Sprite.LoadedSprites.Where(s => s.EntityID != null && s.EntityID.Equals(secondArg, StringComparison.OrdinalIgnoreCase)); + sprites = Sprite.LoadedSprites.Where(s => s.EntityIdentifier != null && s.EntityIdentifier == secondArg); if (sprites.Any()) { foreach (var s in sprites) @@ -3377,7 +3416,7 @@ namespace Barotrauma PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price, adjustDown, depth, adjustItemType); break; case AdjustItemTypes.Additive: - PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price + (int)((newPrice - materialPrefab.DefaultPrice.Price) / (double)fabricationRecipe.RequiredItems.Count), adjustDown, depth, adjustItemType); + PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price + (int)((newPrice - materialPrefab.DefaultPrice.Price) / (double)fabricationRecipe.RequiredItems.Length), adjustDown, depth, adjustItemType); break; case AdjustItemTypes.Multiplicative: PrintItemCosts(newPrices, itemPrefab, fabricableItems, (int)(itemPrefab.DefaultPrice.Price * newPriceMult), adjustDown, depth, adjustItemType); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index 781d2c3ba..7f8c08485 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -94,7 +94,7 @@ namespace Barotrauma var (relative, min) = GetSizes(dialogType); - GUIMessageBox messageBox = new GUIMessageBox(string.Empty, string.Empty, new string[0], + GUIMessageBox messageBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize: relative, minSize: min, type: GUIMessageBox.Type.InGame, backgroundIcon: EventSet.GetEventSprite(spriteIdentifier)) { @@ -105,7 +105,7 @@ namespace Barotrauma messageBox.InnerFrame.ClearChildren(); messageBox.AutoClose = false; - GUI.Style.Apply(messageBox.InnerFrame, "DialogBox"); + GUIStyle.Apply(messageBox.InnerFrame, "DialogBox"); if (actionInstance != null) { @@ -222,11 +222,11 @@ namespace Barotrauma closeButton.SlideIn(0.5f, 0.33f, 16, SlideDirection.Down); InputType? closeInput = null; - if (GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None) { closeInput = InputType.Use; } - else if (GameMain.Config.KeyBind(InputType.Select).MouseButton == MouseButton.None) + else if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select].MouseButton == MouseButton.None) { closeInput = InputType.Select; } @@ -239,7 +239,7 @@ namespace Barotrauma { GUIButton btn = component as GUIButton; btn?.OnClicked(btn, btn.UserData); - btn?.Flash(GUI.Style.Green); + btn?.Flash(GUIStyle.Green); } }; } @@ -308,7 +308,7 @@ namespace Barotrauma AlwaysOverrideCursor = true }; - string translatedText = TextManager.Get(text, returnNull: true) ?? text; + LocalizedString translatedText = TextManager.Get(text); if (speaker?.Info != null && drawChathead) { @@ -335,9 +335,9 @@ namespace Barotrauma { foreach (string option in options) { - var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option, returnNull: true) ?? option, style: "ListBoxElement"); + var btn = new GUIButton(new RectTransform(new Vector2(0.9f, 0.01f), textContent.RectTransform), TextManager.Get(option), style: "ListBoxElement"); btn.TextBlock.TextAlignment = Alignment.CenterLeft; - btn.TextColor = btn.HoverTextColor = GUI.Style.Green; + btn.TextColor = btn.HoverTextColor = GUIStyle.Green; btn.TextBlock.Wrap = true; buttons.Add(btn); } @@ -384,7 +384,7 @@ namespace Barotrauma } // Too broken, left it here if I ever want to come back to it - private static List GetQuoteHighlights(string text, Color color) + /*private static List GetQuoteHighlights(string text, Color color) { char[] quotes = { '“', '”', '\"', '\'', '「', '」'}; @@ -406,6 +406,6 @@ namespace Barotrauma last.EndIndex = text.Length; } return textColors; - } + }*/ } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index 238445418..9e678f095 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -34,7 +35,7 @@ namespace Barotrauma var textOffset = new Vector2(-150, 0); spriteBatch.DrawCircle(drawPos, 600, 6, Color.White, thickness: 20); - GUI.DrawString(spriteBatch, drawPos + textOffset, ev.ToString(), Color.White, Color.Black, 0, GUI.LargeFont); + GUI.DrawString(spriteBatch, drawPos + textOffset, ev.ToString(), Color.White, Color.Black, 0, GUIStyle.LargeFont); } } @@ -46,24 +47,23 @@ namespace Barotrauma } float theoreticalMaxMonsterStrength = 10000; - float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * GameMain.GameSession.LevelData.Difficulty / 100; + float relativeMaxMonsterStrength = theoreticalMaxMonsterStrength * (GameMain.GameSession?.LevelData?.Difficulty ?? 0f) / 100; float absoluteMonsterStrength = monsterStrength / theoreticalMaxMonsterStrength; float relativeMonsterStrength = monsterStrength / relativeMaxMonsterStrength; - GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + (int)Math.Max(eventCoolDown, 0), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int)Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, currentIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int)Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, targetIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + (int)Math.Max(eventCoolDown, 0), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int)Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, currentIntensity), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int)Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUIStyle.Red, targetIntensity), Color.Black * 0.6f, 0, GUIStyle.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "Crew health: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "Hull integrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "Flooding amount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "Fire amount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "Enemy danger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "Current monster strength (total): " + (int)Math.Round(monsterStrength), Color.Lerp(GUI.Style.Green, GUI.Style.Red, relativeMonsterStrength), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 155), "Main events: " + (int)Math.Round(CumulativeMonsterStrengthMain), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 170), "Ruin events: " + (int)Math.Round(CumulativeMonsterStrengthRuins), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 185), "Wreck events: " + (int)Math.Round(CumulativeMonsterStrengthWrecks), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 200), "Cave events: " + (int)Math.Round(CumulativeMonsterStrengthCaves), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "Crew health: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "Hull integrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUIStyle.Red, GUIStyle.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "Flooding amount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, floodingAmount), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "Fire amount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, fireAmount), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "Enemy danger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUIStyle.Green, GUIStyle.Red, enemyDanger), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 140), "Current monster strength (total): " + (int)Math.Round(monsterStrength), Color.Lerp(GUIStyle.Green, GUIStyle.Red, relativeMonsterStrength), Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 155), "Main events: " + (int)Math.Round(CumulativeMonsterStrengthMain), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 170), "Ruin events: " + (int)Math.Round(CumulativeMonsterStrengthRuins), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 185), "Wreck events: " + (int)Math.Round(CumulativeMonsterStrengthWrecks), Color.White, Color.Black * 0.6f, 0, GUIStyle.SmallFont); #if DEBUG if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && @@ -101,7 +101,7 @@ namespace Barotrauma { isGraphSelected = false; } - Color intensityColor = Color.Lerp(Color.White, GUI.Style.Red, currentIntensity); + Color intensityColor = Color.Lerp(Color.White, GUIStyle.Red, currentIntensity); if (isGraphHovered || isGraphSelected) { graphRect.Size = new Point(GameMain.GraphicsWidth - 30, (int)(GameMain.GraphicsHeight * 0.35f)); @@ -122,7 +122,7 @@ namespace Barotrauma { height *= 3; string text = (order / 6).ToString(); - var font = GUI.SmallFont; + var font = GUIStyle.SmallFont; Vector2 textSize = font.MeasureString(text); Vector2 textPos = new Vector2(bottomPoint.X - textSize.X / 2, bottomPoint.Y + height * 1.5f); GUI.DrawString(sBatch, textPos, text, Color.White, font: font); @@ -175,23 +175,23 @@ namespace Barotrauma int x = graphRect.X; if (isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) { - GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } else if (crewAwayResetTimer > 0.0f) { - GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } else if (eventCoolDown > 0.0f) { - GUI.DrawString(spriteBatch, new Vector2(x, y), "Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } else if (currentIntensity > eventThreshold) { GUI.DrawString(spriteBatch, new Vector2(x, y), - "Intensity too high for new events: " + (int)(currentIntensity * 100) + "%/" + (int)(eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + "Intensity too high for new events: " + (int)(currentIntensity * 100) + "%/" + (int)(eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUIStyle.SmallFont); y += 15; } @@ -199,29 +199,29 @@ namespace Barotrauma { if (Submarine.MainSub == null) { break; } - GUI.DrawString(spriteBatch, new Vector2(x, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "New event (ID " + eventSet.Identifier + ") after: ", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; if (eventSet.PerCave) { - GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near cave", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near cave", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } if (eventSet.PerWreck) { - GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } if (eventSet.PerRuin) { - GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } if (roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(x, y), " " + (int) (eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int) (distanceTraveled * 100.0f) + " %)", - ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUI.Style.Green) * 0.8f, null, 0, GUI.SmallFont); + ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUIStyle.Green) * 0.8f, null, 0, GUIStyle.SmallFont); y += 12; } @@ -229,15 +229,15 @@ namespace Barotrauma { GUI.DrawString(spriteBatch, new Vector2(x, y), " intensity between " + eventSet.MinIntensity.FormatDoubleDecimal() + " and " + eventSet.MaxIntensity.FormatDoubleDecimal(), - Color.Orange * 0.8f, null, 0, GUI.SmallFont); - y += 12; + Color.Orange * 0.8f, null, 0, GUIStyle.SmallFont); + y += 12; } if (roundDuration < eventSet.MinMissionTime) { GUI.DrawString(spriteBatch, new Vector2(x, y), " " + (int) (eventSet.MinMissionTime - roundDuration) + " s", - Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUI.SmallFont); + Color.Lerp(GUIStyle.Yellow, GUIStyle.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUIStyle.SmallFont); } y += 15; @@ -249,14 +249,14 @@ namespace Barotrauma } } - GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUIStyle.SmallFont); y += yStep; foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) { - GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUIStyle.SmallFont); - Rectangle rect = new Rectangle(new Point(x + 5, y), GUI.SmallFont.MeasureString(ev.ToString()).ToPoint()); + Rectangle rect = new Rectangle(new Point(x + 5, y), GUIStyle.SmallFont.MeasureString(ev.ToString()).ToPoint()); Rectangle outlineRect = new Rectangle(rect.Location, rect.Size); outlineRect.Inflate(4, 4); @@ -331,8 +331,8 @@ namespace Barotrauma if (Screen.Selected is GameScreen screen) { Camera cam = screen.Cam; - Dictionary> tagsDictionary = new Dictionary>(); - foreach ((string key, List value) in scriptedEvent.Targets) + Dictionary> tagsDictionary = new Dictionary>(); + foreach ((Identifier key, List value) in scriptedEvent.Targets) { foreach (Entity entity in value) { @@ -342,24 +342,24 @@ namespace Barotrauma } else { - tagsDictionary.Add(entity, new List { key }); + tagsDictionary.Add(entity, new List { key }); } } } - string identifier = scriptedEvent.Prefab.Identifier; + Identifier identifier = scriptedEvent.Prefab.Identifier; - foreach ((Entity entity, List tags) in tagsDictionary) + foreach ((Entity entity, List tags) in tagsDictionary) { if (entity.Removed) { continue; } string text = tags.Aggregate("Tags:\n", (current, tag) => current + $" {tag.ColorizeObject()}\n").TrimEnd('\r', '\n'); - if (!string.IsNullOrWhiteSpace(identifier)) { text = $"Event: {identifier.ColorizeObject()}\n{text}"; } + if (!identifier.IsEmpty) { text = $"Event: {identifier.ColorizeObject()}\n{text}"; } - List richTextData = RichTextData.GetRichTextData(text, out text); + ImmutableArray? richTextData = RichTextData.GetRichTextData(text, out text); Vector2 entityPos = cam.WorldToScreen(entity.WorldPosition); - Vector2 infoSize = GUI.SmallFont.MeasureString(text); + Vector2 infoSize = GUIStyle.SmallFont.MeasureString(text); Vector2 infoPos = entityPos + new Vector2(128 * cam.Zoom, -(128 * cam.Zoom)); infoPos.Y -= infoSize.Y / 2; @@ -370,7 +370,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, infoRect, Color.White, isFilled: false); - GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextData, font: GUI.SmallFont); + GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextData, font: GUIStyle.SmallFont); GUI.DrawLine(spriteBatch, entityPos, new Vector2(infoRect.Location.X, infoRect.Location.Y + infoRect.Height / 2), Color.White); } @@ -489,15 +489,15 @@ namespace Barotrauma { text = text.TrimEnd('\r', '\n'); - string identifier = @event.Prefab.Identifier; - if (!string.IsNullOrWhiteSpace(identifier)) + Identifier identifier = @event.Prefab.Identifier; + if (!identifier.IsEmpty) { text = $"Identifier: {identifier.ColorizeObject()}\n{text}"; } - List richTextData = RichTextData.GetRichTextData(text, out text); + ImmutableArray? richTextData = RichTextData.GetRichTextData(text, out text); - Vector2 size = GUI.SmallFont.MeasureString(text); + Vector2 size = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = pinnedPosition; Rectangle infoRect; Rectangle? infoBarRect = null; @@ -520,7 +520,7 @@ namespace Barotrauma const string titleHeader = "Pinned event"; GUI.DrawRectangle(spriteBatch, barRect, Color.DarkGray * 0.8f, isFilled: true); - GUI.DrawString(spriteBatch, barRect.Location.ToVector2() + barRect.Size.ToVector2() / 2 - GUI.SubHeadingFont.MeasureString(titleHeader) / 2, titleHeader, Color.White); + GUI.DrawString(spriteBatch, barRect.Location.ToVector2() + barRect.Size.ToVector2() / 2 - GUIStyle.SubHeadingFont.MeasureString(titleHeader) / 2, titleHeader, Color.White); GUI.DrawRectangle(spriteBatch, barRect, Color.White); infoBarRect = barRect; } @@ -546,8 +546,14 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, infoRect, Color.White); - GUI.DrawStringWithColors(spriteBatch, pos, text, Color.White, richTextData, null, 0, GUI.SmallFont); - richTextData.Clear(); + if (richTextData.HasValue && richTextData.Value.Length > 0) + { + GUI.DrawStringWithColors(spriteBatch, pos, text, Color.White, richTextData.Value, null, 0, GUIStyle.SmallFont); + } + else + { + GUI.DrawString(spriteBatch, pos, text, Color.White, null, 0, GUIStyle.SmallFont); + } return infoBarRect ?? infoRect; } @@ -557,7 +563,7 @@ namespace Barotrauma switch (eventType) { case NetworkEventType.STATUSEFFECT: - string eventIdentifier = msg.ReadString(); + Identifier eventIdentifier = msg.ReadIdentifier(); UInt16 actionIndex = msg.ReadUInt16(); UInt16 targetCount = msg.ReadUInt16(); List targets = new List(); @@ -571,14 +577,14 @@ namespace Barotrauma var eventPrefab = EventSet.GetEventPrefab(eventIdentifier); if (eventPrefab == null) { return; } int j = 0; - foreach (XElement element in eventPrefab.ConfigElement.Descendants()) + foreach (var element in eventPrefab.ConfigElement.Descendants()) { if (j != actionIndex) { j++; continue; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } StatusEffect effect = StatusEffect.Load(subElement, $"EventManager.ClientRead ({eventIdentifier})"); @@ -633,13 +639,13 @@ namespace Barotrauma } break; case NetworkEventType.MISSION: - string missionIdentifier = msg.ReadString(); + Identifier missionIdentifier = msg.ReadIdentifier(); - MissionPrefab? prefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (prefab != null) { new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), - new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; @@ -657,7 +663,7 @@ namespace Barotrauma { GameMain.GameSession.Map.Connections[connectionIndex].Locked = false; new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), - new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); + Array.Empty(), type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs index 072e0615a..fcd12b072 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -13,7 +13,7 @@ namespace Barotrauma if (state != value) { base.State = value; - if (state == HostagesKilledState && !string.IsNullOrEmpty(hostagesKilledMessage)) + if (state == HostagesKilledState && !hostagesKilledMessage.IsNullOrEmpty()) { CreateMessageBox(string.Empty, hostagesKilledMessage); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs index e4de29441..08356c60c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CargoMission.cs @@ -8,23 +8,29 @@ namespace Barotrauma public override bool DisplayAsCompleted => false; public override bool DisplayAsFailed => false; - public override string GetMissionRewardText(Submarine sub) + public override RichString GetMissionRewardText(Submarine sub) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + LocalizedString retVal; if (rewardPerCrate.HasValue) { - string rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); - return TextManager.GetWithVariables("missionrewardcargopercrate", - new string[] { "[rewardpercrate]", "[itemcount]", "[maxitemcount]", "[totalreward]" }, - new string[] { rewardPerCrateText, itemsToSpawn.Count.ToString(), maxItemCount.ToString(), $"‖color:gui.orange‖{rewardText}‖end‖" }); + LocalizedString rewardPerCrateText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", rewardPerCrate.Value)); + retVal = TextManager.GetWithVariables("missionrewardcargopercrate", + ("[rewardpercrate]", rewardPerCrateText), + ("[itemcount]", itemsToSpawn.Count.ToString()), + ("[maxitemcount]", maxItemCount.ToString()), + ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖")); } else { - return TextManager.GetWithVariables("missionrewardcargo", - new string[] { "[totalreward]", "[itemcount]", "[maxitemcount]" }, - new string[] { $"‖color:gui.orange‖{rewardText}‖end‖", itemsToSpawn.Count.ToString(), maxItemCount.ToString() }); + retVal = TextManager.GetWithVariables("missionrewardcargo", + ("[totalreward]", $"‖color:gui.orange‖{rewardText}‖end‖"), + ("[itemcount]", itemsToSpawn.Count.ToString()), + ("[maxitemcount]", maxItemCount.ToString())); } + + return RichString.Rich(retVal); } public override void ClientReadInitial(IReadMessage msg) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 401f5278f..3957dff43 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -4,7 +4,7 @@ namespace Barotrauma { partial class CombatMission : Mission { - public override string Description + public override LocalizedString Description { get { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index f97da96d5..bb573b27d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -56,7 +56,7 @@ namespace Barotrauma for(int i = 0; i < resourceClusters.Count; i++) { - var identifier = msg.ReadString(); + var identifier = msg.ReadIdentifier(); var count = msg.ReadByte(); var resources = new Item[count]; for (int j = 0; j < count; j++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index ab7a3a6f0..cff6b76b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -9,11 +9,8 @@ namespace Barotrauma { abstract partial class Mission { - private readonly List shownMessages = new List(); - public IEnumerable ShownMessages - { - get { return shownMessages; } - } + private readonly List shownMessages = new List(); + public IEnumerable ShownMessages => shownMessages; public bool DisplayTargetHudIcons => Prefab.DisplayTargetHudIcons; @@ -32,29 +29,29 @@ namespace Barotrauma { int v = Difficulty ?? MissionPrefab.MinDifficulty; float t = MathUtils.InverseLerp(MissionPrefab.MinDifficulty, MissionPrefab.MaxDifficulty, v); - return ToolBox.GradientLerp(t, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return ToolBox.GradientLerp(t, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } - public virtual string GetMissionRewardText(Submarine sub) + public virtual RichString GetMissionRewardText(Submarine sub) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); - return TextManager.GetWithVariable("missionreward", "[reward]", $"‖color:gui.orange‖{rewardText}‖end‖"); + LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))); + return RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", "‖color:gui.orange‖"+rewardText+"‖end‖")); } - public string GetReputationRewardText(Location currLocation) + public RichString GetReputationRewardText(Location currLocation) { - List reputationRewardTexts = new List(); + List reputationRewardTexts = new List(); foreach (var reputationReward in ReputationRewards) { - string name = ""; + LocalizedString name = ""; - if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (reputationReward.Key == "location") { name = $"‖color:gui.orange‖{currLocation.Name}‖end‖"; } else { - var faction = FactionPrefab.Prefabs.Find(f => f.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); + var faction = FactionPrefab.Prefabs.Find(f => f.Identifier == reputationReward.Key); if (faction != null) { name = $"‖color:{XMLExtensions.ColorToString(faction.IconColor)}‖{faction.Name}‖end‖"; @@ -66,28 +63,28 @@ namespace Barotrauma } float normalizedValue = MathUtils.InverseLerp(-100.0f, 100.0f, reputationReward.Value); string formattedValue = ((int)reputationReward.Value).ToString("+#;-#;0"); //force plus sign for positive numbers - string rewardText = TextManager.GetWithVariables( + LocalizedString rewardText = TextManager.GetWithVariables( "reputationformat", - new string[] { "[reputationname]", "[reputationvalue]" }, - new string[] { name, $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" }); - reputationRewardTexts.Add(rewardText); + ("[reputationname]", name), + ("[reputationvalue]", $"‖color:{XMLExtensions.ColorToString(Reputation.GetReputationColor(normalizedValue))}‖{formattedValue}‖end‖" )); + reputationRewardTexts.Add(rewardText.Value); } - return TextManager.AddPunctuation(':', TextManager.Get("reputation"), string.Join(", ", reputationRewardTexts)); + return RichString.Rich(TextManager.AddPunctuation(':', TextManager.Get("reputation"), LocalizedString.Join(", ", reputationRewardTexts))); } partial void ShowMessageProjSpecific(int missionState) { int messageIndex = missionState - 1; - if (messageIndex >= Headers.Count && messageIndex >= Messages.Count) { return; } + if (messageIndex >= Headers.Length && messageIndex >= Messages.Length) { return; } if (messageIndex < 0) { return; } - string header = messageIndex < Headers.Count ? Headers[messageIndex] : ""; - string message = messageIndex < Messages.Count ? Messages[messageIndex] : ""; + LocalizedString header = messageIndex < Headers.Length ? Headers[messageIndex] : ""; + LocalizedString message = messageIndex < Messages.Length ? Messages[messageIndex] : ""; CoroutineManager.StartCoroutine(ShowMessageBoxAfterRoundSummary(header, message)); } - private IEnumerable ShowMessageBoxAfterRoundSummary(string header, string message) + private IEnumerable ShowMessageBoxAfterRoundSummary(LocalizedString header, LocalizedString message) { while (GUIMessageBox.VisibleBox?.UserData is RoundSummary) { @@ -97,10 +94,10 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - protected void CreateMessageBox(string header, string message) + protected void CreateMessageBox(LocalizedString header, LocalizedString message) { shownMessages.Add(message); - new GUIMessageBox(header, message, buttons: new string[0], type: GUIMessageBox.Type.InGame, icon: Prefab.Icon, parseRichText: true) + new GUIMessageBox(RichString.Rich(header), RichString.Rich(message), buttons: Array.Empty(), type: GUIMessageBox.Type.InGame, icon: Prefab.Icon) { IconColor = Prefab.IconColor }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 342b61004..3ec2386cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using System; + +namespace Barotrauma { abstract partial class MissionMode : GameMode { @@ -6,7 +8,7 @@ { foreach (Mission mission in missions) { - new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon, parseRichText: true) + new GUIMessageBox(RichString.Rich(mission.Name), RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, UserData = "missionstartmessage" diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index e3c6f8633..c8172bfaa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -4,7 +4,7 @@ using System.Xml.Linq; namespace Barotrauma { - partial class MissionPrefab + partial class MissionPrefab : PrefabWithUintIdentifier { public Sprite Icon { @@ -49,11 +49,11 @@ namespace Barotrauma private Sprite hudIcon; private Color? hudIconColor; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string name = subElement.Name.ToString(); if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) @@ -68,5 +68,10 @@ namespace Barotrauma } } } + + partial void DisposeProjectSpecific() + { + Icon?.Remove(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index ad66f2a91..bbcc7acca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using SharpFont; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -19,7 +20,6 @@ namespace Barotrauma private Face face; private uint size; private int baseHeight; - //private int lineHeight; private Dictionary texCoords; private List textures; private GraphicsDevice graphicsDevice; @@ -53,6 +53,8 @@ namespace Barotrauma } } + public bool ForceUpperCase = false; + public float LineHeight => baseHeight * 1.8f; private uint[] charRanges; @@ -79,9 +81,9 @@ namespace Barotrauma } } - public ScalableFont(XElement element, GraphicsDevice gd = null) + public ScalableFont(ContentXElement element, GraphicsDevice gd = null) : this( - element.GetAttributeString("file", ""), + element.GetAttributeContentPath("file")?.Value, (uint)element.GetAttributeInt("size", 14), gd, element.GetAttributeBool("dynamicloading", false), @@ -104,10 +106,7 @@ namespace Barotrauma break; } } - if (this.face == null) - { - this.face = new Face(Lib, filename); - } + this.face ??= new Face(Lib, filename); this.size = size; this.textures = new List(); this.texCoords = new Dictionary(); @@ -186,7 +185,7 @@ namespace Barotrauma GlyphData blankData = new GlyphData( advance: (float)face.Glyph.Metrics.HorizontalAdvance, texIndex: -1); //indicates no texture because the glyph is empty - + texCoords.Add(j, blankData); } continue; @@ -399,6 +398,52 @@ namespace Barotrauma } } + // TODO: refactor this further + private void HandleNewLineAndAlignment( + string text, + in Vector2 advanceUnit, + in Vector2 position, + in Vector2 scale, + Alignment alignment, + int i, + ref float lineWidth, + ref Vector2 currentLineOffset, + ref int lineNum, + ref Vector2 currentPos, + out uint charIndex, + out bool shouldContinue) + { + if ((alignment.HasFlag(Alignment.CenterX) || alignment.HasFlag(Alignment.Right)) && (lineWidth < 0.0f || text[i] == '\n')) + { + int startIndex = lineWidth < 0.0f ? i : (i + 1); + lineWidth = 0.0f; + for (int j = startIndex; j < text.Length; j++) + { + if (text[j] == '\n') { break; } + uint chrIndex = text[j]; + + var gd2 = GetGlyphData(chrIndex); + lineWidth += gd2.Advance; + } + currentLineOffset = -lineWidth * advanceUnit * scale.X; + if (alignment.HasFlag(Alignment.CenterX)) { currentLineOffset *= 0.5f; } + + currentLineOffset.X = MathF.Round(currentLineOffset.X); + currentLineOffset.Y = MathF.Round(currentLineOffset.Y); + } + if (text[i] == '\n') + { + lineNum++; + currentPos = position; + currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; + currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; + shouldContinue = true; charIndex = 0; return; + } + + shouldContinue = false; + charIndex = text[i]; + } + private GlyphData GetGlyphData(uint charIndex) { const uint DEFAULT_INDEX = 0x25A1; //U+25A1 = white square @@ -412,29 +457,27 @@ namespace Barotrauma return new GlyphData(texIndex: -1); } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { if (textures.Count == 0 && !DynamicLoading) { return; } + text = ApplyUpperCase(text, forceUpperCase); if (DynamicLoading) { DynamicRenderAtlas(graphicsDevice, text); } + float lineWidth = -1.0f; + Vector2 currentLineOffset = Vector2.Zero; + int lineNum = 0; Vector2 currentPos = position; Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); for (int i = 0; i < text.Length; i++) { - if (text[i] == '\n') - { - lineNum++; - currentPos = position; - currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; - currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; - continue; - } - - uint charIndex = text[i]; + HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i, + ref lineWidth, ref currentLineOffset, ref lineNum, ref currentPos, + out uint charIndex, out bool shouldContinue); + if (shouldContinue) { continue; } GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) @@ -444,20 +487,30 @@ namespace Barotrauma drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X; - sb.Draw(tex, currentPos + drawOffset, gd.TexCoords, color, rotation, origin, scale, se, layerDepth); + sb.Draw(tex, currentPos + currentLineOffset + drawOffset, gd.TexCoords, color, rotation, origin, scale, se, layerDepth); } currentPos += gd.Advance * advanceUnit * scale.X; } } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth) + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - DrawString(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth); + DrawString(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, alignment, forceUpperCase); } - public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color) + private string ApplyUpperCase(string text, ForceUpperCase forceUpperCase) + => forceUpperCase switch + { + Barotrauma.ForceUpperCase.Inherit => ForceUpperCase ? text.ToUpper() : text, + Barotrauma.ForceUpperCase.Yes => text.ToUpper(), + Barotrauma.ForceUpperCase.No => text + }; + + private readonly static VertexPositionColorTexture[] quadVertices = new VertexPositionColorTexture[4]; + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) { if (textures.Count == 0 && !DynamicLoading) { return; } + text = ApplyUpperCase(text, forceUpperCase); if (DynamicLoading) { DynamicRenderAtlas(graphicsDevice, text); @@ -478,21 +531,48 @@ namespace Barotrauma GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) { + float halfCharHeight = gd.TexCoords.Height * 0.5f; + float slantStrength = 0.35f; + float topItalicOffset = italics ? ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f; + float bottomItalicOffset = italics ? ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f; + Texture2D tex = textures[gd.TexIndex]; - sb.Draw(tex, currentPos + gd.DrawOffset, gd.TexCoords, color); + quadVertices[0].Position = new Vector3(currentPos + gd.DrawOffset + (bottomItalicOffset, gd.TexCoords.Height), 0.0f); + quadVertices[0].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Bottom / tex.Height); + quadVertices[0].Color = color; + + quadVertices[1].Position = new Vector3(currentPos + gd.DrawOffset + (topItalicOffset, 0.0f), 0.0f); + quadVertices[1].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Top / tex.Height); + quadVertices[1].Color = color; + + quadVertices[2].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + bottomItalicOffset, gd.TexCoords.Height), 0.0f); + quadVertices[2].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Bottom / tex.Height); + quadVertices[2].Color = color; + + quadVertices[3].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + topItalicOffset, 0.0f), 0.0f); + quadVertices[3].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Top / tex.Height); + quadVertices[3].Color = color; + + sb.Draw(tex, quadVertices, 0.0f); } currentPos.X += gd.Advance; } } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { - DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData, rtdOffset); + DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (!richTextData.HasValue || richTextData.Value.Length <= 0) { DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, forceUpperCase: forceUpperCase); return; } + + text = ApplyUpperCase(text, forceUpperCase); + + float lineWidth = -1.0f; + Vector2 currentLineOffset = Vector2.Zero; if (DynamicLoading) { DynamicRenderAtlas(graphicsDevice, text); @@ -503,27 +583,21 @@ namespace Barotrauma Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); int richTextDataIndex = 0; - RichTextData currentRichTextData = richTextData[richTextDataIndex]; + RichTextData currentRichTextData = richTextData.Value[richTextDataIndex]; for (int i = 0; i < text.Length; i++) { - if (text[i] == '\n') - { - lineNum++; - currentPos = position; - currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y; - currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y; - continue; - } - - uint charIndex = text[i]; + HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i, + ref lineWidth, ref currentLineOffset, ref lineNum, ref currentPos, + out uint charIndex, out bool shouldContinue); + if (shouldContinue) { continue; } Color currentTextColor; while (currentRichTextData != null && i + rtdOffset > currentRichTextData.EndIndex + lineNum) { richTextDataIndex++; - currentRichTextData = richTextDataIndex < richTextData.Count ? richTextData[richTextDataIndex] : null; + currentRichTextData = richTextDataIndex < richTextData.Value.Length ? richTextData.Value[richTextDataIndex] : null; } if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i + rtdOffset && i + rtdOffset <= currentRichTextData.EndIndex + lineNum) @@ -547,7 +621,7 @@ namespace Barotrauma drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X; - sb.Draw(tex, currentPos + drawOffset, gd.TexCoords, currentTextColor, rotation, origin, scale, se, layerDepth); + sb.Draw(tex, currentPos + currentLineOffset + drawOffset, gd.TexCoords, currentTextColor, rotation, origin, scale, se, layerDepth); } currentPos += gd.Advance * advanceUnit * scale.X; } @@ -628,6 +702,8 @@ namespace Barotrauma //A breaker (whitespace or CJK) was found earlier //in this line, so let's break the line there i = lastBreakerIndex.Value + 1; + gd = GetGlyphData(text[i]); + advance = gd.Advance; } nextLine(); @@ -648,7 +724,12 @@ namespace Barotrauma requestedCharPos = foundCharPos; return result; } - + + public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false) + { + return MeasureString(str.Value, removeExtraSpacing); + } + public Vector2 MeasureString(string text, bool removeExtraSpacing = false) { if (text == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 6e3ae862d..35ae05d0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -25,7 +25,7 @@ namespace Barotrauma get { return _toggleOpen; } set { - _toggleOpen = GameMain.Config.ChatOpen = value; + _toggleOpen = value; if (value) hideableElements.Visible = true; } } @@ -121,7 +121,7 @@ namespace Barotrauma }; arrowIcon.HoverColor = arrowIcon.PressedColor = arrowIcon.PressedColor = arrowIcon.Color; - channelText = new GUITextBox(new RectTransform(new Vector2(0.25f, 0.8f), channelSettingsContent.RectTransform), style: "DigitalFrameLight", textAlignment: Alignment.Center, font: GUI.DigitalFont) + channelText = new GUITextBox(new RectTransform(new Vector2(0.25f, 0.8f), channelSettingsContent.RectTransform), style: "DigitalFrameLight", textAlignment: Alignment.Center, font: GUIStyle.DigitalFont) { textFilterFunction = text => { @@ -173,7 +173,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), channelPickerContent.RectTransform), i.ToString(), style: "GUITextBlock") { TextColor = new Color(51, 59, 46), - SelectedTextColor = GUI.Style.Green, + SelectedTextColor = GUIStyle.Green, UserData = i, OnClicked = (btn, userdata) => { @@ -185,13 +185,13 @@ namespace Barotrauma int.TryParse(channelText.Text, out int newChannel); radio.SetChannelMemory(index, newChannel); btn.ToolTip = TextManager.GetWithVariables("radiochannelpreset", - new string[] { "[index]", "[channel]" }, - new string[] { index.ToString(), radio.GetChannelMemory(index).ToString() }); + ("[index]", index.ToString()), + ("[channel]", radio.GetChannelMemory(index).ToString())); channelMemPending = false; channelPickerContent.Children.First().CanBeFocused = true; memButton.Enabled = true; - channelPickerContent.Flash(GUI.Style.Green); - channelText.Flash(GUI.Style.Green); + channelPickerContent.Flash(GUIStyle.Green); + channelText.Flash(GUIStyle.Green); } SetChannel(radio.GetChannelMemory(index), setText: true); SoundPlayer.PlayUISound(GUISoundType.PopupMenu); @@ -224,7 +224,7 @@ namespace Barotrauma style: "ChatTextBox") { OverflowClip = true, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, MaxTextLength = ChatMessage.MaxLength }; @@ -265,7 +265,7 @@ namespace Barotrauma }; showNewMessagesButton.Visible = false; - ToggleOpen = GameMain.Config.ChatOpen; + ToggleOpen = GameSettings.CurrentConfig.ChatOpen; } public bool TypingChatMessage(GUITextBox textBox, string text) @@ -337,7 +337,7 @@ namespace Barotrauma color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f); GUITextBlock senderNameTimestamp = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, - ChatMessage.GetTimeStamp(), textColor: Color.LightGray, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null) + ChatMessage.GetTimeStamp(), textColor: Color.LightGray, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft, style: null) { CanBeFocused = true }; @@ -350,9 +350,9 @@ namespace Barotrauma { Padding = Vector4.Zero }, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, CanBeFocused = true, - ForceUpperCase = false, + ForceUpperCase = ForceUpperCase.No, UserData = message.SenderClient, OnClicked = (_, o) => { @@ -379,8 +379,8 @@ namespace Barotrauma var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.Rect.Height) }, - displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, - color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, parseRichText: true) + RichString.Rich(displayedText), textColor: message.Color, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, + color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f) { UserData = message.SenderName, CanBeFocused = false @@ -454,7 +454,7 @@ namespace Barotrauma if (!string.IsNullOrEmpty(senderName)) { var senderText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - senderName, textColor: senderColor, style: null, font: GUI.SmallFont) + senderName, textColor: senderColor, style: null, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -462,7 +462,7 @@ namespace Barotrauma senderText.RectTransform.MinSize = new Point(0, senderText.Rect.Height); } var msgPopupText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true, parseRichText: true) + RichString.Rich(displayedText), textColor: message.Color, font: GUIStyle.SmallFont, textAlignment: Alignment.BottomLeft, style: null, wrap: true) { CanBeFocused = false }; @@ -553,8 +553,8 @@ namespace Barotrauma { int index = (int)presetButton.UserData; presetButton.ToolTip = TextManager.GetWithVariables("radiochannelpreset", - new string[] { "[index]", "[channel]" }, - new string[] { index.ToString(), radio.GetChannelMemory(index).ToString() }); + ("[index]", index.ToString()), + ("[channel]", radio.GetChannelMemory(index).ToString())); } SetChannel(radio.Channel, setText: true); prevRadio = radio; @@ -563,7 +563,7 @@ namespace Barotrauma { if (channelPickerContent.FlashTimer <= 0) { - channelPickerContent.Flash(GUI.Style.Green, flashRectInflate: new Vector2(GUI.Scale * 5.0f)); + channelPickerContent.Flash(GUIStyle.Green, flashRectInflate: new Vector2(GUI.Scale * 5.0f)); } if (PlayerInput.PrimaryMouseButtonClicked() && !GUI.IsMouseOn(channelPickerContent)) { @@ -671,7 +671,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; - GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel"] }); + GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel".ToIdentifier()] }); if (setText) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 07e7ef7f8..7d3b239de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -16,7 +17,7 @@ namespace Barotrauma Toggle } - public class GUIComponentStyle + public class GUIComponentStyle : GUIPrefab { public readonly Vector4 Padding; @@ -35,31 +36,53 @@ namespace Barotrauma public readonly float ColorCrossFadeTime; public readonly TransitionMode TransitionMode; - public readonly string Font; + public readonly Identifier Font; public readonly bool ForceUpperCase; public readonly Color OutlineColor; - public readonly XElement Element; + public readonly ContentXElement Element; public readonly Dictionary> Sprites; public SpriteFallBackState FallBackState; - - public Dictionary ChildStyles; - public readonly GUIStyle Style; + public readonly GUIComponentStyle ParentStyle; + public readonly Dictionary ChildStyles; + + public static GUIComponentStyle FromHierarchy(IReadOnlyList hierarchy) + { + if (hierarchy is null || hierarchy.None()) { return null; } + GUIStyle.ComponentStyles.TryGet(hierarchy[0], out GUIComponentStyle style); + for (int i = 1; i < hierarchy.Count; i++) + { + if (style is null) { return null; } + style.ChildStyles.TryGetValue(hierarchy[i], out style); + } + return style; + } + + public static Identifier[] ToHierarchy(GUIComponentStyle style) + { + List ids = new List(); + while (style != null) + { + ids.Insert(0, style.Identifier); + style = style.ParentStyle; + } + + return ids.ToArray(); + } public readonly string Name; public int? Width { get; private set; } public int? Height { get; private set; } - public GUIComponentStyle(XElement element, GUIStyle style) + public GUIComponentStyle(ContentXElement element, UIStyleFile file, GUIComponentStyle parent = null) : base(element, file) { Name = element.Name.LocalName; - Style = style; Element = element; Sprites = new Dictionary>(); @@ -68,7 +91,8 @@ namespace Barotrauma Sprites[state] = new List(); } - ChildStyles = new Dictionary(); + ParentStyle = parent; + ChildStyles = new Dictionary(); Padding = element.GetAttributeVector4("padding", Vector4.Zero); @@ -95,10 +119,10 @@ namespace Barotrauma FallBackState = s; } - Font = element.GetAttributeString("font", ""); + Font = element.GetAttributeIdentifier("font", ""); ForceUpperCase = element.GetAttributeBool("forceuppercase", false); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -128,15 +152,15 @@ namespace Barotrauma case "size": break; default: - string styleName = subElement.Name.ToString().ToLowerInvariant(); + Identifier styleName = subElement.NameAsIdentifier(); if (ChildStyles.ContainsKey(styleName)) { DebugConsole.ThrowError("UI style \"" + element.Name.ToString() + "\" contains multiple child styles with the same name (\"" + styleName + "\")!"); - ChildStyles[styleName] = new GUIComponentStyle(subElement, style); + ChildStyles[styleName] = new GUIComponentStyle(subElement, file, this); } else { - ChildStyles.Add(styleName, new GUIComponentStyle(subElement, style)); + ChildStyles.Add(styleName, new GUIComponentStyle(subElement, file, this)); } break; } @@ -157,7 +181,7 @@ namespace Barotrauma public void GetSize(XElement element) { Point size = new Point(0, 0); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); @@ -172,5 +196,7 @@ namespace Barotrauma if (size.X > 0) { Width = size.X; } if (size.Y > 0) { Height = size.Y; } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 9e975d413..32cfeee49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -108,10 +108,10 @@ namespace Barotrauma }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "CrewManagementHeaderIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaigncrew.header"), font: GUIStyle.LargeFont) { CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; var hireablesGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -162,13 +162,13 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - TextManager.Get("campaignstore.balance"), font: GUI.Font, textAlignment: Alignment.BottomRight) + TextManager.Get("campaignstore.balance"), font: GUIStyle.Font, textAlignment: Alignment.BottomRight) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopRight) { AutoScaleVertical = true, TextScale = 1.1f, @@ -182,13 +182,13 @@ namespace Barotrauma }).RectTransform)); float height = 0.05f; - new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaigncrew.pending"), font: GUIStyle.SubHeadingFont); pendingList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), TextManager.Get("campaignmenucrew"), font: GUIStyle.SubHeadingFont); crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 8 * height), pendingAndCrewGroup.RectTransform)) { Spacing = 1 @@ -196,7 +196,7 @@ namespace Barotrauma var group = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, height), pendingAndCrewGroup.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), TextManager.Get("campaignstore.total")); - totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + totalBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), group.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextScale = 1.1f }; @@ -207,12 +207,12 @@ 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, - ForceUpperCase = true, + 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")) { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, Enabled = HasPermission, OnClicked = (b, o) => RemoveAllPendingHires() }; @@ -302,30 +302,42 @@ namespace Barotrauma if (sortingMethod == SortingMethod.AlphabeticalAsc) { list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as Tuple).Item1.Name.CompareTo((y.GUIComponent.UserData as Tuple).Item1.Name)); + ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Name.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Name)); } else if (sortingMethod == SortingMethod.JobAsc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - String.Compare((x.GUIComponent.UserData as Tuple)?.Item1.Job.Name, (y.GUIComponent.UserData as Tuple).Item1.Job.Name, StringComparison.Ordinal)); + String.Compare(((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Job.Name.Value, ((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Job.Name.Value, StringComparison.Ordinal)); } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as Tuple).Item1.Salary.CompareTo((y.GUIComponent.UserData as Tuple).Item1.Salary)); + ((InfoSkill)x.GUIComponent.UserData).CharacterInfo.Salary.CompareTo(((InfoSkill)y.GUIComponent.UserData).CharacterInfo.Salary)); if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } else if (sortingMethod == SortingMethod.SkillAsc || sortingMethod == SortingMethod.SkillDesc) { SortCharacters(list, SortingMethod.AlphabeticalAsc); list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as Tuple).Item2.CompareTo((y.GUIComponent.UserData as Tuple).Item2)); + ((InfoSkill)x.GUIComponent.UserData).SkillLevel.CompareTo(((InfoSkill)y.GUIComponent.UserData).SkillLevel)); if (sortingMethod == SortingMethod.SkillDesc) { list.Content.RectTransform.ReverseChildren(); } } } + private readonly struct InfoSkill + { + public readonly CharacterInfo CharacterInfo; + public readonly float SkillLevel; + + public InfoSkill(CharacterInfo characterInfo, float skillLevel) + { + CharacterInfo = characterInfo; + SkillLevel = skillLevel; + } + } + private void CreateCharacterFrame(CharacterInfo characterInfo, GUIListBox listBox) { Skill skill = null; @@ -338,7 +350,7 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement") { - UserData = new Tuple(characterInfo, skill?.Level ?? 0.0f) + UserData = new InfoSkill(characterInfo, skill?.Level ?? 0.0f) }; GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), frame.RectTransform, anchor: Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { @@ -363,7 +375,7 @@ namespace Barotrauma nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width); GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), nameAndJobGroup.RectTransform), - characterInfo.Job.Name, textColor: Color.White, font: GUI.SmallFont, textAlignment: Alignment.TopLeft) + characterInfo.Job.Name, textColor: Color.White, font: GUIStyle.SmallFont, textAlignment: Alignment.TopLeft) { CanBeFocused = false }; @@ -374,7 +386,7 @@ namespace Barotrauma { GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true); float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width; - GUIImage skillIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1.0f), skillGroup.RectTransform), skill.Icon) + GUIImage skillIcon = new GUIImage(new RectTransform(Vector2.One, skillGroup.RectTransform, scaleBasis: ScaleBasis.Smallest), skill.Icon, scaleToFit: true) { CanBeFocused = false }; @@ -448,7 +460,7 @@ namespace Barotrauma var confirmDialog = new GUIMessageBox( TextManager.Get("FireWarningHeader"), TextManager.GetWithVariable("FireWarningText", "[charactername]", ((CharacterInfo)obj).Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmDialog.Buttons[0].UserData = (CharacterInfo)obj; confirmDialog.Buttons[0].OnClicked = FireCharacter; confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; @@ -510,10 +522,11 @@ namespace Barotrauma string name = listBox == hireableList ? characterInfo.OriginalName : characterInfo.Name; nameBlock.Text = ToolBox.LimitString(name, nameBlock.Font, nameBlock.Rect.Width); - if (characterInfo.HasGenders) + if (characterInfo.HasSpecifierTags) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("gender")); - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.Gender.ToString())); + var menuCategoryVar = characterInfo.Prefab.MenuCategoryVar; + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get(menuCategoryVar)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get(characterInfo.ReplaceVars($"[{menuCategoryVar}]"))); } if (characterInfo.Job is Job job) { @@ -523,7 +536,7 @@ namespace Barotrauma if (characterInfo.PersonalityTrait is NPCPersonalityTrait trait) { new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoLabelGroup.RectTransform), TextManager.Get("PersonalityTrait")); - new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get("personalitytrait." + trait.Name.Replace(" ", ""))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, blockHeight), infoValueGroup.RectTransform), TextManager.Get("personalitytrait." + trait.Name.Replace(" ".ToIdentifier(), Identifier.Empty))); } infoLabelGroup.Recalculate(); infoValueGroup.Recalculate(); @@ -568,7 +581,7 @@ namespace Barotrauma return false; } - hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); + hireableList.Content.RemoveChild(hireableList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo)); hireableList.UpdateScrollBarSize(); if (!PendingHires.Contains(characterInfo)) { PendingHires.Add(characterInfo); } CreateCharacterFrame(characterInfo, pendingList); @@ -582,14 +595,14 @@ namespace Barotrauma private bool RemovePendingHire(CharacterInfo characterInfo, bool setTotalHireCost = true, bool createNetworkMessage = true) { if (PendingHires.Contains(characterInfo)) { PendingHires.Remove(characterInfo); } - pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo)); + pendingList.Content.RemoveChild(pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo)); pendingList.UpdateScrollBarSize(); // Server will reset the names to originals in multiplayer if (!GameMain.IsMultiplayer) { characterInfo?.ResetName(); } if (campaign.Map.CurrentLocation.HireManager.AvailableCharacters.Any(info => info.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName()) && - hireableList.Content.Children.None(c => c.UserData is Tuple userData && userData.Item1.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName())) + hireableList.Content.Children.None(c => c.UserData is InfoSkill userData && userData.CharacterInfo.GetIdentifierUsingOriginalName() == characterInfo.GetIdentifierUsingOriginalName())) { CreateCharacterFrame(characterInfo, hireableList); SortCharacters(hireableList, (SortingMethod)sortingDropDown.SelectedItemData); @@ -603,7 +616,7 @@ namespace Barotrauma private bool RemoveAllPendingHires(bool createNetworkMessage = true) { - pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire((c.UserData as Tuple).Item1, setTotalHireCost: false, createNetworkMessage)); + pendingList.Content.Children.ToList().ForEach(c => RemovePendingHire(((InfoSkill)c.UserData).CharacterInfo, setTotalHireCost: false, createNetworkMessage)); SetTotalHireCost(); return true; } @@ -614,7 +627,7 @@ namespace Barotrauma int total = 0; pendingList.Content.Children.ForEach(c => { - total += (c.UserData as Tuple).Item1.Salary; + total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); totalBlock.Text = FormatCurrency(total); bool enoughMoney = campaign != null ? total <= campaign.Money : true; @@ -661,7 +674,7 @@ namespace Barotrauma var dialog = new GUIMessageBox( TextManager.Get("newcrewmembers"), TextManager.GetWithVariable("crewhiredmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), - new string[] { TextManager.Get("Ok") }); + new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; } @@ -687,7 +700,7 @@ namespace Barotrauma RelativeSpacing = 0.02f, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), layoutGroup.RectTransform), TextManager.Get("campaigncrew.givenickname"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); var groupElementSize = new Vector2(1.0f, 0.25f); var nameBox = new GUITextBox(new RectTransform(groupElementSize, layoutGroup.RectTransform)) { @@ -732,7 +745,7 @@ namespace Barotrauma } else { - var crewComponent = crewList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo); + var crewComponent = crewList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo); if (crewComponent != null) { crewList.Content.RemoveChild(crewComponent); @@ -742,7 +755,7 @@ namespace Barotrauma } else { - var pendingComponent = pendingList.Content.FindChild(c => (c.UserData as Tuple).Item1 == characterInfo); + var pendingComponent = pendingList.Content.FindChild(c => ((InfoSkill)c.UserData).CharacterInfo == characterInfo); if (pendingComponent != null) { pendingList.Content.RemoveChild(pendingComponent); @@ -821,15 +834,15 @@ namespace Barotrauma characterPreviewFrame = null; } - static (GUIComponent, CharacterInfo) FindHighlightedCharacter(GUIComponent c) + static (GUIComponent GuiComponent, CharacterInfo CharacterInfo) FindHighlightedCharacter(GUIComponent c) { if (c == null) { return default; } - if (c.UserData is Tuple highlightedData) + if (c.UserData is InfoSkill highlightedData) { - return (c, highlightedData.Item1); + return (c, highlightedData.CharacterInfo); } if (c.Parent != null) { @@ -913,6 +926,6 @@ namespace Barotrauma } } - private string FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); + private LocalizedString FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 0afc20985..2f54a3d4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -1,9 +1,11 @@ -using Microsoft.Xna.Framework; +#nullable enable +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.Extensions; namespace Barotrauma { @@ -18,7 +20,7 @@ namespace Barotrauma } set { - if (value && backgroundFrame == null) { Init(); } + if (value) { InitIfNecessary(); } if (!value) { fileSystemWatcher?.Dispose(); @@ -28,26 +30,31 @@ namespace Barotrauma } } - private static GUIFrame backgroundFrame; - private static GUIFrame window; - private static GUIListBox sidebar; - private static GUIListBox fileList; - private static GUITextBox directoryBox; - private static GUITextBox filterBox; - private static GUITextBox fileBox; - private static GUIDropDown fileTypeDropdown; - private static GUIButton openButton; + private static GUIFrame? backgroundFrame; + private static GUIFrame? window; + private static GUIListBox? sidebar; + private static GUIListBox? fileList; + private static GUITextBox? directoryBox; + private static GUITextBox? filterBox; + private static GUITextBox? fileBox; + private static GUIDropDown? fileTypeDropdown; + private static GUIButton? openButton; - private static System.IO.FileSystemWatcher fileSystemWatcher; + private static System.IO.FileSystemWatcher? fileSystemWatcher; - private static string currentFileTypePattern; + private enum ItemIsDirectory + { + Yes, No + } - private static readonly string[] ignoredDrivePrefixes = new string[] + private static string? currentFileTypePattern; + + private static readonly string[] ignoredDrivePrefixes = { "/sys/", "/snap/" }; - private static string currentDirectory; + private static string currentDirectory = ""; public static string CurrentDirectory { get @@ -91,7 +98,7 @@ namespace Barotrauma } } - public static Action OnFileSelected + public static Action? OnFileSelected { get; set; @@ -99,15 +106,16 @@ namespace Barotrauma private static void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { + if (fileList is null) { return; } switch (e.ChangeType) { case System.IO.WatcherChangeTypes.Created: { - var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name) + var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name ?? string.Empty) { - UserData = (bool?)Directory.Exists(e.FullPath) + UserData = Directory.Exists(e.FullPath) ? ItemIsDirectory.Yes : ItemIsDirectory.No }; - if ((itemFrame.UserData as bool?) ?? false) + if (itemFrame.UserData is ItemIsDirectory.Yes) { itemFrame.Text += "/"; } @@ -122,11 +130,13 @@ namespace Barotrauma break; case System.IO.WatcherChangeTypes.Renamed: { - System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; - var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock; - itemFrame.UserData = (bool?)Directory.Exists(e.FullPath); - itemFrame.Text = renameArgs.Name; - if ((itemFrame.UserData as bool?) ?? false) + System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs ?? throw new InvalidCastException($"Unable to cast {nameof(System.IO.FileSystemEventArgs)} to {nameof(System.IO.RenamedEventArgs)}."); + var itemFrame = + fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock + ?? throw new Exception($"Could not find file list item with name \"{renameArgs.OldName}\""); + itemFrame.UserData = Directory.Exists(e.FullPath) ? ItemIsDirectory.Yes : ItemIsDirectory.No; + itemFrame.Text = renameArgs.Name ?? string.Empty; + if (itemFrame.UserData is ItemIsDirectory.Yes) { itemFrame.Text += "/"; } @@ -138,10 +148,10 @@ namespace Barotrauma private static int SortFiles(RectTransform r1, RectTransform r2) { - string file1 = (r1.GUIComponent as GUITextBlock)?.Text ?? ""; - string file2 = (r2.GUIComponent as GUITextBlock)?.Text ?? ""; - bool dir1 = (r1.GUIComponent.UserData as bool?) ?? false; - bool dir2 = (r2.GUIComponent.UserData as bool?) ?? false; + string file1 = (r1.GUIComponent as GUITextBlock)?.Text?.SanitizedValue ?? ""; + string file2 = (r2.GUIComponent as GUITextBlock)?.Text?.SanitizedValue ?? ""; + bool dir1 = r1.GUIComponent.UserData is ItemIsDirectory.Yes; + bool dir2 = r2.GUIComponent.UserData is ItemIsDirectory.Yes; if (dir1 && !dir2) { return -1; @@ -154,6 +164,11 @@ namespace Barotrauma return string.Compare(file1, file2); } + private static void InitIfNecessary() + { + if (backgroundFrame == null) { Init(); } + } + public static void Init() { backgroundFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) @@ -179,7 +194,7 @@ namespace Barotrauma sidebar.OnSelected = (child, userdata) => { - CurrentDirectory = (child as GUITextBlock).Text; + CurrentDirectory = (child as GUITextBlock)?.Text.SanitizedValue ?? throw new Exception("Sidebar selection is invalid"); return false; }; @@ -228,13 +243,14 @@ namespace Barotrauma { OnSelected = (child, userdata) => { - if (userdata == null) { return false; } + if (userdata is null) { return false; } + if (fileBox is null) { return false; } - var fileName = (child as GUITextBlock).Text; + var fileName = (child as GUITextBlock)!.Text.SanitizedValue; fileBox.Text = fileName; if (PlayerInput.DoubleClicked()) { - bool isDir = (userdata as bool?).Value; + bool isDir = userdata is ItemIsDirectory.Yes; if (isDir) { CurrentDirectory += fileName; @@ -263,7 +279,7 @@ namespace Barotrauma { OnSelected = (child, userdata) => { - currentFileTypePattern = (child as GUITextBlock).UserData as string; + currentFileTypePattern = (child as GUITextBlock)!.UserData as string; RefreshFileList(); return true; @@ -307,30 +323,31 @@ namespace Barotrauma public static void ClearFileTypeFilters() { - if (backgroundFrame == null) { Init(); } - fileTypeDropdown.ClearChildren(); + InitIfNecessary(); + fileTypeDropdown!.ClearChildren(); } public static void AddFileTypeFilter(string name, string pattern) { - if (backgroundFrame == null) { Init(); } - fileTypeDropdown.AddItem(name + " (" + pattern + ")", pattern); + InitIfNecessary(); + fileTypeDropdown!.AddItem(name + " (" + pattern + ")", pattern); } public static void SelectFileTypeFilter(string pattern) { - if (backgroundFrame == null) { Init(); } - fileTypeDropdown.SelectItem(pattern); + InitIfNecessary(); + fileTypeDropdown!.SelectItem(pattern); } public static void RefreshFileList() { - fileList.Content.ClearChildren(); + InitIfNecessary(); + fileList!.Content.ClearChildren(); fileList.BarScroll = 0.0f; try { - var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox.Text + "*"); + var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox!.Text + "*"); foreach (var directory in directories) { string txt = directory; @@ -338,7 +355,7 @@ namespace Barotrauma if (!txt.EndsWith("/")) { txt += "/"; } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { - UserData = (bool?)true + UserData = ItemIsDirectory.Yes }; var folderIcon = new GUIImage(new RectTransform(new Point((int)(itemFrame.Rect.Height * 0.8f)), itemFrame.RectTransform, Anchor.CenterLeft) { @@ -347,18 +364,18 @@ namespace Barotrauma itemFrame.Padding = new Vector4(folderIcon.Rect.Width * 1.5f, itemFrame.Padding.Y, itemFrame.Padding.Z, itemFrame.Padding.W); } - IEnumerable files = null; - if (currentFileTypePattern == null) + IEnumerable files = Enumerable.Empty(); + if (currentFileTypePattern.IsNullOrEmpty()) { files = Directory.GetFiles(currentDirectory); } else { - foreach (string pattern in currentFileTypePattern.Split(',')) + foreach (string pattern in currentFileTypePattern!.Split(',')) { string patternTrimmed = pattern.Trim(); patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed; - if (files == null) + if (files.None()) { files = Directory.EnumerateFiles(currentDirectory, patternTrimmed); } @@ -375,7 +392,7 @@ namespace Barotrauma if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); } var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt) { - UserData = (bool?)false + UserData = ItemIsDirectory.No }; } } @@ -387,8 +404,8 @@ namespace Barotrauma }; } - directoryBox.Text = currentDirectory; - fileBox.Text = ""; + directoryBox!.Text = currentDirectory; + fileBox!.Text = ""; fileList.Deselect(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 116f2b857..c83bca84c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -14,6 +14,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; +using System.Collections.Immutable; namespace Barotrauma { @@ -36,13 +37,13 @@ namespace Barotrauma public enum CursorState { - Default, // Cursor - Hand, // Hand with a finger - Move, // arrows pointing to all directions - IBeam, // Text - Dragging,// Closed hand - Waiting, // Hourglass - WaitingBackground // Cursor + Hourglass + Default = 0, // Cursor + Hand = 1, // Hand with a finger + Move = 2, // arrows pointing to all directions + IBeam = 3, // Text + Dragging = 4,// Closed hand + Waiting = 5, // Hourglass + WaitingBackground = 6, // Cursor + Hourglass } public static class GUI @@ -78,20 +79,19 @@ namespace Barotrauma FilterMode = TextureFilterMode.Default, }; - - public static readonly string[] vectorComponentLabels = { "X", "Y", "Z", "W" }; - public static readonly string[] rectComponentLabels = { "X", "Y", "W", "H" }; - public static readonly string[] colorComponentLabels = { "R", "G", "B", "A" }; + public static readonly string[] VectorComponentLabels = { "X", "Y", "Z", "W" }; + public static readonly string[] RectComponentLabels = { "X", "Y", "W", "H" }; + public static readonly string[] ColorComponentLabels = { "R", "G", "B", "A" }; private static readonly object mutex = new object(); - public static Vector2 ReferenceResolution => new Vector2(1920f, 1080f); - public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; - public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.HUDScale; - public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.HUDScale; + public static readonly Vector2 ReferenceResolution = new Vector2(1920f, 1080f); + public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.CurrentConfig.Graphics.HUDScale; + public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.CurrentConfig.Graphics.HUDScale; + public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.CurrentConfig.Graphics.HUDScale; public static int IntScale(float f) => (int)(f * Scale); public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale); - public static int IntScaleCeiling(float f) => (int) Math.Ceiling(f * Scale); + public static int IntScaleCeiling(float f) => (int)Math.Ceiling(f * Scale); public static float HorizontalAspectRatio => GameMain.GraphicsWidth / (float)GameMain.GraphicsHeight; public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); @@ -102,7 +102,6 @@ namespace Barotrauma { get { - // Ultrawide if (IsUltrawide) { return (int)(GameMain.GraphicsHeight * ReferenceResolution.X / ReferenceResolution.Y); @@ -127,23 +126,21 @@ namespace Barotrauma } } - public static GUIStyle Style; - - private static Texture2D t; - public static Texture2D WhiteTexture => t; - private static Sprite[] MouseCursorSprites => Style.CursorSprite; + private static Texture2D solidWhiteTexture; + public static Texture2D WhiteTexture => solidWhiteTexture; + private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite; private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; private static int debugDrawMetadataOffset; private static readonly string[] ignoredMetadataInfo = { string.Empty, string.Empty, string.Empty, string.Empty }; - public static GraphicsDevice GraphicsDevice { get; private set; } + public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice; private static List messages = new List(); - private static readonly Dictionary soundIdentifiers = new Dictionary(); - private static bool pauseMenuOpen, settingsMenuOpen; + public static GUIFrame PauseMenu { get; private set; } - private static Sprite arrow; + public static GUIFrame SettingsMenuContainer { get; private set; } + public static Sprite Arrow => GUIStyle.Arrow.Value.Sprite; public static bool HideCursor; @@ -154,61 +151,31 @@ namespace Barotrauma /// public static bool ScreenChanged; - public static ScalableFont Font => Style?.Font; - - // Usable in CJK as a regular font - public static ScalableFont GlobalFont => Style?.GlobalFont; - public static ScalableFont UnscaledSmallFont => Style?.UnscaledSmallFont; - public static ScalableFont SmallFont => Style?.SmallFont; - public static ScalableFont LargeFont => Style?.LargeFont; - public static ScalableFont SubHeadingFont => Style?.SubHeadingFont; - public static ScalableFont DigitalFont => Style?.DigitalFont; - public static ScalableFont HotkeyFont => Style?.HotkeyFont; - public static ScalableFont MonospacedFont => Style?.MonospacedFont; - - public static ScalableFont CJKFont { get; private set; } - - public static UISprite UIGlow => Style.UIGlow; - public static UISprite UIGlowCircular => Style.UIGlowCircular; - - public static Sprite SubmarineIcon - { - get; - private set; - } - - public static Sprite BrokenIcon - { - get; - private set; - } - - public static Sprite SpeechBubbleIcon - { - get; - private set; - } - - public static Sprite Arrow - { - get { return arrow; } - } - + private static bool settingsMenuOpen; public static bool SettingsMenuOpen { get { return settingsMenuOpen; } set { - if (value == settingsMenuOpen) { return; } - GameMain.Config.ResetSettingsFrame(); + if (value == SettingsMenuOpen) { return; } + + if (value) + { + SettingsMenuContainer = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, SettingsMenuContainer.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + var settingsMenuInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), SettingsMenuContainer.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(640, 480) }); + SettingsMenu.Create(settingsMenuInner.RectTransform); + } + else + { + SettingsMenu.Instance?.Close(); + } settingsMenuOpen = value; } } - public static bool PauseMenuOpen - { - get { return pauseMenuOpen; } - } + public static bool PauseMenuOpen { get; private set; } public static bool InputBlockingMenuOpen { @@ -251,66 +218,14 @@ namespace Barotrauma FadingOut } - public static void Init(GameWindow window, IEnumerable selectedContentPackages, GraphicsDevice graphicsDevice) + public static void Init() { - GraphicsDevice = graphicsDevice; - - var files = ContentPackage.GetFilesOfType(selectedContentPackages, ContentType.UIStyle); - XElement selectedStyle = null; - foreach (var file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - if (selectedStyle != null) - { - DebugConsole.NewMessage($"Overriding the ui styles with '{file.Path}'", Color.Yellow); - } - } - else if (selectedStyle != null) - { - DebugConsole.ThrowError("Another ui style already loaded! Use tags to override it."); - break; - } - selectedStyle = mainElement; - } - if (selectedStyle == null) - { - DebugConsole.ThrowError("No UI styles defined in the selected content package!"); - } - else - { - Style = new GUIStyle(selectedStyle, graphicsDevice); - } - - if (CJKFont == null) - { - CJKFont = new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", - Font.Size, graphicsDevice, dynamicLoading: true, isCJK: true); - } - } - - public static void LoadContent() - { - foreach (GUISoundType soundType in Enum.GetValues(typeof(GUISoundType))) - { - soundIdentifiers.Add(soundType, soundType.ToString().ToLowerInvariant()); - } - // create 1x1 texture for line drawing CrossThread.RequestExecutionOnMainThread(() => { - t = new Texture2D(GraphicsDevice, 1, 1); - t.SetData(new Color[] { Color.White });// fill the texture with white + solidWhiteTexture = new Texture2D(GraphicsDevice, 1, 1); + solidWhiteTexture.SetData(new Color[] { Color.White });// fill the texture with white }); - - SubmarineIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(452, 385, 182, 81), new Vector2(0.5f, 0.5f)); - arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(393, 393, 49, 45), new Vector2(0.5f, 0.5f)); - SpeechBubbleIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(385, 449, 66, 60), new Vector2(0.5f, 0.5f)); - BrokenIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(898, 386, 123, 123), new Vector2(0.5f, 0.5f)); } /// @@ -341,41 +256,41 @@ namespace Barotrauma } #if UNSTABLE - string line1 = "Barotrauma Unstable v" + GameMain.Version; - string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; + string line1 = "Barotrauma Unstable v" + GameMain.Version; + string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; - Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(LargeFont.MeasureString(line1).X, Font.MeasureString(line2).X) * 1.2f), 100); - float alpha = 1.0f; + Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X) * 1.2f), 100); + float alpha = 1.0f; - int yOffset = 0; + int yOffset = 0; - if (Screen.Selected == GameMain.GameScreen) - { - yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f); - watermarkRect.Y += yOffset; - } + if (Screen.Selected == GameMain.GameScreen) + { + yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f); + watermarkRect.Y += yOffset; + } - if (Screen.Selected == GameMain.GameScreen || Screen.Selected == GameMain.SubEditorScreen) - { - alpha = 0.2f; - } + if (Screen.Selected == GameMain.GameScreen || Screen.Selected == GameMain.SubEditorScreen) + { + alpha = 0.2f; + } - Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( - spriteBatch, watermarkRect, Color.Black * 0.8f * alpha); - LargeFont.DrawString(spriteBatch, line1, - new Vector2(10, GameMain.GraphicsHeight - 30 - LargeFont.MeasureString(line1).Y + yOffset), Color.White * 0.6f * alpha); - Font.DrawString(spriteBatch, line2, + GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( + spriteBatch, watermarkRect, Color.Black * 0.8f * alpha); + GUIStyle.LargeFont.DrawString(spriteBatch, line1, + new Vector2(10, GameMain.GraphicsHeight - 30 - GUIStyle.LargeFont.MeasureString(line1).Y + yOffset), Color.White * 0.6f * alpha); + GUIStyle.Font.DrawString(spriteBatch, line2, new Vector2(10, GameMain.GraphicsHeight - 30 + yOffset), Color.White * 0.6f * alpha); - if (Screen.Selected != GameMain.GameScreen) - { - var buttonRect = - new Rectangle(20 + (int)Math.Max(LargeFont.MeasureString(line1).X, Font.MeasureString(line2).X), GameMain.GraphicsHeight - (int)(45 * Scale) + yOffset, (int)(150 * Scale), (int)(40 * Scale)); - if (DrawButton(spriteBatch, buttonRect, "Report Bug", Style.GetComponentStyle("GUIBugButton").Color * 0.8f)) + if (Screen.Selected != GameMain.GameScreen) { - GameMain.Instance.ShowBugReporter(); + var buttonRect = + new Rectangle(20 + (int)Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X), GameMain.GraphicsHeight - (int)(45 * Scale) + yOffset, (int)(150 * Scale), (int)(40 * Scale)); + if (DrawButton(spriteBatch, buttonRect, "Report Bug", GUIStyle.GetComponentStyle("GUIBugButton").Color * 0.8f)) + { + GameMain.Instance.ShowBugReporter(); + } } - } #endif if (DisableHUD) @@ -388,12 +303,12 @@ namespace Barotrauma { DrawString(spriteBatch, new Vector2(10, 10), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) { DrawString(spriteBatch, new Vector2(10, 25), $"Physics: {GameMain.CurrentUpdateRate}", - (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, SmallFont); + (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } @@ -403,15 +318,15 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(300, y), "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" + " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms", - Style.Green, Color.Black * 0.8f, font: SmallFont); - y += 15; - GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Style.Green); + GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont); + y += 15; + GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: GUIStyle.Green); y += 50; DrawString(spriteBatch, new Vector2(300, 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: SmallFont); + Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont); y += 15; GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Color.LightBlue); y += 50; @@ -420,19 +335,25 @@ namespace Barotrauma float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); DrawString(spriteBatch, new Vector2(300, y), key + ": " + elapsedMillisecs.ToString("0.00"), - Color.Lerp(Color.LightGreen, GUI.Style.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, SmallFont); + Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; } + if (Powered.Grids != null) + { + DrawString(spriteBatch, new Vector2(300, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += 15; + } + if (Settings.EnableDiagnostics) { - DrawString(spriteBatch, new Vector2(320, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 15), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 30), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 45), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 60), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(320, y + 75), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds, Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(320, 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(320, y + 15), "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(320, y + 30), "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(320, y + 45), "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(320, y + 60), "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(320, y + 75), "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); } } @@ -440,56 +361,56 @@ namespace Barotrauma { DrawString(spriteBatch, new Vector2(10, 25), "Physics: " + GameMain.World.UpdateTime, - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); DrawString(spriteBatch, new Vector2(10, 40), $"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, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (Screen.Selected.Cam != null) { DrawString(spriteBatch, new Vector2(10, 55), "Camera pos: " + Screen.Selected.Cam.Position.ToPoint() + ", zoom: " + Screen.Selected.Cam.Zoom, - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } if (Submarine.MainSub != null) { DrawString(spriteBatch, new Vector2(10, 70), "Sub pos: " + Submarine.MainSub.Position.ToPoint(), - Color.White, Color.Black * 0.5f, 0, SmallFont); + Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } DrawString(spriteBatch, new Vector2(10, 90), "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, - Color.Lerp(GUI.Style.Green, GUI.Style.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, SmallFont); + 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); } - DrawString(spriteBatch, new Vector2(10, 115), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(10, 115), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (debugDrawSounds) { int y = 0; DrawString(spriteBatch, new Vector2(500, y), - "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; DrawString(spriteBatch, new Vector2(500, y), - "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, SmallFont); + "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; DrawString(spriteBatch, new Vector2(500, y), - "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, SmallFont); + "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; DrawString(spriteBatch, new Vector2(500, y), - "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; for (int i = 0; i < SoundManager.SOURCE_COUNT; i++) @@ -538,27 +459,27 @@ namespace Barotrauma } } - DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(500, y), soundStr, clr, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; } } else { DrawString(spriteBatch, new Vector2(500, 0), - "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } if (debugDrawEvents) { DrawString(spriteBatch, new Vector2(10, 300), - "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); GameMain.GameSession?.EventManager?.DebugDrawHUD(spriteBatch, 315); } else { DrawString(spriteBatch, new Vector2(10, 300), - "Ctrl+E to show EventManager debug info", Color.White, Color.Black * 0.5f, 0, SmallFont); + "Ctrl+E to show EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) @@ -570,17 +491,17 @@ namespace Barotrauma $"Ctrl+2 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[1]) ? "hide" : "show")} faction reputations, \n" + $"Ctrl+3 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[2]) ? "hide" : "show")} upgrade levels, \n" + $"Ctrl+4 to {(string.IsNullOrWhiteSpace(ignoredMetadataInfo[3]) ? "hide" : "show")} upgrade prices"; - var (x, y) = SmallFont.MeasureString(text); + var (x, y) = GUIStyle.SmallFont.MeasureString(text); Vector2 pos = new Vector2(GameMain.GraphicsWidth - (x + 10), 300); - DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); pos.Y += y + 8; campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, debugDrawMetadataOffset, ignoredMetadataInfo); } else { const string text = "Ctrl+M to show campaign metadata debug info"; - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (SmallFont.MeasureString(text).X + 10), 300), - text, Color.White, Color.Black * 0.5f, 0, SmallFont); + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (GUIStyle.SmallFont.MeasureString(text).X + 10), 300), + text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } } @@ -616,9 +537,9 @@ namespace Barotrauma foreach (string str in strings) { - Vector2 stringSize = SmallFont.MeasureString(str); + Vector2 stringSize = GUIStyle.SmallFont.MeasureString(str); - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, SmallFont); + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, GUIStyle.SmallFont); yPos += (int)stringSize.Y + padding / 2; } } @@ -639,7 +560,7 @@ namespace Barotrauma DrawMessages(spriteBatch, cam); - if (MouseOn != null && !string.IsNullOrWhiteSpace(MouseOn.ToolTip)) + if (MouseOn != null && !MouseOn.ToolTip.IsNullOrWhiteSpace()) { MouseOn.DrawToolTip(spriteBatch); } @@ -651,7 +572,7 @@ namespace Barotrauma { case ItemPrefab itemPrefab: { - var sprite = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + var sprite = itemPrefab.InventoryIcon ?? itemPrefab.Sprite; sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); break; } @@ -660,12 +581,13 @@ namespace Barotrauma var (x, y) = PlayerInput.MousePosition; foreach (var pair in iPrefab.DisplayEntities) { - Rectangle dRect = pair.Second; + Rectangle dRect = pair.Item2; dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x), y: (int)(dRect.Y * iPrefab.Scale - y), width: (int)(dRect.Width * iPrefab.Scale), height: (int)(dRect.Height * iPrefab.Scale)); - pair.First.DrawPlacing(spriteBatch, dRect, pair.First.Scale * iPrefab.Scale); + MapEntityPrefab prefab = MapEntityPrefab.Find("", pair.Item1); + prefab.DrawPlacing(spriteBatch, dRect, prefab.Scale * iPrefab.Scale); } break; } @@ -678,14 +600,14 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); - - if (GameMain.GameSession?.CrewManager is { DraggedOrder: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true }) + + if (GameMain.GameSession?.CrewManager is { DraggedOrderPrefab: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true }) { float spriteSize = Math.Max(orderSprite.size.X, orderSprite.size.Y); orderSprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, color, orderSprite.size / 2f, scale: 32f / spriteSize * Scale); } - var sprite = MouseCursorSprites[(int)MouseCursor] ?? MouseCursorSprites[(int)CursorState.Default]; + var sprite = MouseCursorSprites[MouseCursor] ?? MouseCursorSprites[CursorState.Default]; sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f); spriteBatch.End(); @@ -891,13 +813,13 @@ namespace Barotrauma GUIMessageBox.AddActiveToGUIUpdateList(); GUIContextMenu.AddActiveToGUIUpdateList(); - if (pauseMenuOpen) + if (PauseMenuOpen) { PauseMenu.AddToGUIUpdateList(); } - if (settingsMenuOpen) + if (SettingsMenuOpen) { - GameMain.Config.SettingsFrame.AddToGUIUpdateList(); + SettingsMenuContainer.AddToGUIUpdateList(); } //the "are you sure you want to quit" prompts are drawn on top of everything else @@ -1303,7 +1225,7 @@ namespace Barotrauma private static void UpdateSavingIndicator(float deltaTime) { - if (Style.SavingIndicator == null) { return; } + if (GUIStyle.SavingIndicator == null) { return; } lock (mutex) { if (timeUntilSavingIndicatorDisabled.HasValue) @@ -1349,7 +1271,7 @@ namespace Barotrauma } if (IsSavingIndicatorVisible) { - savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (Style.SavingIndicator.FrameCount + 1); + savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (GUIStyle.SavingIndicator.FrameCount + 1); } } } @@ -1439,7 +1361,7 @@ namespace Barotrauma public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1) { - DrawLine(sb, t, start, end, clr, depth, (int)width); + DrawLine(sb, solidWhiteTexture, start, end, clr, depth, (int)width); } public static void DrawLine(SpriteBatch sb, Sprite sprite, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1) @@ -1482,21 +1404,26 @@ namespace Barotrauma depth); } - public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null) + public static void DrawString(SpriteBatch sb, Vector2 pos, LocalizedString text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null) { - if (font == null) font = Font; + DrawString(sb, pos, text.Value, color, backgroundColor, backgroundPadding, font); + } + + public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit) + { + if (font == null) font = GUIStyle.Font; if (backgroundColor != null) { Vector2 textSize = font.MeasureString(text); DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true); } - font.DrawString(sb, text, pos, color); + font.DrawString(sb, text, pos, color, forceUpperCase: forceUpperCase); } - public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, List richTextData, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null, float depth = 0.0f) + public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, in ImmutableArray? richTextData, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, float depth = 0.0f) { - if (font == null) font = Font; + if (font == null) font = GUIStyle.Font; if (backgroundColor != null) { Vector2 textSize = font.MeasureString(text); @@ -1506,6 +1433,63 @@ namespace Barotrauma font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData); } + private const int DonutSegments = 30; + private static readonly ImmutableArray canonicalCircle + = Enumerable.Range(0, DonutSegments) + .Select(i => i * (2.0f * MathF.PI / DonutSegments)) + .Select(angle => new Vector2(MathF.Cos(angle), MathF.Sin(angle))) + .ToImmutableArray(); + private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4]; + + public static void DrawDonutSection( + SpriteBatch sb, Vector2 center, Range radii, float sectionRad, Color clr, float depth = 0.0f) + { + float getRadius(int vertexIndex) + => (vertexIndex % 4) switch + { + 0 => radii.End, + 1 => radii.End, + 2 => radii.Start, + 3 => radii.Start, + _ => throw new InvalidOperationException() + }; + int getDirectionIndex(int vertexIndex) + => (vertexIndex % 4) switch + { + 0 => (vertexIndex / 4) + 0, + 1 => (vertexIndex / 4) + 1, + 2 => (vertexIndex / 4) + 0, + 3 => (vertexIndex / 4) + 1, + _ => throw new InvalidOperationException() + }; + + float sectionProportion = sectionRad / (MathF.PI * 2.0f); + int maxDirectionIndex = Math.Min(DonutSegments, (int)MathF.Ceiling(sectionProportion * DonutSegments)); + + Vector2 getDirection(int vertexIndex) + { + int directionIndex = getDirectionIndex(vertexIndex); + Vector2 dir = canonicalCircle[directionIndex % DonutSegments]; + if (maxDirectionIndex > 0 && directionIndex >= maxDirectionIndex) + { + float maxSectionProportion = (float)maxDirectionIndex / DonutSegments; + dir = Vector2.Lerp( + canonicalCircle[maxDirectionIndex - 1], + canonicalCircle[maxDirectionIndex % DonutSegments], + 1.0f - (maxSectionProportion - sectionProportion) * DonutSegments); + } + + return new Vector2(dir.Y, -dir.X); + } + + for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++) + { + donutVerts[vertexIndex].Color = clr; + donutVerts[vertexIndex].Position = new Vector3(center + getDirection(vertexIndex) * getRadius(vertexIndex), 0.0f); + } + sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex); + } + public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1) { if (size.X < 0) @@ -1525,15 +1509,15 @@ namespace Barotrauma { if (isFilled) { - sb.Draw(t, rect, null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, rect, null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth); } else { Rectangle srcRect = new Rectangle(0, 0, 1, 1); - sb.Draw(t, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth); - sb.Draw(t, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); - sb.Draw(t, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); - sb.Draw(t, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth); } } @@ -1555,7 +1539,7 @@ namespace Barotrauma size.Y = -size.Y; } - sb.Draw(t, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth); + sb.Draw(solidWhiteTexture, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth); } public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, float thickness = 1) @@ -1621,14 +1605,14 @@ namespace Barotrauma Vector2 origin; try { - origin = Font.MeasureString(text) / 2; + origin = GUIStyle.Font.MeasureString(text) / 2; } catch { origin = Vector2.Zero; } - Font.DrawString(sb, text, new Vector2(rect.Center.X, rect.Center.Y), Color.White, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(sb, text, new Vector2(rect.Center.X, rect.Center.Y), Color.White, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f); return clicked; } @@ -1698,7 +1682,7 @@ namespace Barotrauma public static void DrawSineWithDots(SpriteBatch spriteBatch, Vector2 from, Vector2 dir, float amplitude, float length, float scale, int pointCount, Color color, int dotSize = 2) { Vector2 up = dir.Right(); - //DrawLine(spriteBatch, from, from + dir, GUI.Style.Red); + //DrawLine(spriteBatch, from, from + dir, GUIStyle.Red); //DrawLine(spriteBatch, from, from + up * dir.Length(), Color.Blue); for (int i = 0; i < pointCount; i++) { @@ -1715,8 +1699,8 @@ namespace Barotrauma private static void DrawSavingIndicator(SpriteBatch spriteBatch) { - if (!IsSavingIndicatorVisible || Style.SavingIndicator == null) { return; } - var sheet = Style.SavingIndicator; + if (!IsSavingIndicatorVisible || GUIStyle.SavingIndicator == null) { return; } + var sheet = GUIStyle.SavingIndicator; Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2(); sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale)); } @@ -1907,9 +1891,9 @@ namespace Barotrauma return CreateElements(count, parent, constructor, null, absoluteSize, anchor, pivot, null, null, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal); } - public static GUIComponent CreateEnumField(Enum value, int elementHeight, string name, RectTransform parent, string toolTip = null, ScalableFont font = null) + public static GUIComponent CreateEnumField(Enum value, int elementHeight, LocalizedString name, RectTransform parent, string toolTip = null, GUIFont font = null) { - font = font ?? SmallFont; + font = font ?? GUIStyle.SmallFont; var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementHeight), parent), color: Color.Transparent); new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform), name, font: font) { @@ -1928,10 +1912,10 @@ namespace Barotrauma return frame; } - public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, string name, RectTransform parent, string toolTip = null, ScalableFont font = null) + public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null) { var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent); - font = font ?? SmallFont; + font = font ?? GUIStyle.SmallFont; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), frame.RectTransform), name, font: font) { ToolTip = toolTip @@ -1944,7 +1928,7 @@ namespace Barotrauma for (int i = 3; i >= 0; i--) { 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); + 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) { @@ -1973,10 +1957,10 @@ namespace Barotrauma return frame; } - public static GUIComponent CreatePointField(Point value, int elementHeight, string displayName, RectTransform parent, string toolTip = null) + public static GUIComponent CreatePointField(Point value, int elementHeight, LocalizedString displayName, RectTransform parent, LocalizedString toolTip = null) { var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent); - new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: SmallFont) + new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -1988,11 +1972,11 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { 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: SmallFont, textAlignment: Alignment.CenterLeft); + 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) { - Font = SmallFont + Font = GUIStyle.SmallFont }; if (i == 0) @@ -2003,9 +1987,9 @@ namespace Barotrauma return frame; } - public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, string name, RectTransform parent, string toolTip = null, ScalableFont font = null, int decimalsToDisplay = 1) + public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null, int decimalsToDisplay = 1) { - font = font ?? SmallFont; + font = font ?? GUIStyle.SmallFont; var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent); new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), name, font: font) { @@ -2019,7 +2003,7 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { 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); + 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 }; switch (i) { @@ -2035,7 +2019,7 @@ namespace Barotrauma return frame; } - public static void NotifyPrompt(string header, string body) + public static void NotifyPrompt(LocalizedString header, LocalizedString body) { GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); msgBox.Buttons[0].OnClicked = delegate @@ -2045,9 +2029,9 @@ namespace Barotrauma }; } - public static GUIMessageBox AskForConfirmation(string header, string body, Action onConfirm, Action onDeny = null) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null) { - string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); // Cancel button @@ -2068,9 +2052,9 @@ namespace Barotrauma return msgBox; } - public static GUIMessageBox PromptTextInput(string header, string body, Action onConfirm) + public static GUIMessageBox PromptTextInput(LocalizedString header, string body, Action onConfirm) { - string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body) { @@ -2198,16 +2182,18 @@ namespace Barotrauma /// The elements will not be moved outside this area. If the parameter is not given, the elements are kept inside the window. public static void PreventElementOverlap(IList elements, IList disallowedAreas = null, Rectangle? clampArea = null) { + List sortedElements = elements.OrderByDescending(e => e.Rect.Width + e.Rect.Height).ToList(); + Rectangle area = clampArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - for (int i = 0; i < elements.Count; i++) + for (int i = 0; i < sortedElements.Count; i++) { Point moveAmount = Point.Zero; - Rectangle rect1 = elements[i].Rect; + Rectangle rect1 = sortedElements[i].Rect; moveAmount.X += Math.Max(area.X - rect1.X, 0); moveAmount.X -= Math.Max(rect1.Right - area.Right, 0); moveAmount.Y += Math.Max(area.Y - rect1.Y, 0); moveAmount.Y -= Math.Max(rect1.Bottom - area.Bottom, 0); - elements[i].RectTransform.ScreenSpaceOffset += moveAmount; + sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount; } bool intersections = true; @@ -2215,18 +2201,18 @@ namespace Barotrauma while (intersections && iterations < 100) { intersections = false; - for (int i = 0; i < elements.Count; i++) + for (int i = 0; i < sortedElements.Count; i++) { - Rectangle rect1 = elements[i].Rect; - for (int j = i + 1; j < elements.Count; j++) + Rectangle rect1 = sortedElements[i].Rect; + for (int j = i + 1; j < sortedElements.Count; j++) { - Rectangle rect2 = elements[j].Rect; + Rectangle rect2 = sortedElements[j].Rect; if (!rect1.Intersects(rect2)) { continue; } intersections = true; Point centerDiff = rect1.Center - rect2.Center; //move the interfaces away from each other, in a random direction if they're at the same position - Vector2 moveAmount = centerDiff == Point.Zero ? Rand.Vector(1.0f) : Vector2.Normalize(centerDiff.ToVector2()); + Vector2 moveAmount = centerDiff == Point.Zero ? Vector2.UnitX + Rand.Vector(0.1f) : Vector2.Normalize(centerDiff.ToVector2()); //if the horizontal move amount is much larger than vertical, only move horizontally //(= attempt to place the elements side-by-side if they're more apart horizontally than vertically) @@ -2246,8 +2232,8 @@ namespace Barotrauma //move by 10 units in the desired direction and repeat until nothing overlaps //(or after 100 iterations, in which case we'll just give up and let them overlap) - elements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint(); - elements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint(); + sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint(); + sortedElements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint(); } if (disallowedAreas == null) { continue; } @@ -2265,7 +2251,7 @@ namespace Barotrauma //move by 10 units in the desired direction and repeat until nothing overlaps //(or after 100 iterations, in which case we'll just give up and let them overlap) - elements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint(); + sortedElements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint(); } } iterations++; @@ -2301,11 +2287,11 @@ namespace Barotrauma if (Screen.Selected == GameMain.MainMenuScreen) { return; } if (PreventPauseMenuToggle) { return; } - settingsMenuOpen = false; + SettingsMenuOpen = false; TogglePauseMenu(null, null); - if (pauseMenuOpen) + if (PauseMenuOpen) { Inventory.DraggingItems.Clear(); Inventory.DraggingInventory = null; @@ -2330,7 +2316,7 @@ namespace Barotrauma }; CreateButton("PauseMenuResume", buttonContainer, null); - CreateButton("PauseMenuSettings", buttonContainer, () => { settingsMenuOpen = !settingsMenuOpen; }); + CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true); bool IsOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedOutpost; if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null) @@ -2407,7 +2393,7 @@ namespace Barotrauma { if (string.IsNullOrEmpty(verificationTextTag)) { - pauseMenuOpen = false; + PauseMenuOpen = false; action?.Invoke(); } else @@ -2422,13 +2408,13 @@ namespace Barotrauma void CreateVerificationPrompt(string textTag, Action confirmAction) { var msgBox = new GUIMessageBox("", TextManager.Get(textTag), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; msgBox.Buttons[0].OnClicked = (_, __) => { - pauseMenuOpen = false; + PauseMenuOpen = false; confirmAction?.Invoke(); return true; }; @@ -2439,8 +2425,8 @@ namespace Barotrauma private static bool TogglePauseMenu(GUIButton button, object obj) { - pauseMenuOpen = !pauseMenuOpen; - if (!pauseMenuOpen && PauseMenu != null) + PauseMenuOpen = !PauseMenuOpen; + if (!PauseMenuOpen && PauseMenu != null) { PauseMenu.RectTransform.Parent = null; PauseMenu = null; @@ -2451,10 +2437,21 @@ namespace Barotrauma /// /// Displays a message at the center of the screen, automatically preventing overlapping with other centered messages. TODO: Allow to show messages at the middle of the screen (instead of the top center). /// - public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, ScalableFont font = null) + /// + public static void AddMessage(LocalizedString message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) + { + AddMessage(message.Value, color, lifeTime, playSound, font); + } + + public static void AddMessage(LocalizedString message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1) + { + AddMessage(message.Value, color, pos, velocity, lifeTime, playSound, soundType, subId); + } + + public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null) { if (messages.Any(msg => msg.Text == message)) { return; } - messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? LargeFont)); + messages.Add(new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont)); if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); } } @@ -2462,7 +2459,7 @@ namespace Barotrauma { Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId); - var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, Font, sub: sub); + var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub); if (playSound) { SoundPlayer.PlayUISound(soundType); } bool overlapFound = true; int tries = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 52b34ece5..1846bc809 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -116,32 +116,32 @@ namespace Barotrauma get { return Frame.FlashTimer; } } - public override ScalableFont Font + public override GUIFont Font { get { - return (textBlock == null) ? GUI.Font : textBlock.Font; + return (textBlock == null) ? GUIStyle.Font : textBlock.Font; } set { base.Font = value; - if (textBlock != null) textBlock.Font = value; + if (textBlock != null) { textBlock.Font = value; } } } - public string Text + public LocalizedString Text { get { return textBlock.Text; } set { textBlock.Text = value; } } - public bool ForceUpperCase + public ForceUpperCase ForceUpperCase { get { return textBlock.ForceUpperCase; } set { textBlock.ForceUpperCase = value; } } - public override string ToolTip + public override RichString ToolTip { get { @@ -160,39 +160,36 @@ namespace Barotrauma private bool flashed; public GUISoundType ClickSound { get; set; } = GUISoundType.Click; - - public GUIButton(RectTransform rectT, string text = "", Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) + + public GUIButton(RectTransform rectT, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : this(rectT, new RawLString(""), textAlignment, style, color) { } + + public GUIButton(RectTransform rectT, LocalizedString text, Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) { CanBeFocused = true; HoverCursor = CursorState.Hand; frame = new GUIFrame(new RectTransform(Vector2.One, rectT), style) { CanBeFocused = false }; - if (style != null) { GUI.Style.Apply(frame, style == "" ? "GUIButton" : style); } + if (style != null) { GUIStyle.Apply(frame, style == "" ? "GUIButton" : style); } if (color.HasValue) { this.color = frame.Color = color.Value; } + + var selfStyle = Style; textBlock = new GUITextBlock(new RectTransform(Vector2.One, rectT, Anchor.Center), text, textAlignment: textAlignment, style: null) { - TextColor = this.style == null ? Color.Black : this.style.TextColor, - HoverTextColor = this.style == null ? Color.Black : this.style.HoverTextColor, - SelectedTextColor = this.style == null ? Color.Black : this.style.SelectedTextColor, + TextColor = selfStyle?.TextColor ?? Color.Black, + HoverTextColor = selfStyle?.HoverTextColor ?? Color.Black, + SelectedTextColor = selfStyle?.SelectedTextColor ?? Color.Black, CanBeFocused = false }; - if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) + if (rectT.Rect.Height == 0 && !text.IsNullOrEmpty()) { RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(textBlock.Text).Y)); RectTransform.MinSize = textBlock.RectTransform.MinSize = new Point(0, System.Math.Max(rectT.MinSize.Y, Rect.Height)); TextBlock.SetTextPos(); } - GUI.Style.Apply(textBlock, "", this); - - //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, - //use the default CJK font as a fallback - if (TextManager.IsCJK(textBlock.Text) && !textBlock.Font.IsCJK) - { - textBlock.Font = GUI.CJKFont; - } + GUIStyle.Apply(textBlock, "", this); Enabled = true; } @@ -217,7 +214,7 @@ namespace Barotrauma float expand = (pulseExpand * 20.0f) * GUI.Scale; expandRect.Inflate(expand, expand); - GUI.Style.ButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); + GUIStyle.EndRoundButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e9ce18130..e78e4e160 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using Barotrauma.IO; using RestSharp; using System.Net; +using System.Collections.Immutable; namespace Barotrauma { @@ -66,7 +67,7 @@ namespace Barotrauma { foreach (GUIComponent child in Children) { - if (child.UserData == obj || (child.userData != null && child.userData.Equals(obj))) { return child; } + if (child.UserData == obj || (child.UserData != null && child.UserData.Equals(obj))) { return child; } } return null; } @@ -107,7 +108,7 @@ namespace Barotrauma } public GUIComponent FindChild(object userData, bool recursive = false) { - var matchingChild = Children.FirstOrDefault(c => c.userData == userData); + var matchingChild = Children.FirstOrDefault(c => c.UserData == userData); if (recursive && matchingChild == null) { foreach (GUIComponent child in Children) @@ -122,7 +123,7 @@ namespace Barotrauma public IEnumerable FindChildren(object userData) { - return Children.Where(c => c.userData == userData); + return Children.Where(c => c.UserData == userData); } public IEnumerable FindChildren(Func predicate) @@ -161,9 +162,7 @@ namespace Barotrauma protected Alignment alignment; - protected GUIComponentStyle style; - - protected object userData; + protected Identifier[] styleHierarchy; public bool CanBeFocused; @@ -206,16 +205,14 @@ namespace Barotrauma } } - public virtual ScalableFont Font + public virtual GUIFont Font { get; set; } - - // Use the rawtooltip when copying displayed tooltips so that any possible color-data related values are translated over as well - public string RawToolTip; - private string toolTip; - public virtual string ToolTip + + private RichString toolTip; + public virtual RichString ToolTip { get { @@ -223,18 +220,12 @@ namespace Barotrauma } set { - RawToolTip = value; - TooltipRichTextData = RichTextData.GetRichTextData(value, out value); toolTip = value; } } - public List TooltipRichTextData = null; - public GUIComponentStyle Style - { - get { return style; } - } + => GUIComponentStyle.FromHierarchy(styleHierarchy); public bool Visible { @@ -258,8 +249,8 @@ namespace Barotrauma protected Rectangle ClampRect(Rectangle r) { - if (Parent == null || !ClampMouseRectToParent) { return r; } - Rectangle parentRect = Parent.ClampRect(Parent.Rect); + if (Parent is null) { return r; } + Rectangle parentRect = !Parent.ClampMouseRectToParent ? Parent.Rect : Parent.ClampRect(Parent.Rect); if (parentRect.Width <= 0 || parentRect.Height <= 0) { return Rectangle.Empty; } if (parentRect.X > r.X) { @@ -293,11 +284,13 @@ namespace Barotrauma } public bool ClampMouseRectToParent { get; set; } = false; + public virtual Rectangle MouseRect { get { if (!CanBeFocused) { return Rectangle.Empty; } + return ClampMouseRectToParent ? ClampRect(Rect) : Rect; } } @@ -310,13 +303,13 @@ namespace Barotrauma protected ComponentState _state; protected ComponentState _previousState; - protected bool selected; + protected bool isSelected; public virtual bool Selected { - get { return selected; } + get { return isSelected; } set { - selected = value; + isSelected = value; foreach (var child in Children) { child.Selected = value; @@ -338,11 +331,8 @@ namespace Barotrauma } } - public object UserData - { - get { return userData; } - set { userData = value; } - } + #warning TODO: this is cursed, stop using this + public object UserData; public int CountChildren { @@ -417,20 +407,20 @@ namespace Barotrauma Visible = true; OutlineColor = Color.Transparent; - Font = GUI.Font; + Font = GUIStyle.Font; CanBeFocused = true; - if (style != null) { GUI.Style.Apply(this, style); } + if (style != null) { GUIStyle.Apply(this, style); } } protected GUIComponent(string style) { Visible = true; OutlineColor = Color.Transparent; - Font = GUI.Font; + Font = GUIStyle.Font; CanBeFocused = true; - if (style != null) { GUI.Style.Apply(this, style); } + if (style != null) { GUIStyle.Apply(this, style); } } #region Updating @@ -486,7 +476,7 @@ namespace Barotrauma { if (GUI.IsMouseOn(this) && PlayerInput.SecondaryMouseButtonClicked()) { - OnSecondaryClicked?.Invoke(this, userData); + OnSecondaryClicked?.Invoke(this, UserData); } } @@ -704,7 +694,7 @@ namespace Barotrauma if (GlowOnSelect && State == ComponentState.Selected) { - GUI.UIGlow.Draw(spriteBatch, Rect, SelectedColor); + GUIStyle.UIGlow.Draw(spriteBatch, Rect, SelectedColor); } if (flashTimer > 0.0f) @@ -724,7 +714,7 @@ namespace Barotrauma } else { - var glow = useCircularFlash ? GUI.UIGlowCircular : GUI.UIGlow; + var glow = useCircularFlash ? GUIStyle.UIGlowCircular : GUIStyle.UIGlow; glow.Draw(spriteBatch, flashRect, flashColor * (float)Math.Sin(flashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f)); @@ -738,24 +728,24 @@ namespace Barotrauma public void DrawToolTip(SpriteBatch spriteBatch) { if (!Visible) { return; } - DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); + DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect); } - public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Vector2 pos, List richTextData = null) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) { - if (Tutorials.Tutorial.ContentRunning) { return; } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); Point padding = new Point((int)(10 * GUI.Scale)); - if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) + if (toolTipBlock == null || (RichString)toolTipBlock.UserData != toolTip) { - toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), toolTip, font: GUIStyle.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( - (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), - (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); - toolTipBlock.userData = toolTip; + (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), + (int)(GUIStyle.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); + toolTipBlock.UserData = toolTip; } toolTipBlock.RectTransform.AbsoluteOffset = pos.ToPoint(); @@ -764,21 +754,21 @@ namespace Barotrauma toolTipBlock.DrawManually(spriteBatch); } - public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List richTextData = null) + public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement) { - if (Tutorials.Tutorial.ContentRunning) { return; } + if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; } int width = (int)(400 * GUI.Scale); int height = (int)(18 * GUI.Scale); Point padding = new Point((int)(10 * GUI.Scale)); - if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) + if (toolTipBlock == null || (RichString)toolTipBlock.UserData != toolTip) { - toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), toolTip, font: GUIStyle.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); - toolTipBlock.userData = toolTip; + toolTipBlock.UserData = toolTip; } toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom); @@ -811,7 +801,7 @@ namespace Barotrauma this.useRectangleFlash = useRectangleFlash; this.useCircularFlash = useCircularFlash; this.flashDuration = flashDuration; - flashColor = (color == null) ? GUI.Style.Red : (Color)color; + flashColor = (color == null) ? GUIStyle.Red : (Color)color; } public void FadeOut(float duration, bool removeAfter, float wait = 0.0f) @@ -952,8 +942,7 @@ namespace Barotrauma ApplySizeRestrictions(style); } - - this.style = style; + styleHierarchy = GUIComponentStyle.ToHierarchy(style); } public void ApplySizeRestrictions(GUIComponentStyle style) @@ -972,11 +961,11 @@ namespace Barotrauma } } - public static GUIComponent FromXML(XElement element, RectTransform parent) + public static GUIComponent FromXML(ContentXElement element, RectTransform parent) { GUIComponent component = null; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase) && !CheckConditional(subElement)) { @@ -1027,7 +1016,7 @@ namespace Barotrauma if (component != null) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) { continue; } FromXML(subElement, component is GUIListBox listBox ? listBox.Content.RectTransform : component.RectTransform); @@ -1078,8 +1067,9 @@ namespace Barotrauma switch (attribute.Name.ToString().ToLowerInvariant()) { case "language": - string[] languages = element.GetAttributeStringArray(attribute.Name.ToString(), new string[0]); - if (!languages.Any(l => GameMain.Config.Language.Equals(l, StringComparison.OrdinalIgnoreCase))) { return false; } + var languages = element.GetAttributeIdentifierArray(attribute.Name.ToString(), Array.Empty()) + .Select(s => new LanguageIdentifier(s)); + if (!languages.Any(l => GameSettings.CurrentConfig.Language == l)) { return false; } break; case "gameversion": var version = new Version(attribute.Value); @@ -1136,23 +1126,12 @@ namespace Barotrauma if (element.Attribute("color") != null) { color = element.GetAttributeColor("color", Color.White); } float scale = element.GetAttributeFloat("scale", 1.0f); bool wrap = element.GetAttributeBool("wrap", true); - Alignment alignment = Alignment.Center; - Enum.TryParse(element.GetAttributeString("alignment", "Center"), out alignment); - ScalableFont font = GUI.Font; - switch (element.GetAttributeString("font", "Font").ToLowerInvariant()) + Alignment alignment = + element.GetAttributeEnum("alignment", text.Contains('\n') ? Alignment.Left : Alignment.Center); + GUIFont font; + if (!GUIStyle.Fonts.TryGetValue(element.GetAttributeIdentifier("font", "Font"), out font)) { - case "font": - font = GUI.Font; - break; - case "smallfont": - font = GUI.SmallFont; - break; - case "largefont": - font = GUI.LargeFont; - break; - case "subheading": - font = GUI.SubHeadingFont; - break; + font = GUIStyle.Font; } var textBlock = new GUITextBlock(RectTransform.Load(element, parent), @@ -1265,7 +1244,7 @@ namespace Barotrauma }; } - private static GUIImage LoadGUIImage(XElement element, RectTransform parent) + private static GUIImage LoadGUIImage(ContentXElement element, RectTransform parent) { Sprite sprite; string url = element.GetAttributeString("url", ""); @@ -1298,11 +1277,11 @@ namespace Barotrauma return new GUIImage(RectTransform.Load(element, parent), sprite, scaleToFit: true); } - private static GUIButton LoadAccordion(XElement element, RectTransform parent) + private static GUIButton LoadAccordion(ContentXElement element, RectTransform parent) { var button = LoadGUIButton(element, parent); List content = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { var contentElement = FromXML(subElement, parent); if (contentElement != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index f752af7c2..5b4ae436a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -9,16 +9,23 @@ namespace Barotrauma { struct ContextMenuOption { - public string Label; + public LocalizedString Label; public Action OnSelected; public ContextMenuOption[]? SubOptions; public bool IsEnabled; - public string Tooltip; + public LocalizedString Tooltip; + + public ContextMenuOption(string labelTag, bool isEnabled, Action onSelected) + : this(TextManager.Get(labelTag), isEnabled, onSelected) { } + + public ContextMenuOption(Identifier labelTag, bool isEnabled, Action onSelected) + : this(TextManager.Get(labelTag), isEnabled, onSelected) { } + // Creates a regular context menu - public ContextMenuOption(string label, bool isEnabled, Action onSelected) + public ContextMenuOption(LocalizedString label, bool isEnabled, Action onSelected) { - Label = TextManager.Get(label, returnNull: true) ?? label; + Label = label; OnSelected = onSelected; IsEnabled = isEnabled; SubOptions = null; @@ -49,14 +56,14 @@ namespace Barotrauma /// Header text /// Background style /// list of context menu options - public GUIContextMenu(Vector2? position, string header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) + public GUIContextMenu(Vector2? position, LocalizedString header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) { Vector2 pos = position ?? PlayerInput.MousePosition; - ScalableFont headerFont = GUI.SubHeadingFont; - ScalableFont font = GUI.SmallFont; // font the context menu options use + GUIFont headerFont = GUIStyle.SubHeadingFont; + GUIFont font = GUIStyle.SmallFont; // font the context menu options use Vector4 padding = new Vector4(4), headerPadding = new Vector4(8); int horizontalPadding = (int) (padding.X + padding.Z), verticalPadding = (int) (padding.Y + padding.W); - bool hasHeader = !string.IsNullOrWhiteSpace(header); + bool hasHeader = !header.IsNullOrWhiteSpace(); //---------------------------------------------------------------------------------- // Estimate the size of the context menu @@ -111,7 +118,7 @@ namespace Barotrauma }; Options.Add(option, optionElement); - if (!string.IsNullOrWhiteSpace(option.Tooltip) && optionElement.Enabled) + if (!option.Tooltip.IsNullOrWhiteSpace() && optionElement.Enabled) { optionElement.ToolTip = option.Tooltip; } @@ -179,7 +186,7 @@ namespace Barotrauma public static GUIContextMenu CreateContextMenu(params ContextMenuOption[] options) => CreateContextMenu(PlayerInput.MousePosition, string.Empty, null, options); - public static GUIContextMenu CreateContextMenu(Vector2? pos, string header, Color? headerColor, params ContextMenuOption[] options) + public static GUIContextMenu CreateContextMenu(Vector2? pos, LocalizedString header, Color? headerColor, params ContextMenuOption[] options) { GUIContextMenu menu = new GUIContextMenu(pos,header, "GUIToolTip", options); if (headerColor != null) @@ -209,7 +216,7 @@ namespace Barotrauma /// String whose size to inflate by /// What font to use /// The size of the text - private Vector2 InflateSize(ref Point size, string label, ScalableFont font) + private Vector2 InflateSize(ref Point size, LocalizedString label, ScalableFont font) { Vector2 textSize = font.MeasureString(label); size.X = Math.Max((int) Math.Ceiling(textSize.X), size.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 6c2780430..b4dc7513d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -55,9 +55,8 @@ namespace Barotrauma { get { return listBox.SelectedComponent; } } - - // TODO: fix implicit hiding - public bool Selected + + public override bool Selected { get { @@ -97,7 +96,7 @@ namespace Barotrauma set { button.TextColor = value; } } - public override ScalableFont Font + public override GUIFont Font { get { return button?.Font ?? base.Font; } set @@ -142,13 +141,13 @@ namespace Barotrauma get { return selectedIndexMultiple; } } - public string Text + public LocalizedString Text { get { return button.Text; } set { button.Text = value; } } - public override string ToolTip + public override RichString ToolTip { get { @@ -162,8 +161,10 @@ namespace Barotrauma } } - public GUIDropDown(RectTransform rectT, string text = "", int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false) : base(style, rectT) + public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false) : base(style, rectT) { + text ??= new RawLString(""); + HoverCursor = CursorState.Hand; CanBeFocused = true; @@ -173,7 +174,7 @@ namespace Barotrauma { OnClicked = OnClicked }; - GUI.Style.Apply(button, "", this); + GUIStyle.Apply(button, "", this); button.TextBlock.SetTextPos(); Anchor listAnchor = dropAbove ? Anchor.TopCenter : Anchor.BottomCenter; @@ -184,13 +185,13 @@ namespace Barotrauma Enabled = !selectMultiple }; if (!selectMultiple) { listBox.OnSelected = SelectItem; } - GUI.Style.Apply(listBox, "GUIListBox", this); - GUI.Style.Apply(listBox.ContentBackground, "GUIListBox", this); + GUIStyle.Apply(listBox, "GUIListBox", this); + GUIStyle.Apply(listBox.ContentBackground, "GUIListBox", this); - if (button.Style.ChildStyles.ContainsKey("dropdownicon")) + if (button.Style.ChildStyles.ContainsKey("dropdownicon".ToIdentifier())) { icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), button.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, null, scaleToFit: true); - icon.ApplyStyle(button.Style.ChildStyles["dropdownicon"]); + icon.ApplyStyle(button.Style.ChildStyles["dropdownicon".ToIdentifier()]); } currentHighestParent = FindHighestParent(); @@ -244,8 +245,9 @@ namespace Barotrauma return parentHierarchy.Last(); } - public void AddItem(string text, object userData = null, string toolTip = "") + public void AddItem(LocalizedString text, object userData = null, LocalizedString toolTip = null) { + toolTip ??= ""; if (selectMultiple) { var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) @@ -261,7 +263,7 @@ namespace Barotrauma ToolTip = toolTip, OnSelected = (GUITickBox tb) => { - List texts = new List(); + List texts = new List(); selectedDataMultiple.Clear(); selectedIndexMultiple.Clear(); int i = 0; @@ -276,7 +278,7 @@ namespace Barotrauma } i++; } - button.Text = string.Join(", ", texts); + button.Text = LocalizedString.Join(", ", texts); // TODO: The callback is called at least twice, remove this? OnSelected?.Invoke(tb.Parent, tb.Parent.UserData); return true; @@ -368,8 +370,9 @@ namespace Barotrauma Dropped = !Dropped; if (Dropped && Enabled) { - OnDropped?.Invoke(this, userData); + OnDropped?.Invoke(this, UserData); listBox.UpdateScrollBarSize(); + listBox.UpdateDimensions(); GUI.KeyboardDispatcher.Subscriber = this; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index e7be6a3ec..23b891e34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -177,6 +177,7 @@ namespace Barotrauma spriteBatch.Begin(blendState: BlendState, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } + var style = Style; if (style != null) { foreach (UISprite uiSprite in style.Sprites[State]) @@ -193,7 +194,7 @@ namespace Barotrauma } } } - else if (sprite?.Texture != null) + else if (sprite?.Texture is { IsDisposed: false }) { spriteBatch.Draw(sprite.Texture, Rect.Center.ToVector2(), sourceRect, currentColor * (currentColor.A / 255.0f), Rotation, origin, Scale, SpriteEffects, 0.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index d6e4efb6e..d21951cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -92,29 +92,19 @@ namespace Barotrauma foreach (RectTransform child in RectTransform.Children) { if (child.GUIComponent.IgnoreLayoutGroups) { continue; } - if (child.ScaleBasis == ScaleBasis.BothHeight) { child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); } - if (child.ScaleBasis == ScaleBasis.BothWidth) { child.MinSize = new Point(child.MinSize.X, child.Rect.Width); } - if (child.ScaleBasis == ScaleBasis.Smallest) + + switch (child.ScaleBasis) { - if (Rect.Width < Rect.Height) - { - child.MinSize = new Point(child.MinSize.X, child.Rect.Width); - } - else - { - child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); - } - } - if (child.ScaleBasis == ScaleBasis.Largest) - { - if (Rect.Width > Rect.Height) - { - child.MinSize = new Point(child.MinSize.X, child.Rect.Width); - } - else - { - child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); - } + case ScaleBasis.BothHeight: + case ScaleBasis.Smallest when Rect.Height <= Rect.Width: + case ScaleBasis.Largest when Rect.Height > Rect.Width: + child.MinSize = new Point((int)((child.Rect.Height * child.RelativeSize.X) / child.RelativeSize.Y), child.MinSize.Y); + break; + case ScaleBasis.BothWidth: + case ScaleBasis.Smallest when Rect.Width <= Rect.Height: + case ScaleBasis.Largest when Rect.Width > Rect.Height: + child.MinSize = new Point(child.MinSize.X, (int)((child.Rect.Width * child.RelativeSize.Y) / child.RelativeSize.X)); + break; } } @@ -144,42 +134,65 @@ namespace Barotrauma foreach (var child in RectTransform.Children) { if (child.GUIComponent.IgnoreLayoutGroups) { continue; } + + float currentStretchFactor = child.ScaleBasis == ScaleBasis.Normal ? stretchFactor : 1.0f; child.SetPosition(childAnchor); + + void advancePositionsAndCalculateChildSizes( + ref int childNonScaledSize, + ref float childRelativeSize, + int childMinSize, + int childMaxSize, + int childRectSize, + int selfRectSize) + { + if (child.IsFixedSize) + { + absPos += childNonScaledSize + absoluteSpacing; + } + else + { + absPos += (int)Math.Round(MathHelper.Clamp(childRectSize * currentStretchFactor, childMinSize, childMaxSize) + (absoluteSpacing * currentStretchFactor)); + if (stretch) + { + float relativeSize = + MathF.Round(childRelativeSize * currentStretchFactor * selfRectSize) / selfRectSize; + childRelativeSize = relativeSize; + } + } + } + + Point childNonScaledSize = child.NonScaledSize; + Vector2 childRelativeSize = child.RelativeSize; if (isHorizontal) { child.RelativeOffset = new Vector2(relPos, child.RelativeOffset.Y); child.AbsoluteOffset = new Point(absPos, child.AbsoluteOffset.Y); - if (child.IsFixedSize) - { - absPos += child.NonScaledSize.X + absoluteSpacing; - } - else - { - absPos += (int)(MathHelper.Clamp(child.Rect.Width * stretchFactor, child.MinSize.X, child.MaxSize.X) + (absoluteSpacing * stretchFactor)); - if (stretch) - { - child.RelativeSize = new Vector2(child.RelativeSize.X * stretchFactor, child.RelativeSize.Y); - } - } + advancePositionsAndCalculateChildSizes( + ref childNonScaledSize.X, + ref childRelativeSize.X, + child.MinSize.X, + child.MaxSize.X, + child.Rect.Width, + Rect.Width); } else { child.RelativeOffset = new Vector2(child.RelativeOffset.X, relPos); child.AbsoluteOffset = new Point(child.AbsoluteOffset.X, absPos); - if (child.IsFixedSize) - { - absPos += child.NonScaledSize.Y + absoluteSpacing; - } - else - { - absPos += (int)(MathHelper.Clamp(child.Rect.Height * stretchFactor, child.MinSize.Y, child.MaxSize.Y) + (absoluteSpacing * stretchFactor)); - if (stretch) - { - child.RelativeSize = new Vector2(child.RelativeSize.X, child.RelativeSize.Y * stretchFactor); - } - } + advancePositionsAndCalculateChildSizes( + ref childNonScaledSize.Y, + ref childRelativeSize.Y, + child.MinSize.Y, + child.MaxSize.Y, + child.Rect.Height, + Rect.Height); } + child.NonScaledSize = childNonScaledSize; + child.RelativeSize = childRelativeSize; relPos += relativeSpacing * stretchFactor; + if (isHorizontal) { relPos = MathF.Round(relPos * Rect.Width) / Rect.Width; } + else { relPos = MathF.Round(relPos * Rect.Height) / Rect.Height; } } needsToRecalculate = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 92b4362c2..a40d29880 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -148,7 +148,11 @@ namespace Barotrauma } // TODO: fix implicit hiding - public bool Selected { get; set; } + public override bool Selected + { + get { return isSelected; } + set { isSelected = value; } + } public IReadOnlyList AllSelected => selected; @@ -328,7 +332,7 @@ namespace Barotrauma }; if (style != null) { - GUI.Style.Apply(ContentBackground, "", this); + GUIStyle.Apply(ContentBackground, "", this); } if (color.HasValue) { @@ -435,7 +439,7 @@ namespace Barotrauma Vector2 topOffset = CalculateTopOffset(); int x = (int)topOffset.X; int y = (int)topOffset.Y; - + for (int i = 0; i < Content.CountChildren; i++) { GUIComponent child = Content.GetChild(i); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 4ec5929f3..72084edbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -10,7 +10,8 @@ namespace Barotrauma { public class GUIMessageBox : GUIFrame { - public static List MessageBoxes = new List(); + #warning TODO: change this to List and fix incorrect uses of this list + public readonly static List MessageBoxes = new List(); private static int DefaultWidth { get { return Math.Max(400, (int)(400 * (GameMain.GraphicsWidth / GUI.ReferenceResolution.X))); } @@ -70,14 +71,14 @@ namespace Barotrauma public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); - public GUIMessageBox(string headerText, string text, Vector2? relativeSize = null, Point? minSize = null) - : this(headerText, text, new string[] { "OK" }, relativeSize, minSize) + public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null) + : this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize) { this.Buttons[0].OnClicked = Close; } - public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null, bool parseRichText = false) - : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") + public GUIMessageBox(RichString headerText, RichString text, LocalizedString[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null, string iconStyle = "", Sprite backgroundIcon = null) + : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUIStyle.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { int width = (int)(DefaultWidth * type switch { @@ -125,23 +126,24 @@ namespace Barotrauma InnerFrame.RectTransform.ScreenSpaceOffset = new Point(-offset, offset); CanBeFocused = false; } - GUI.Style.Apply(InnerFrame, "", this); + GUIStyle.Apply(InnerFrame, "", this); this.type = type; Tag = tag; + #warning TODO: These should be broken into separate methods at least if (type == Type.Default || type == Type.Vote) { Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), - headerText, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Header, "", this); + headerText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (!text.IsNullOrWhiteSpace()) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Text, "", this); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUIStyle.Apply(Text, "", this); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); Text.RectTransform.IsFixedSize = true; @@ -154,7 +156,7 @@ namespace Barotrauma }; int buttonSize = 35; - var buttonStyle = GUI.Style.GetComponentStyle("GUIButton"); + var buttonStyle = GUIStyle.GetComponentStyle("GUIButton"); if (buttonStyle != null && buttonStyle.Height.HasValue) { buttonSize = buttonStyle.Height.Value; @@ -189,7 +191,7 @@ namespace Barotrauma InnerFrame.RectTransform.AbsoluteOffset = new Point(0, GameMain.GraphicsHeight); CanBeFocused = false; AutoClose = true; - GUI.Style.Apply(InnerFrame, "", this); + GUIStyle.Apply(InnerFrame, "", this); var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), InnerFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) @@ -219,11 +221,11 @@ namespace Barotrauma }; InputType? closeInput = null; - if (GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None) { closeInput = InputType.Use; } - else if (GameMain.Config.KeyBind(InputType.Select).MouseButton == MouseButton.None) + else if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select].MouseButton == MouseButton.None) { closeInput = InputType.Select; } @@ -236,24 +238,24 @@ namespace Barotrauma { GUIButton btn = component as GUIButton; btn?.OnClicked(btn, btn.UserData); - btn?.Flash(GUI.Style.Green); + btn?.Flash(GUIStyle.Green); } }; } - Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Header, "", this); + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); + GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (!text.IsNullOrWhiteSpace()) { - Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true, parseRichText: parseRichText); - GUI.Style.Apply(Text, "", this); + Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); + GUIStyle.Apply(Text, "", this); Content.Recalculate(); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); Text.RectTransform.IsFixedSize = true; - if (string.IsNullOrWhiteSpace(headerText)) + if (headerText.IsNullOrWhiteSpace()) { Content.ChildAnchor = Anchor.Center; } @@ -275,7 +277,7 @@ namespace Barotrauma else if (type == Type.Hint) { CanBeFocused = false; - GUI.Style.Apply(InnerFrame, "", this); + GUIStyle.Apply(InnerFrame, "", this); Point absoluteSpacing = GUIStyle.ItemFrameMargin.Multiply(1.0f / 5.0f); var verticalLayoutGroup = new GUILayoutGroup(new RectTransform(GetVerticalLayoutGroupSize(), parent: InnerFrame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) @@ -353,18 +355,18 @@ namespace Barotrauma }; Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); - GUI.Style.Apply(Header, "", this); + GUIStyle.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); - if (!string.IsNullOrWhiteSpace(text)) + if (!text.IsNullOrWhiteSpace()) { Text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), text, textAlignment: textAlignment, wrap: true); - GUI.Style.Apply(Text, "", this); + GUIStyle.Apply(Text, "", this); Content.Recalculate(); Text.RectTransform.NonScaledSize = Text.RectTransform.MinSize = Text.RectTransform.MaxSize = new Point(Text.Rect.Width, Text.Rect.Height); Text.RectTransform.IsFixedSize = true; - if (string.IsNullOrWhiteSpace(headerText)) + if (headerText.IsNullOrWhiteSpace()) { Header.RectTransform.Parent = null; Content.ChildAnchor = Anchor.Center; @@ -410,7 +412,7 @@ namespace Barotrauma /// /// Use to create a message box of Hint type /// - public GUIMessageBox(string hintIdentifier, string text, Sprite icon) : this("", text, new string[0], textAlignment: Alignment.CenterLeft, type: Type.Hint, icon: icon) + public GUIMessageBox(Identifier hintIdentifier, LocalizedString text, Sprite icon) : this("", text, Array.Empty(), textAlignment: Alignment.CenterLeft, type: Type.Hint, icon: icon) { if (InnerFrame.FindChild("dontshowagain", recursive: true) is GUITickBox dontShowAgainTickBox) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index e512e979a..a410c8db5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -162,7 +162,7 @@ namespace Barotrauma } } - public override ScalableFont Font + public override GUIFont Font { get { @@ -225,7 +225,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); - GUI.Style.Apply(PlusButton, "PlusButton", this); + GUIStyle.Apply(PlusButton, "PlusButton", this); PlusButton.OnButtonDown += () => { pressedTimer = pressedDelay; @@ -246,7 +246,7 @@ namespace Barotrauma }; MinusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform, Anchor.BottomRight), style: null); - GUI.Style.Apply(MinusButton, "MinusButton", this); + GUIStyle.Apply(MinusButton, "MinusButton", this); MinusButton.OnButtonDown += () => { pressedTimer = pressedDelay; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs new file mode 100644 index 000000000..ccef64092 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -0,0 +1,395 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + public abstract class GUIPrefab : Prefab + { + public GUIPrefab(ContentXElement element, UIStyleFile file) : base(file, element) { } + + protected override Identifier DetermineIdentifier(XElement element) + { + return element.NameAsIdentifier(); + } + } + + public abstract class GUISelector where T : GUIPrefab + { + public readonly PrefabSelector Prefabs = new PrefabSelector(); + public readonly Identifier Identifier; + + public GUISelector(string identifier) + { + Identifier = identifier.ToIdentifier(); + } + } + + public class GUIFontPrefab : GUIPrefab + { + private readonly ContentXElement element; + private ScalableFont font; + public ScalableFont Font + { + get + { + if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } + return font; + } + } + + private ScalableFont cjkFont; + + public ScalableFont CjkFont + { + get + { + if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } + if (font.IsCJK) { return font; } + return cjkFont; + } + } + + public LanguageIdentifier Language { get; private set; } + + public GUIFontPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + this.element = element; + LoadFont(); + } + + private void LoadFont() + { + string fontPath = GetFontFilePath(element); + uint size = GetFontSize(element); + bool dynamicLoading = GetFontDynamicLoading(element); + bool isCJK = GetIsCJK(element); + font?.Dispose(); + cjkFont?.Dispose(); + font = new ScalableFont(fontPath, size, GameMain.Instance.GraphicsDevice, dynamicLoading, isCJK) + { + ForceUpperCase = element.GetAttributeBool("forceuppercase", false) + }; + if (!isCJK) + { + cjkFont = ExtractCjkFont(element) + ?? new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf", + font.Size, GameMain.Instance.GraphicsDevice, dynamicLoading: true, isCJK: true); + cjkFont.ForceUpperCase = font.ForceUpperCase; + } + Language = GameSettings.CurrentConfig.Language; + } + + public override void Dispose() + { + font?.Dispose(); font = null; + cjkFont?.Dispose(); cjkFont = null; + } + + private ScalableFont ExtractCjkFont(ContentXElement element) + { + foreach (var subElement in element.Elements().Reverse()) + { + if (subElement.NameAsIdentifier() != "override") { continue; } + + if (subElement.GetAttributeBool("iscjk", false)) + { + return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); + } + } + return null; + } + + private string GetFontFilePath(ContentXElement element) + { + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + return subElement.GetAttributeContentPath("file")?.Value; + } + } + return element.GetAttributeContentPath("file")?.Value; + } + + private uint GetFontSize(XElement element, uint defaultSize = 14) + { + //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()) + { + uint overrideFontSize = GetFontSize(subElement, 0); + if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.CurrentConfig.Graphics.TextScale); } + } + } + + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } + Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); + if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) + { + return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.CurrentConfig.Graphics.TextScale); + } + } + return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale); + } + + private bool GetFontDynamicLoading(XElement element) + { + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + return subElement.GetAttributeBool("dynamicloading", false); + } + } + return element.GetAttributeBool("dynamicloading", false); + } + + private bool GetIsCJK(XElement element) + { + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameSettings.CurrentConfig.Language == subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier()) + { + return subElement.GetAttributeBool("iscjk", false); + } + } + return element.GetAttributeBool("iscjk", false); + } + } + + public class GUIFont : GUISelector + { + public GUIFont(string identifier) : base(identifier) { } + + public bool HasValue => Prefabs.Any(); + + public ScalableFont Value => Prefabs.ActivePrefab.Font; + + public static implicit operator ScalableFont(GUIFont reference) => reference.Value; + + public bool ForceUpperCase => HasValue && Value.ForceUpperCase; + + public uint Size => HasValue ? Value.Size : 0; + + private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); + + private ScalableFont GetFontForStr(string str) => + TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font; + + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + { + DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth); + } + + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) + { + GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth); + } + + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft) + { + DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth, alignment); + } + + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) + { + GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, alignment, forceUpperCase); + } + + public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) + { + DrawString(sb, text.Value, position, color, forceUpperCase, italics); + } + + public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false) + { + GetFontForStr(text).DrawString(sb, text, position, color, forceUpperCase, italics); + } + + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit) + { + GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase); + } + + public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false) + { + return GetFontForStr(str).MeasureString(str, removeExtraSpacing); + } + + public Vector2 MeasureChar(char c) + { + return GetFontForStr($"{c}").MeasureChar(c); + } + + public string WrapText(string text, float width) + => GetFontForStr(text).WrapText(text, width); + + public string WrapText(string text, float width, int requestCharPos, out Vector2 requestedCharPos) + => GetFontForStr(text).WrapText(text, width, requestCharPos, out requestedCharPos); + + public string WrapText(string text, float width, out Vector2[] allCharPositions) + => GetFontForStr(text).WrapText(text, width, out allCharPositions); + + public float LineHeight => Value.LineHeight; + } + + public class GUIColorPrefab : GUIPrefab + { + public readonly Color Color; + + public GUIColorPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + Color = element.GetAttributeColor("color", Color.White); + } + + public override void Dispose() { } + } + + public class GUIColor : GUISelector + { + public GUIColor(string identifier) : base(identifier) { } + + public Color Value + { + get + { + return Prefabs.ActivePrefab.Color; + } + } + + public static implicit operator Color(GUIColor reference) => reference.Value; + + public static Color operator*(GUIColor value, float scale) + { + return value.Value * scale; + } + } + + public class GUISpritePrefab : GUIPrefab + { + public readonly UISprite Sprite; + + public GUISpritePrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + Sprite = new UISprite(element); + } + + public override void Dispose() + { + Sprite.Sprite.Remove(); + } + } + + public class GUISprite : GUISelector + { + public GUISprite(string identifier) : base(identifier) { } + + public UISprite Value + { + get + { + return Prefabs.ActivePrefab.Sprite; + } + } + + public static implicit operator UISprite(GUISprite reference) => reference.Value; + + public void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color, SpriteEffects spriteEffects = SpriteEffects.None) + { + Value.Draw(spriteBatch, rect, color, spriteEffects); + } + } + + public class GUISpriteSheetPrefab : GUIPrefab + { + public readonly SpriteSheet SpriteSheet; + + public GUISpriteSheetPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + SpriteSheet = new SpriteSheet(element); + } + + public override void Dispose() + { + SpriteSheet.Remove(); + } + } + + public class GUISpriteSheet : GUISelector + { + public GUISpriteSheet(string identifier) : base(identifier) { } + + public SpriteSheet Value + { + get + { + return Prefabs.ActivePrefab.SpriteSheet; + } + } + + public int FrameCount => Value.FrameCount; + public Point FrameSize => Value.FrameSize; + + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0, float scale = 1, SpriteEffects spriteEffects = SpriteEffects.None) + { + Value.Draw(spriteBatch, pos, rotate, scale, spriteEffects); + } + + public void Draw(ISpriteBatch spriteBatch, Vector2 pos, Color color, Vector2 origin, float rotate = 0, float scale = 1, SpriteEffects spriteEffects = SpriteEffects.None, float? depth = null) + { + Value.Draw(spriteBatch, pos, color, origin, rotate, scale, spriteEffects, depth); + } + + public void Draw(ISpriteBatch spriteBatch, int spriteIndex, Vector2 pos, Color color, Vector2 origin, float rotate, Vector2 scale, SpriteEffects spriteEffects = SpriteEffects.None, float? depth = null) + { + Value.Draw(spriteBatch, spriteIndex, pos, color, origin, rotate, scale, spriteEffects, depth); + } + + public static implicit operator SpriteSheet(GUISpriteSheet reference) => reference.Value; + } + + public class GUICursorPrefab : GUIPrefab + { + public readonly Sprite[] Sprites; + + public GUICursorPrefab(ContentXElement element, UIStyleFile file) : base(element, file) + { + Sprites = new Sprite[Enum.GetValues(typeof(CursorState)).Length]; + foreach (var subElement in element.Elements()) + { + CursorState state = subElement.GetAttributeEnum("state", CursorState.Default); + Sprites[(int)state] = new Sprite(subElement); + } + } + + public override void Dispose() + { + foreach (var sprite in Sprites) + { + sprite?.Remove(); + } + } + } + + public class GUICursor : GUISelector + { + public GUICursor(string identifier) : base(identifier) { } + + public Sprite this[CursorState k] => Prefabs.ActivePrefab.Sprites[(int)k]; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index 0fe0f0675..02d1c9c29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -47,9 +47,9 @@ namespace Barotrauma } isHorizontal = (Rect.Width > Rect.Height); frame = new GUIFrame(new RectTransform(Vector2.One, rectT)); - GUI.Style.Apply(frame, "", this); + GUIStyle.Apply(frame, "", this); slider = new GUIFrame(new RectTransform(Vector2.One, rectT)); - GUI.Style.Apply(slider, "Slider", this); + GUIStyle.Apply(slider, "Slider", this); this.showFrame = showFrame; this.barSize = barSize; Enabled = true; @@ -62,10 +62,10 @@ namespace Barotrauma public Rectangle GetSliderRect(float fillAmount) { Rectangle sliderArea = new Rectangle( - frame.Rect.X + (int)style.Padding.X, - frame.Rect.Y + (int)style.Padding.Y, - (int)(frame.Rect.Width - style.Padding.X - style.Padding.Z), - (int)(frame.Rect.Height - style.Padding.Y - style.Padding.W)); + frame.Rect.X + (int)Style.Padding.X, + frame.Rect.Y + (int)Style.Padding.Y, + (int)(frame.Rect.Width - Style.Padding.X - Style.Padding.Z), + (int)(frame.Rect.Height - Style.Padding.Y - Style.Padding.W)); Vector4 sliceBorderSizes = Vector4.Zero; if (slider.sprites.ContainsKey(slider.State) && (slider.sprites[slider.State].First()?.Slice ?? false)) @@ -116,10 +116,10 @@ namespace Barotrauma var sliderRect = GetSliderRect(barSize); - slider.RectTransform.AbsoluteOffset = new Point((int)style.Padding.X, (int)style.Padding.Y); + slider.RectTransform.AbsoluteOffset = new Point((int)Style.Padding.X, (int)Style.Padding.Y); slider.RectTransform.MaxSize = new Point( - (int)(Rect.Width - style.Padding.X + style.Padding.Z), - (int)(Rect.Height - style.Padding.Y + style.Padding.W)); + (int)(Rect.Width - Style.Padding.X + Style.Padding.Z), + (int)(Rect.Height - Style.Padding.Y + Style.Padding.W)); frame.Visible = showFrame; slider.Visible = BarSize > 0.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index aa44f2ffc..ff245d132 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -1,6 +1,5 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; namespace Barotrauma @@ -29,7 +28,7 @@ namespace Barotrauma public bool IsBooleanSwitch; - public override string ToolTip + public override RichString ToolTip { get { return base.ToolTip; } set @@ -203,7 +202,7 @@ namespace Barotrauma CanBeFocused = true; this.isHorizontal = isHorizontal ?? (Rect.Width > Rect.Height); Frame = new GUIFrame(new RectTransform(Vector2.One, rectT)); - GUI.Style.Apply(Frame, IsHorizontal ? "GUIFrameHorizontal" : "GUIFrameVertical", this); + GUIStyle.Apply(Frame, IsHorizontal ? "GUIFrameHorizontal" : "GUIFrameVertical", this); this.barSize = barSize; Bar = new GUIButton(new RectTransform(Vector2.One, rectT, IsHorizontal ? Anchor.CenterLeft : Anchor.TopCenter), color: color, style: null); @@ -224,7 +223,7 @@ namespace Barotrauma break; } - GUI.Style.Apply(Bar, IsHorizontal ? "GUIButtonHorizontal" : "GUIButtonVertical", this); + GUIStyle.Apply(Bar, IsHorizontal ? "GUIButtonHorizontal" : "GUIButtonVertical", this); Bar.OnPressed = SelectBar; enabled = true; UpdateRect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index f8cbd5414..0c8828ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -1,523 +1,195 @@ -using Barotrauma.Extensions; +using System; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; -using System.Collections.Generic; -using System.Xml.Linq; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; namespace Barotrauma { - public class GUIStyle + public static class GUIStyle { - private Dictionary componentStyles; - - private readonly XElement configElement; - - private GraphicsDevice graphicsDevice; - - private ScalableFont defaultFont; - - public ScalableFont Font { get; private set; } - public ScalableFont GlobalFont { get; private set; } - public ScalableFont UnscaledSmallFont { get; private set; } - public ScalableFont SmallFont { get; private set; } - public ScalableFont LargeFont { get; private set; } - public ScalableFont SubHeadingFont { get; private set; } - public ScalableFont DigitalFont { get; private set; } - public ScalableFont HotkeyFont { get; private set; } - public ScalableFont MonospacedFont { get; private set; } - - public Dictionary ForceFontUpperCase + public readonly static ImmutableDictionary Fonts; + public readonly static ImmutableDictionary Sprites; + public readonly static ImmutableDictionary SpriteSheets; + public readonly static ImmutableDictionary Colors; + static GUIStyle() { - get; - private set; - } = new Dictionary(); + var guiClassProperties = typeof(GUIStyle).GetFields(BindingFlags.Public | BindingFlags.Static); - public readonly Sprite[] CursorSprite = new Sprite[7]; + ImmutableDictionary getPropertiesOfType() where T : class + { + return guiClassProperties + .Where(p => p.FieldType == typeof(T)) + .Select(p => (p.Name.ToIdentifier(), p.GetValue(null) as T)) + .ToImmutableDictionary(); + } - public UISprite RadiationSprite { get; private set; } - public SpriteSheet RadiationAnimSpriteSheet { get; private set; } + Fonts = getPropertiesOfType(); + Sprites = getPropertiesOfType(); + SpriteSheets = getPropertiesOfType(); + Colors = getPropertiesOfType(); + } - public SpriteSheet SavingIndicator { get; private set; } + public readonly static PrefabCollection ComponentStyles = new PrefabCollection(); - public UISprite UIGlow { get; private set; } + 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"); + public readonly static GUIFont SubHeadingFont = new GUIFont("SubHeadingFont"); + public readonly static GUIFont DigitalFont = new GUIFont("DigitalFont"); + public readonly static GUIFont HotkeyFont = new GUIFont("HotkeyFont"); + public readonly static GUIFont MonospacedFont = new GUIFont("MonospacedFont"); - public UISprite PingCircle { get; private set; } + public readonly static GUICursor CursorSprite = new GUICursor("Cursor"); - public UISprite YouAreHereCircle { get; private set; } + public readonly static GUISprite SubmarineLocationIcon = new GUISprite("SubmarineLocationIcon"); + public readonly static GUISprite Arrow = new GUISprite("Arrow"); + public readonly static GUISprite SpeechBubbleIcon = new GUISprite("SpeechBubbleIcon"); + public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon"); + public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle"); - public UISprite UIGlowCircular { get; private set; } + public readonly static GUISprite Radiation = new GUISprite("Radiation"); + public readonly static GUISpriteSheet RadiationAnimSpriteSheet = new GUISpriteSheet("RadiationAnimSpriteSheet"); - public UISprite UIGlowSolidCircular { get; private set; } - public UISprite UIThermalGlow { get; private set; } + public readonly static GUISpriteSheet SavingIndicator = new GUISpriteSheet("SavingIndicator"); + public readonly static GUISpriteSheet GenericThrobber = new GUISpriteSheet("GenericThrobber"); - public UISprite ButtonPulse { get; private set; } + public readonly static GUISprite UIGlow = new GUISprite("UIGlow"); + public readonly static GUISprite TalentGlow = new GUISprite("TalentGlow"); + public readonly static GUISprite PingCircle = new GUISprite("PingCircle"); + public readonly static GUISprite UIGlowCircular = new GUISprite("UIGlowCircular"); + public readonly static GUISprite UIGlowSolidCircular = new GUISprite("UIGlowSolidCircular"); + public readonly static GUISprite UIThermalGlow = new GUISprite("UIGlowSolidCircular"); + public readonly static GUISprite ButtonPulse = new GUISprite("ButtonPulse"); - public SpriteSheet FocusIndicator { get; private set; } + public readonly static GUISprite EndRoundButtonPulse = new GUISprite("EndRoundButtonPulse"); - public UISprite IconOverflowIndicator { get; private set; } + public readonly static GUISpriteSheet FocusIndicator = new GUISpriteSheet("FocusIndicator"); + + public readonly static GUISprite IconOverflowIndicator = new GUISprite("IconOverflowIndicator"); /// /// General green color used for elements whose colors are set from code /// - public Color Green { get; private set; } = Color.LightGreen; + public readonly static GUIColor Green = new GUIColor("Green"); /// /// General red color used for elements whose colors are set from code /// - public Color Orange { get; private set; } = Color.Orange; + public readonly static GUIColor Orange = new GUIColor("Orange"); /// /// General red color used for elements whose colors are set from code /// - public Color Red { get; private set; } = Color.Red; + public readonly static GUIColor Red = new GUIColor("Red"); /// /// General blue color used for elements whose colors are set from code /// - public Color Blue { get; private set; } = Color.Blue; + public readonly static GUIColor Blue = new GUIColor("Blue"); /// /// General yellow color used for elements whose colors are set from code /// - public Color Yellow { get; private set; } = Color.Yellow; + public readonly static GUIColor Yellow = new GUIColor("Yellow"); - public Color ColorInventoryEmpty { get; private set; } = Color.Red; - public Color ColorInventoryHalf { get; private set; } = Color.Orange; - public Color ColorInventoryFull { get; private set; } = Color.LightGreen; - public Color ColorInventoryBackground { get; private set; } = Color.Gray; - public Color ColorInventoryEmptyOverlay { get; private set; } = Color.Red; + public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty"); + public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf"); + public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull"); + public readonly static GUIColor ColorInventoryBackground = new GUIColor("ColorInventoryBackground"); + public readonly static GUIColor ColorInventoryEmptyOverlay = new GUIColor("ColorInventoryEmptyOverlay"); - public Color TextColor { get; private set; } = Color.White * 0.8f; - public Color TextColorBright { get; private set; } = Color.White * 0.9f; - public Color TextColorDark { get; private set; } = Color.Black * 0.9f; - public Color TextColorDim { get; private set; } = Color.White * 0.6f; + public readonly static GUIColor TextColorNormal = new GUIColor("TextColorNormal"); + public readonly static GUIColor TextColorBright = new GUIColor("TextColorBright"); + public readonly static GUIColor TextColorDark = new GUIColor("TextColorDark"); + public readonly static GUIColor TextColorDim = new GUIColor("TextColorDim"); - public Color ItemQualityColorPoor { get; private set; } = Color.DarkRed; - public Color ItemQualityColorNormal { get; private set; } = Color.Gray; - public Color ItemQualityColorGood { get; private set; } = Color.LightGreen; - public Color ItemQualityColorExcellent { get; private set; } = Color.LightBlue; - public Color ItemQualityColorMasterwork { get; private set; } = Color.MediumPurple; - - public Color ColorReputationVeryLow { get; private set; } = Color.Red; - public Color ColorReputationLow { get; private set; } = Color.Orange; - public Color ColorReputationNeutral { get; private set; } = Color.White * 0.8f; - public Color ColorReputationHigh { get; private set; } = Color.LightBlue; - public Color ColorReputationVeryHigh { get; private set; } = Color.Blue; + public readonly static GUIColor ItemQualityColorPoor = new GUIColor("ItemQualityColorPoor"); + public readonly static GUIColor ItemQualityColorNormal = new GUIColor("ItemQualityColorNormal"); + public readonly static GUIColor ItemQualityColorGood = new GUIColor("ItemQualityColorGood"); + public readonly static GUIColor ItemQualityColorExcellent = new GUIColor("ItemQualityColorExcellent"); + public readonly static GUIColor ItemQualityColorMasterwork = new GUIColor("ItemQualityColorMasterwork"); + + public readonly static GUIColor ColorReputationVeryLow = new GUIColor("ColorReputationVeryLow"); + public readonly static GUIColor ColorReputationLow = new GUIColor("ColorReputationLow"); + public readonly static GUIColor ColorReputationNeutral = new GUIColor("ColorReputationNeutral"); + public readonly static GUIColor ColorReputationHigh = new GUIColor("ColorReputationHigh"); + public readonly static GUIColor ColorReputationVeryHigh = new GUIColor("ColorReputationVeryHigh"); // Inventory - public Color EquipmentSlotIconColor { get; private set; } = new Color(99, 70, 64); + public readonly static GUIColor EquipmentSlotIconColor = new GUIColor("EquipmentSlotIconColor"); // Health HUD - public Color BuffColorLow { get; private set; } = Color.LightGreen; - public Color BuffColorMedium { get; private set; } = Color.Green; - public Color BuffColorHigh { get; private set; } = Color.DarkGreen; + public readonly static GUIColor BuffColorLow = new GUIColor("BuffColorLow"); + public readonly static GUIColor BuffColorMedium = new GUIColor("BuffColorMedium"); + public readonly static GUIColor BuffColorHigh = new GUIColor("BuffColorHigh"); - public Color DebuffColorLow { get; private set; } = Color.DarkSalmon; - public Color DebuffColorMedium { get; private set; } = Color.Red; - public Color DebuffColorHigh { get; private set; } = Color.DarkRed; + public readonly static GUIColor DebuffColorLow = new GUIColor("DebuffColorLow"); + public readonly static GUIColor DebuffColorMedium = new GUIColor("DebuffColorMedium"); + public readonly static GUIColor DebuffColorHigh = new GUIColor("DebuffColorHigh"); - public Color HealthBarColorLow { get; private set; } = Color.Red; - public Color HealthBarColorMedium { get; private set; } = Color.Orange; - public Color HealthBarColorHigh { get; private set; } = new Color(78, 114, 88); + public readonly static GUIColor HealthBarColorLow = new GUIColor("HealthBarColorLow"); + public readonly static GUIColor HealthBarColorMedium = new GUIColor("HealthBarColorMedium"); + public readonly static GUIColor HealthBarColorHigh = new GUIColor("HealthBarColorHigh"); - public Color EquipmentIndicatorNotEquipped { get; private set; } = Color.Gray; - public Color EquipmentIndicatorEquipped { get; private set; } = new Color(105, 202, 125); - public Color EquipmentIndicatorRunningOut { get; private set; } = new Color(202, 105, 105); + 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); - public GUIStyle(XElement element, GraphicsDevice graphicsDevice) + public static GUIComponentStyle GetComponentStyle(string name) + => ComponentStyles.ContainsKey(name) ? ComponentStyles[name] : null; + + public static void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) { - this.graphicsDevice = graphicsDevice; - componentStyles = new Dictionary(); - configElement = element; - foreach (XElement subElement in configElement.Elements()) - { - var name = subElement.Name.ToString().ToLowerInvariant(); - switch (name) - { - case "cursor": - if (subElement.HasElements) - { - foreach (var children in subElement.Descendants()) - { - var index = children.GetAttributeInt("state", (int)CursorState.Default); - CursorSprite[index] = new Sprite(children); - } - } - else - { - CursorSprite[(int)CursorState.Default] = new Sprite(subElement); - } - break; - case "green": - Green = subElement.GetAttributeColor("color", Green); - break; - case "orange": - Orange = subElement.GetAttributeColor("color", Orange); - break; - case "red": - Red = subElement.GetAttributeColor("color", Red); - break; - case "blue": - Blue = subElement.GetAttributeColor("color", Blue); - break; - case "yellow": - Yellow = subElement.GetAttributeColor("color", Yellow); - break; - case "colorinventoryempty": - ColorInventoryEmpty = subElement.GetAttributeColor("color", ColorInventoryEmpty); - break; - case "colorinventoryhalf": - ColorInventoryHalf = subElement.GetAttributeColor("color", ColorInventoryHalf); - break; - case "colorinventoryfull": - ColorInventoryFull = subElement.GetAttributeColor("color", ColorInventoryFull); - break; - case "colorinventorybackground": - ColorInventoryBackground = subElement.GetAttributeColor("color", ColorInventoryBackground); - break; - case "colorinventoryemptyoverlay": - ColorInventoryEmptyOverlay = subElement.GetAttributeColor("color", ColorInventoryEmptyOverlay); - break; - case "textcolordark": - TextColorDark = subElement.GetAttributeColor("color", TextColorDark); - break; - case "textcolorbright": - TextColorBright = subElement.GetAttributeColor("color", TextColorBright); - break; - case "textcolordim": - TextColorDim = subElement.GetAttributeColor("color", TextColorDim); - break; - case "textcolornormal": - case "textcolor": - TextColor = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationverylow": - ColorReputationVeryLow = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationlow": - ColorReputationLow = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationneutral": - ColorReputationNeutral = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationhigh": - ColorReputationHigh = subElement.GetAttributeColor("color", TextColor); - break; - case "colorreputationveryhigh": - ColorReputationVeryHigh = subElement.GetAttributeColor("color", TextColor); - break; - case "equipmentsloticoncolor": - EquipmentSlotIconColor = subElement.GetAttributeColor("color", EquipmentSlotIconColor); - break; - case "buffcolorlow": - BuffColorLow = subElement.GetAttributeColor("color", BuffColorLow); - break; - case "buffcolormedium": - BuffColorMedium = subElement.GetAttributeColor("color", BuffColorMedium); - break; - case "buffcolorhigh": - BuffColorHigh = subElement.GetAttributeColor("color", BuffColorHigh); - break; - case "debuffcolorlow": - DebuffColorLow = subElement.GetAttributeColor("color", DebuffColorLow); - break; - case "debuffcolormedium": - DebuffColorMedium = subElement.GetAttributeColor("color", DebuffColorMedium); - break; - case "debuffcolorhigh": - DebuffColorHigh = subElement.GetAttributeColor("color", DebuffColorHigh); - break; - case "healthbarcolorlow": - HealthBarColorLow = subElement.GetAttributeColor("color", HealthBarColorLow); - break; - case "healthbarcolormedium": - HealthBarColorMedium = subElement.GetAttributeColor("color", HealthBarColorMedium); - break; - case "healthbarcolorhigh": - HealthBarColorHigh = subElement.GetAttributeColor("color", HealthBarColorHigh); - break; - case "equipmentindicatornotequipped": - EquipmentIndicatorNotEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorNotEquipped); - break; - case "equipmentindicatorequipped": - EquipmentIndicatorEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorEquipped); - break; - case "equipmentindicatorrunningout": - EquipmentIndicatorRunningOut = subElement.GetAttributeColor("color", EquipmentIndicatorRunningOut); - break; - case "uiglow": - UIGlow = new UISprite(subElement); - break; - case "pingcircle": - PingCircle = new UISprite(subElement); - break; - case "youareherecircle": - YouAreHereCircle = new UISprite(subElement); - break; - case "radiation": - RadiationSprite = new UISprite(subElement); - break; - case "radiationanimspritesheet": - RadiationAnimSpriteSheet = new SpriteSheet(subElement); - break; - case "uiglowcircular": - UIGlowCircular = new UISprite(subElement); - break; - case "uiglowsolidcircular": - UIGlowSolidCircular = new UISprite(subElement); - break; - case "uithermalglow": - UIThermalGlow = new UISprite(subElement); - break; - case "endroundbuttonpulse": - ButtonPulse = new UISprite(subElement); - break; - case "iconoverflowindicator": - IconOverflowIndicator = new UISprite(subElement); - break; - case "focusindicator": - FocusIndicator = new SpriteSheet(subElement); - break; - case "savingindicator": - SavingIndicator = new SpriteSheet(subElement); - break; - case "font": - Font = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[Font] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "globalfont": - GlobalFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[GlobalFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "unscaledsmallfont": - UnscaledSmallFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[UnscaledSmallFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "smallfont": - SmallFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[SmallFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "largefont": - LargeFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[LargeFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "digitalfont": - DigitalFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[DigitalFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "monospacedfont": - MonospacedFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[MonospacedFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "hotkeyfont": - HotkeyFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[HotkeyFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - case "objectivetitle": - case "subheading": - SubHeadingFont = LoadFont(subElement, graphicsDevice); - ForceFontUpperCase[SubHeadingFont] = subElement.GetAttributeBool("forceuppercase", false); - break; - default: - GUIComponentStyle componentStyle = new GUIComponentStyle(subElement, this); - componentStyles.Add(subElement.Name.ToString().ToLowerInvariant(), componentStyle); - break; - } - } - - if (GlobalFont == null) - { - GlobalFont = Font; - DebugConsole.NewMessage("Global font not defined in the current UI style file. The global font is used to render western symbols when using Chinese/Japanese/Korean localization. Using default font instead...", Color.Orange); - } - - // TODO: Needs to unregister if we ever remove GUIStyles. - GameMain.Instance.ResolutionChanged += RescaleElements; + Apply(targetComponent, styleName.ToIdentifier(), parent); } - /// - /// Returns the default font of the currently selected language - /// - public ScalableFont LoadCurrentDefaultFont() - { - defaultFont?.Dispose(); - defaultFont = null; - foreach (XElement subElement in configElement.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "font": - defaultFont = LoadFont(subElement, graphicsDevice); - break; - } - } - return defaultFont; - } - - - private void RescaleElements() - { - if (configElement == null) { return; } - if (configElement.Elements() == null) { return; } - foreach (XElement subElement in configElement.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "font": - if (Font == null) { continue; } - Font.Size = GetFontSize(subElement); - break; - case "smallfont": - if (SmallFont == null) { continue; } - SmallFont.Size = GetFontSize(subElement); - break; - case "largefont": - if (LargeFont == null) { continue; } - LargeFont.Size = GetFontSize(subElement); - break; - case "hotkeyfont": - if (HotkeyFont == null) { continue; } - HotkeyFont.Size = GetFontSize(subElement); - break; - case "objectivetitle": - case "subheading": - if (SubHeadingFont == null) { continue; } - SubHeadingFont.Size = GetFontSize(subElement); - break; - } - } - - foreach (var componentStyle in componentStyles.Values) - { - componentStyle.GetSize(componentStyle.Element); - foreach (var childStyle in componentStyle.ChildStyles.Values) - { - childStyle.GetSize(childStyle.Element); - } - } - } - - private ScalableFont LoadFont(XElement element, GraphicsDevice graphicsDevice) - { - string file = GetFontFilePath(element); - uint size = GetFontSize(element); - bool dynamicLoading = GetFontDynamicLoading(element); - bool isCJK = GetIsCJK(element); - return new ScalableFont(file, size, graphicsDevice, dynamicLoading, isCJK); - } - - private uint GetFontSize(XElement element, uint defaultSize = 14) - { - //check if any of the language override fonts want to override the font size as well - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - uint overrideFontSize = GetFontSize(subElement, 0); - if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.TextScale); } - } - } - - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } - Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); - if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) - { - return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.TextScale); - } - } - return (uint)Math.Round(defaultSize * GameSettings.TextScale); - } - - private string GetFontFilePath(XElement element) - { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - return subElement.GetAttributeString("file", ""); - } - } - return element.GetAttributeString("file", ""); - } - - private bool GetFontDynamicLoading(XElement element) - { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - return subElement.GetAttributeBool("dynamicloading", false); - } - } - return element.GetAttributeBool("dynamicloading", false); - } - - private bool GetIsCJK(XElement element) - { - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } - if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) - { - return subElement.GetAttributeBool("iscjk", false); - } - } - return element.GetAttributeBool("iscjk", false); - } - - public GUIComponentStyle GetComponentStyle(string name) - { - componentStyles.TryGetValue(name.ToLowerInvariant(), out GUIComponentStyle style); - return style; - } - - public void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null) + public static void Apply(GUIComponent targetComponent, Identifier styleName, GUIComponent parent = null) { GUIComponentStyle componentStyle = null; if (parent != null) { GUIComponentStyle parentStyle = parent.Style; - if (parent.Style == null) + if (parentStyle == null) { - string parentStyleName = parent.GetType().Name.ToLowerInvariant(); + Identifier parentStyleName = parent.GetType().Name.ToIdentifier(); - if (!componentStyles.TryGetValue(parentStyleName, out parentStyle)) + if (!ComponentStyles.ContainsKey(parentStyleName)) { - DebugConsole.ThrowError("Couldn't find a GUI style \""+ parentStyleName + "\""); + DebugConsole.ThrowError($"Couldn't find a GUI style \"{parentStyleName}\""); return; } + parentStyle = ComponentStyles[parentStyleName]; } - - string childStyleName = string.IsNullOrEmpty(styleName) ? targetComponent.GetType().Name : styleName; - parentStyle.ChildStyles.TryGetValue(childStyleName.ToLowerInvariant(), out componentStyle); + Identifier childStyleName = styleName.IsEmpty ? targetComponent.GetType().Name.ToIdentifier() : styleName; + parentStyle.ChildStyles.TryGetValue(childStyleName, out componentStyle); } else { - if (string.IsNullOrEmpty(styleName)) + Identifier styleIdentifier = styleName.ToIdentifier(); + if (styleIdentifier == Identifier.Empty) { - styleName = targetComponent.GetType().Name; + styleIdentifier = targetComponent.GetType().Name.ToIdentifier(); } - if (!componentStyles.TryGetValue(styleName.ToLowerInvariant(), out componentStyle)) + if (!ComponentStyles.ContainsKey(styleIdentifier)) { - DebugConsole.ThrowError("Couldn't find a GUI style \""+ styleName+"\""); + DebugConsole.ThrowError($"Couldn't find a GUI style \"{styleIdentifier}\""); return; } + componentStyle = ComponentStyles[styleIdentifier]; } targetComponent.ApplyStyle(componentStyle); } - public Color GetQualityColor(int quality) + public static GUIColor GetQualityColor(int quality) { switch (quality) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 031ec550b..f295fdc0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -7,9 +8,16 @@ using System.Linq; namespace Barotrauma { + public enum ForceUpperCase + { + Inherit, + No, + Yes + } + public class GUITextBlock : GUIComponent { - protected string text; + protected RichString text; protected Alignment textAlignment; @@ -20,10 +28,10 @@ namespace Barotrauma protected Color textColor, disabledTextColor, selectedTextColor; - private string wrappedText; + private LocalizedString wrappedText; private string censoredText; - public delegate string TextGetterHandler(); + public delegate LocalizedString TextGetterHandler(); public TextGetterHandler TextGetter; public bool Wrap; @@ -41,8 +49,6 @@ namespace Barotrauma private float textDepth; - private ScalableFont originalFont; - public Vector2 TextOffset { get; set; } private Vector4 padding; @@ -56,7 +62,7 @@ namespace Barotrauma } } - public override ScalableFont Font + public override GUIFont Font { get { @@ -65,23 +71,25 @@ namespace Barotrauma set { if (base.Font == value) { return; } - base.Font = originalFont = value; - if (text != null && GUI.Style.ForceFontUpperCase.ContainsKey(Font) && GUI.Style.ForceFontUpperCase[Font]) - { - Text = text.ToUpper(); - } + base.Font = value; + if (text != null) { Text = text; } SetTextPos(); } } - public string Text + public RichString Text { get { return text; } set { - string newText = forceUpperCase || (GUI.Style.ForceFontUpperCase.ContainsKey(Font) && GUI.Style.ForceFontUpperCase[Font]) || (style != null && style.ForceUpperCase) ? - value?.ToUpper() : - value; + #warning TODO: Remove this eventually. Nobody should want to pass null. + value ??= ""; + RichString newText = forceUpperCase switch + { + ForceUpperCase.Inherit => value.CaseTiedToFontAndStyle(Font, Style), + ForceUpperCase.No => value.CaseTiedToFontAndStyle(null, null), + ForceUpperCase.Yes => value.ToUpper() + }; if (Text == newText) { return; } @@ -89,21 +97,12 @@ namespace Barotrauma if (autoScaleHorizontal || autoScaleVertical) { textScale = 1.0f; } text = newText; - wrappedText = newText; - if (TextManager.IsCJK(text)) - { - //switch to fallback CJK font - if (!Font.IsCJK) { base.Font = GUI.CJKFont; } - } - else - { - if (Font == GUI.CJKFont) { base.Font = originalFont; } - } + wrappedText = newText.SanitizedString; SetTextPos(); } } - public string WrappedText + public LocalizedString WrappedText { get { return wrappedText; } } @@ -117,7 +116,11 @@ namespace Barotrauma public Vector2 TextPos { get { return textPos; } - set { textPos = value; } + set + { + textPos = value; + ClearCaretPositions(); + } } public float TextScale @@ -169,8 +172,8 @@ namespace Barotrauma } } - private bool forceUpperCase; - public bool ForceUpperCase + private ForceUpperCase forceUpperCase = ForceUpperCase.Inherit; + public ForceUpperCase ForceUpperCase { get { return forceUpperCase; } set @@ -178,12 +181,7 @@ namespace Barotrauma if (forceUpperCase == value) { return; } forceUpperCase = value; - if (forceUpperCase || - (style != null && style.ForceUpperCase) || - (GUI.Style.ForceFontUpperCase.ContainsKey(Font) && GUI.Style.ForceFontUpperCase[Font])) - { - Text = text?.ToUpper(); - } + if (text != null) { Text = text; } } } @@ -247,7 +245,7 @@ namespace Barotrauma public class StrikethroughSettings { - public Color Color { get; set; } = GUI.Style.Red; + public Color Color { get; set; } = GUIStyle.Red; private int thickness; private int expand; @@ -266,13 +264,9 @@ namespace Barotrauma public StrikethroughSettings Strikethrough = null; - public List RichTextData - { - get; - private set; - } + public ImmutableArray? RichTextData => text.RichTextData; - public bool HasColorHighlight => RichTextData != null; + public bool HasColorHighlight => RichTextData.HasValue; public bool OverrideRichTextDataAlpha = true; @@ -292,9 +286,9 @@ namespace Barotrauma /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. /// - public GUITextBlock(RectTransform rectT, string text, Color? textColor = null, ScalableFont font = null, + 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, bool parseRichText = false) + bool playerInput = false) : base(style, rectT) { if (color.HasValue) @@ -306,28 +300,15 @@ namespace Barotrauma OverrideTextColor(textColor.Value); } - if (parseRichText) - { - RichTextData = Barotrauma.RichTextData.GetRichTextData(text, out text); - if (RichTextData != null && RichTextData.Count == 0) - { - RichTextData = null; - } - } - //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, //use the default CJK font as a fallback - var selectedFont = originalFont = font ?? GUI.Font; - if (TextManager.IsCJK(text) && !selectedFont.IsCJK) - { - selectedFont = GUI.CJKFont; - } + var selectedFont = font ?? GUIStyle.Font; this.Font = selectedFont; this.textAlignment = textAlignment; this.Wrap = wrap; this.Text = text ?? ""; this.playerInput = playerInput; - if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) + if (rectT.Rect.Height == 0 && !text.IsNullOrEmpty()) { CalculateHeightFromText(); } @@ -339,11 +320,6 @@ namespace Barotrauma Enabled = true; Censor = false; } - public GUITextBlock(RectTransform rectT, List richTextData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) - : this(rectT, text, textColor, font, textAlignment, wrap, style, color, playerInput) - { - this.RichTextData = richTextData; - } public void CalculateHeightFromText(int padding = 0, bool removeExtraSpacing = false) { @@ -351,10 +327,9 @@ namespace Barotrauma RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(wrappedText, removeExtraSpacing).Y + padding)); } - public void SetRichText(string richText) + public void SetRichText(LocalizedString richText) { - RichTextData = Barotrauma.RichTextData.GetRichTextData(richText, out string sanitizedText); - Text = sanitizedText; + Text = RichString.Rich(richText); } public override void ApplyStyle(GUIComponentStyle componentStyle) @@ -368,41 +343,34 @@ namespace Barotrauma disabledTextColor = componentStyle.DisabledTextColor; selectedTextColor = componentStyle.SelectedTextColor; - switch (componentStyle.Font) + if (Font == null || !componentStyle.Font.IsEmpty) { - case "font": - Font = componentStyle.Style.Font; - break; - case "smallfont": - Font = componentStyle.Style.SmallFont; - break; - case "largefont": - Font = componentStyle.Style.LargeFont; - break; - case "objectivetitle": - case "subheading": - Font = componentStyle.Style.SubHeadingFont; - break; + Font = GUIStyle.Fonts[componentStyle.Font.AppendIfMissing("Font")]; } } + + public void ClearCaretPositions() + { + cachedCaretPositions = ImmutableArray.Empty; + } public void SetTextPos() { - cachedCaretPositions = ImmutableArray.Empty; + ClearCaretPositions(); if (text == null) { return; } - censoredText = string.IsNullOrEmpty(text) ? "" : new string('\u2022', text.Length); + censoredText = text.IsNullOrEmpty() ? "" : new string('\u2022', text.Length); var rect = Rect; overflowClipActive = false; - wrappedText = text; + wrappedText = text.SanitizedString; - TextSize = MeasureText(text); + TextSize = MeasureText(text.SanitizedString); if (Wrap && rect.Width > 0) { - wrappedText = ToolBox.WrapText(text, rect.Width - padding.X - padding.Z, Font, textScale); + wrappedText = ToolBox.WrapText(text.SanitizedString, rect.Width - padding.X - padding.Z, Font, textScale); TextSize = MeasureText(wrappedText); } else if (OverflowClip) @@ -426,15 +394,15 @@ namespace Barotrauma textPos = new Vector2(padding.X + (rect.Width - padding.Z - padding.X) / 2.0f, padding.Y + (rect.Height - padding.Y - padding.W) / 2.0f); origin = TextSize * 0.5f; + origin.X = 0; if (textAlignment.HasFlag(Alignment.Left) && !overflowClipActive) { textPos.X = padding.X; - origin.X = 0; } if (textAlignment.HasFlag(Alignment.Right) || overflowClipActive) { textPos.X = rect.Width - padding.Z; - origin.X = TextSize.X; + //origin.X = TextSize.X; } if (textAlignment.HasFlag(Alignment.Top)) { @@ -454,7 +422,12 @@ namespace Barotrauma textPos.Y = (int)textPos.Y; } - private Vector2 MeasureText(string text) + private Vector2 MeasureText(LocalizedString text) + { + return MeasureText(text.Value); + } + + private Vector2 MeasureText(string text) { if (Font == null) return Vector2.Zero; @@ -498,12 +471,20 @@ namespace Barotrauma { return cachedCaretPositions; } - string textDrawn = Censor ? CensoredText : Text; + string textDrawn = Censor ? CensoredText : Text.SanitizedValue; float w = Wrap ? (Rect.Width - Padding.X - Padding.Z) / TextScale : float.PositiveInfinity; - Font.WrapText(textDrawn, w, out Vector2[] positions); - cachedCaretPositions = positions.Select(p => p * TextScale + TextPos - Origin * TextScale).ToImmutableArray(); + string wrapped = Font.WrapText(textDrawn, w, out Vector2[] positions); + int textWidth = (int)Font.MeasureString(wrapped).X; + int alignmentXDiff + = textAlignment.HasFlag(Alignment.Right) ? textWidth + : textAlignment.HasFlag(Alignment.Center) ? textWidth / 2 + : 0; + cachedCaretPositions = positions + .Select(p => p - new Vector2(alignmentXDiff, 0)) + .Select(p => p * TextScale + TextPos - Origin * TextScale) + .ToImmutableArray(); return cachedCaretPositions; } @@ -584,7 +565,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - if (!string.IsNullOrEmpty(text)) + if (!text.IsNullOrEmpty()) { Vector2 pos = rect.Location.ToVector2() + textPos + TextOffset; if (RoundToNearestPixel) @@ -605,28 +586,29 @@ namespace Barotrauma if (!HasColorHighlight) { - string textToShow = Censor ? censoredText : (Wrap ? wrappedText : text); + string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue); Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f); if (Shadow) { Vector2 shadowOffset = new Vector2(GUI.IntScale(2)); - Font.DrawString(spriteBatch, textToShow, pos + shadowOffset, Color.Black, 0.0f, origin, TextScale, SpriteEffects.None, textDepth); + Font.DrawString(spriteBatch, textToShow, pos + shadowOffset, Color.Black, 0.0f, origin, TextScale, SpriteEffects.None, textDepth, alignment: textAlignment, forceUpperCase: ForceUpperCase); } - Font.DrawString(spriteBatch, textToShow, pos, colorToShow, 0.0f, origin, TextScale, SpriteEffects.None, textDepth); + Font.DrawString(spriteBatch, textToShow, pos, colorToShow, 0.0f, origin, TextScale, SpriteEffects.None, textDepth, alignment: textAlignment, forceUpperCase: ForceUpperCase); } else { if (OverrideRichTextDataAlpha) { - RichTextData.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); + RichTextData.Value.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f); } - Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text), pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData); + Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text.SanitizedString).Value, pos, + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData.Value, alignment: textAlignment, forceUpperCase: ForceUpperCase); } - Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, ForceUpperCase ? pos.Y : pos.Y + GUI.Scale * 2f); + Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, + /* TODO: ???? */ForceUpperCase == ForceUpperCase.Yes ? pos.Y : pos.Y + GUI.Scale * 2f); } if (overflowClipActive) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 147a24e37..7a096a1a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -66,9 +66,6 @@ namespace Barotrauma private int selectionStartIndex; private int selectionEndIndex; private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex; - private Vector2 selectionStartPos; - private Vector2 selectionEndPos; - private Vector2 selectionRectSize; private GUICustomComponent caretAndSelectionRenderer; @@ -141,7 +138,7 @@ namespace Barotrauma maxTextLength = value; if (Text.Length > MaxTextLength) { - SetText(textBlock.Text.Substring(0, (int)maxTextLength)); + SetText(Text.Substring(0, (int)maxTextLength)); } } } @@ -172,7 +169,7 @@ namespace Barotrauma set { textBlock.Censor = value; } } - public override string ToolTip + public override RichString ToolTip { get { @@ -184,7 +181,7 @@ namespace Barotrauma } } - public override ScalableFont Font + public override GUIFont Font { get { return textBlock?.Font ?? base.Font; } set @@ -237,7 +234,7 @@ namespace Barotrauma { get { - return textBlock.Text; + return textBlock.Text.SanitizedValue; } set { @@ -249,12 +246,12 @@ namespace Barotrauma public string WrappedText { - get { return textBlock.WrappedText; } + get { return textBlock.WrappedText.Value; } } public bool Readonly { get; set; } - public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, ScalableFont font = null, + 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) { @@ -263,9 +260,10 @@ namespace Barotrauma this.color = color ?? Color.White; frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); - GUI.Style.Apply(frame, style == "" ? "GUITextBox" : style); - textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text, textColor, font, textAlignment, wrap, playerInput: true); - GUI.Style.Apply(textBlock, "", this); + GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style); + textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap, playerInput: true); + GUIStyle.Apply(textBlock, "", this); + if (font != null) { textBlock.Font = font; } CaretEnabled = true; caretPosDirty = true; @@ -287,10 +285,11 @@ namespace Barotrauma clearButtonWidth = (int)(clearButton.Rect.Width * 1.2f); } - if (this.style != null && this.style.ChildStyles.ContainsKey("textboxicon") && createPenIcon) + var selfStyle = Style; + if (selfStyle != null && selfStyle.ChildStyles.ContainsKey("textboxicon".ToIdentifier()) && createPenIcon) { icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5 + clearButtonWidth, 0) }, null, scaleToFit: true); - icon.ApplyStyle(this.style.ChildStyles["textboxicon"]); + icon.ApplyStyle(this.Style.ChildStyles["textboxicon".ToIdentifier()]); textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - clearButtonWidth - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); } Font = textBlock.Font; @@ -315,53 +314,38 @@ namespace Barotrauma { text = textFilterFunction(text); } - if (textBlock.Text == text) { return false; } + if (Text == text) { return false; } textBlock.Text = text; - if (textBlock.Text == null) textBlock.Text = ""; - if (textBlock.Text != "" && !Wrap) + if (Text == null) textBlock.Text = ""; + if (Text != "" && !Wrap) { if (maxTextLength != null) { if (textBlock.Text.Length > maxTextLength) { - textBlock.Text = textBlock.Text.Substring(0, (int)maxTextLength); + textBlock.Text = Text.Substring(0, (int)maxTextLength); } } else { while (ClampText && textBlock.Text.Length > 0 && Font.MeasureString(textBlock.Text).X * TextBlock.TextScale > (int)(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z)) { - textBlock.Text = textBlock.Text.Substring(0, textBlock.Text.Length - 1); + textBlock.Text = Text.Substring(0, textBlock.Text.Length - 1); } } } if (store) { - memento.Store(textBlock.Text); + memento.Store(Text); } return true; } private void CalculateCaretPos() { - if (Censor || !Wrap) - { - string textDrawn = textBlock.CensoredText; - CaretIndex = Math.Min(CaretIndex, textDrawn.Length); - textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; - Vector2 textSize = Font.MeasureString(textDrawn[..CaretIndex]) * TextBlock.TextScale; - caretPos = new Vector2(textSize.X, 0) + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; - } - else - { - CaretIndex = Math.Min(CaretIndex, textBlock.Text.Length); - textBlock.Font.WrapText( - textBlock.Text, - (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, - CaretIndex, - out Vector2 requestedCharPos); - caretPos = requestedCharPos * TextBlock.TextScale + textBlock.TextPos - textBlock.Origin * TextBlock.TextScale; - } + CaretIndex = Math.Clamp(CaretIndex, 0, textBlock.Text.Length); + var caretPositions = textBlock.GetAllCaretPositions(); + caretPos = caretPositions[CaretIndex]; caretPosDirty = false; } @@ -460,14 +444,19 @@ namespace Barotrauma { if (textBlock.OverflowClipActive) { - if (CaretScreenPos.X < textBlock.Rect.X + textBlock.Padding.X) + float left = textBlock.Rect.X + textBlock.Padding.X; + if (CaretScreenPos.X < left) { - textBlock.TextPos = new Vector2(textBlock.TextPos.X + ((textBlock.Rect.X + textBlock.Padding.X) - CaretScreenPos.X), textBlock.TextPos.Y); + float diff = left - CaretScreenPos.X; + textBlock.TextPos = new Vector2(textBlock.TextPos.X + diff, textBlock.TextPos.Y); CalculateCaretPos(); } - else if (CaretScreenPos.X > textBlock.Rect.Right - textBlock.Padding.Z) + + float right = textBlock.Rect.Right - textBlock.Padding.Z; + if (CaretScreenPos.X > right) { - textBlock.TextPos = new Vector2(textBlock.TextPos.X - (CaretScreenPos.X - (textBlock.Rect.Right - textBlock.Padding.Z)), textBlock.TextPos.Y); + float diff = CaretScreenPos.X - right; + textBlock.TextPos = new Vector2(textBlock.TextPos.X - diff, textBlock.TextPos.Y); CalculateCaretPos(); } } @@ -499,74 +488,54 @@ namespace Barotrauma private void DrawCaretAndSelection(SpriteBatch spriteBatch, GUICustomComponent customComponent) { if (!Visible) { return; } - if (Selected) + if (!Selected) { return; } + + if (caretVisible) { - if (caretVisible ) - { - GUI.DrawLine(spriteBatch, - new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + 3), - new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + Font.MeasureString("I").Y * textBlock.TextScale - 3), - CaretColor ?? textBlock.TextColor * (textBlock.TextColor.A / 255.0f)); - } - if (selectedCharacters > 0) - { - DrawSelectionRect(spriteBatch); - } - //GUI.DrawString(spriteBatch, new Vector2(100, 0), selectedCharacters.ToString(), Color.LightBlue, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 20), selectionStartIndex.ToString(), Color.White, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(140, 20), selectionEndIndex.ToString(), Color.White, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 40), selectedText.ToString(), Color.Yellow, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 60), $"caret index: {CaretIndex.ToString()}", GUI.Style.Red, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 80), $"caret pos: {caretPos.ToString()}", GUI.Style.Red, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 100), $"caret screen pos: {CaretScreenPos.ToString()}", GUI.Style.Red, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 120), $"text start pos: {(textBlock.TextPos - textBlock.Origin).ToString()}", Color.White, Color.Black); - //GUI.DrawString(spriteBatch, new Vector2(100, 140), $"cursor pos: {PlayerInput.MousePosition.ToString()}", Color.White, Color.Black); + GUI.DrawLine(spriteBatch, + new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + 3), + new Vector2(Rect.X + (int)caretPos.X + 2, Rect.Y + caretPos.Y + Font.LineHeight * textBlock.TextScale - 3), + CaretColor ?? textBlock.TextColor * (textBlock.TextColor.A / 255.0f)); + } + if (selectedCharacters > 0) + { + DrawSelectionRect(spriteBatch); } } private void DrawSelectionRect(SpriteBatch spriteBatch) { - if (textBlock.WrappedText.Contains("\n")) - { - // Multiline selection - var characterPositions = textBlock.GetAllCaretPositions(); - (int startIndex, int endIndex) = selectionStartIndex < selectionEndIndex - ? (selectionStartIndex, selectionEndIndex) - : (selectionEndIndex, selectionStartIndex); - endIndex--; + var characterPositions = textBlock.GetAllCaretPositions(); + (int startIndex, int endIndex) = IsLeftToRight + ? (selectionStartIndex, selectionEndIndex) + : (selectionEndIndex, selectionStartIndex); + endIndex--; - void drawRect(Vector2 topLeft, Vector2 bottomRight) - { - int minWidth = GUI.IntScale(5); - if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; } - GUI.DrawRectangle(spriteBatch, - Rect.Location.ToVector2() + topLeft, - bottomRight - topLeft, - SelectionColor, isFilled: true); - } - - Vector2 topLeft = characterPositions[startIndex]; - for (int i = startIndex+1; i <= endIndex; i++) - { - Vector2 currPos = characterPositions[i]; - if (!MathUtils.NearlyEqual(topLeft.Y, currPos.Y)) - { - Vector2 bottomRight = characterPositions[i - 1]; - bottomRight += Font.MeasureChar(Text[i - 1]); - drawRect(topLeft, bottomRight); - topLeft = currPos; - } - } - Vector2 finalBottomRight = characterPositions[endIndex]; - finalBottomRight += Font.MeasureChar(Text[endIndex]); - drawRect(topLeft, finalBottomRight); - } - else + void drawRect(Vector2 topLeft, Vector2 bottomRight) { - // Single line selection - Vector2 topLeft = IsLeftToRight ? selectionStartPos : selectionEndPos; - GUI.DrawRectangle(spriteBatch, Rect.Location.ToVector2() + topLeft, selectionRectSize, SelectionColor, isFilled: true); + int minWidth = GUI.IntScale(5); + if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; } + GUI.DrawRectangle(spriteBatch, + Rect.Location.ToVector2() + topLeft, + bottomRight - topLeft, + SelectionColor, isFilled: true); } + + Vector2 topLeft = characterPositions[startIndex]; + for (int i = startIndex+1; i <= endIndex; i++) + { + Vector2 currPos = characterPositions[i]; + if (!MathUtils.NearlyEqual(topLeft.Y, currPos.Y)) + { + Vector2 bottomRight = characterPositions[i - 1]; + bottomRight += Font.MeasureChar(Text[i - 1]) * TextBlock.TextScale; + drawRect(topLeft, bottomRight); + topLeft = currPos; + } + } + Vector2 finalBottomRight = characterPositions[endIndex]; + finalBottomRight += Font.MeasureChar(Text[endIndex]) * TextBlock.TextScale; + drawRect(topLeft, finalBottomRight); } public void ReceiveTextInput(char inputChar) @@ -700,8 +669,8 @@ namespace Barotrauma float lineHeight = Font.LineHeight * TextBlock.TextScale; int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y - lineHeight * 0.5f)); textBlock.Font.WrapText( - textBlock.Text, - (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + textBlock.Text.SanitizedValue, + GetWrapWidth(), newIndex, out Vector2 requestedCharPos); requestedCharPos *= TextBlock.TextScale; @@ -718,8 +687,8 @@ namespace Barotrauma lineHeight = Font.LineHeight * TextBlock.TextScale; newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y + lineHeight * 1.5f)); textBlock.Font.WrapText( - textBlock.Text, - (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale, + textBlock.Text.SanitizedValue, + GetWrapWidth(), newIndex, out Vector2 requestedCharPos2); requestedCharPos2 *= TextBlock.TextScale; @@ -806,7 +775,6 @@ namespace Barotrauma { CaretIndex = 0; CalculateCaretPos(); - selectionStartPos = caretPos; selectionStartIndex = 0; CaretIndex = Text.Length; CalculateSelection(); @@ -846,6 +814,9 @@ namespace Barotrauma OnTextChanged?.Invoke(this, Text); } + private float GetWrapWidth() + => Wrap ? (textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z) / TextBlock.TextScale : float.PositiveInfinity; + private void InitSelectionStart() { if (caretPosDirty) @@ -855,29 +826,20 @@ namespace Barotrauma if (selectionStartIndex == -1) { selectionStartIndex = CaretIndex; - selectionStartPos = caretPos; } } private void CalculateSelection() { - string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; + string textDrawn = Censor ? textBlock.CensoredText : WrappedText; InitSelectionStart(); selectionEndIndex = Math.Min(CaretIndex, textDrawn.Length); - selectionEndPos = caretPos; selectedCharacters = Math.Abs(selectionStartIndex - selectionEndIndex); try { - if (IsLeftToRight) - { - selectedText = Text.Substring(selectionStartIndex, Math.Min(selectedCharacters, Text.Length)); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionStartIndex, Math.Min(selectedCharacters, textDrawn.Length))) * TextBlock.TextScale; - } - else - { - selectedText = Text.Substring(selectionEndIndex, Math.Min(selectedCharacters, Text.Length)); - selectionRectSize = Font.MeasureString(textDrawn.Substring(selectionEndIndex, Math.Min(selectedCharacters, textDrawn.Length))) * TextBlock.TextScale; - } + selectedText = Text.Substring( + IsLeftToRight ? selectionStartIndex : selectionEndIndex, + Math.Min(selectedCharacters, Text.Length)); } catch (ArgumentOutOfRangeException exception) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs index 2d589dc6b..05e59d5fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITickBox.cs @@ -20,18 +20,18 @@ namespace Barotrauma public override bool Selected { - get { return selected; } + get { return isSelected; } set { - if (value == selected) { return; } + if (value == isSelected) { return; } if (radioButtonGroup != null && radioButtonGroup.SelectedRadioButton == this) { - selected = true; + isSelected = true; return; } - selected = value; - State = selected ? ComponentState.Selected : ComponentState.None; + isSelected = value; + State = isSelected ? ComponentState.Selected : ComponentState.None; if (value && radioButtonGroup != null) { radioButtonGroup.SelectRadioButton(this); @@ -88,7 +88,7 @@ namespace Barotrauma } }*/ - public override ScalableFont Font + public override GUIFont Font { get { @@ -112,7 +112,7 @@ namespace Barotrauma get { return text; } } - public override string ToolTip + public override RichString ToolTip { get { return base.ToolTip; } set @@ -123,13 +123,13 @@ namespace Barotrauma } } - public string Text + public LocalizedString Text { get { return text.Text; } set { text.Text = value; } } - public GUITickBox(RectTransform rectT, string label, ScalableFont font = null, string style = "") : base(null, rectT) + public GUITickBox(RectTransform rectT, LocalizedString label, GUIFont font = null, string style = "") : base(null, rectT) { CanBeFocused = true; HoverCursor = CursorState.Hand; @@ -145,7 +145,7 @@ namespace Barotrauma SelectedColor = Color.DarkGray, CanBeFocused = false }; - GUI.Style.Apply(box, style == "" ? "GUITickBox" : style); + GUIStyle.Apply(box, style == "" ? "GUITickBox" : style); if (box.RectTransform.MinSize.Y > 0) { RectTransform.MinSize = box.RectTransform.MinSize; @@ -159,7 +159,7 @@ namespace Barotrauma { CanBeFocused = false }; - GUI.Style.Apply(text, "GUITextBlock", this); + GUIStyle.Apply(text, "GUITextBlock", this); Enabled = true; ResizeBox(); @@ -205,13 +205,13 @@ namespace Barotrauma { Selected = !Selected; } - else if (!selected) + else if (!isSelected) { Selected = true; } } } - else if (selected) + else if (isSelected) { State = ComponentState.Selected; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 69f18c269..ad3b9c4d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -90,7 +90,8 @@ namespace Barotrauma if (GameMain.Instance != null) { GameMain.Instance.ResolutionChanged += CreateAreas; - GameMain.Config.OnHUDScaleChanged += CreateAreas; + #warning TODO: reimplement + //GameSettings.CurrentConfig.OnHUDScaleChanged += CreateAreas; CreateAreas(); CharacterInfo.Init(); } @@ -163,7 +164,7 @@ namespace Barotrauma public static void Draw(SpriteBatch spriteBatch) { GUI.DrawRectangle(spriteBatch, ButtonAreaTop, Color.White * 0.5f); - GUI.DrawRectangle(spriteBatch, MessageAreaTop, GUI.Style.Orange * 0.5f); + GUI.DrawRectangle(spriteBatch, MessageAreaTop, GUIStyle.Orange * 0.5f); GUI.DrawRectangle(spriteBatch, CrewArea, Color.Blue * 0.5f); GUI.DrawRectangle(spriteBatch, ChatBoxArea, Color.Cyan * 0.5f); GUI.DrawRectangle(spriteBatch, HealthBarArea, Color.Red * 0.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index 8d83781a8..35bcab726 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Barotrauma.Media; using System.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -69,14 +70,10 @@ namespace Barotrauma } } - private string selectedTip; - private List selectedTipRichTextData; - private bool selectedTipRichTextUnparsed; - private void SetSelectedTip(string tip) + private RichString selectedTip; + private void SetSelectedTip(LocalizedString tip) { - selectedTip = tip; - selectedTipRichTextData = null; - selectedTipRichTextUnparsed = true; + selectedTip = RichString.Rich(tip); } private readonly object loadMutex = new object(); @@ -113,6 +110,8 @@ namespace Barotrauma set; } + public LanguageIdentifier[] AvailableLanguages = null; + public LoadingScreen(GraphicsDevice graphics) { defaultBackgroundTexture = TextureLoader.FromFile("Content/Map/LocationPortraits/AlienRuins.png"); @@ -123,12 +122,12 @@ namespace Barotrauma overlay = TextureLoader.FromFile("Content/UI/LoadingScreenOverlay.png"); noiseSprite = new Sprite("Content/UI/noise.png", Vector2.Zero); DrawLoadingText = true; - SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); + SetSelectedTip(TextManager.Get("LoadingScreenTip")); } public void Draw(SpriteBatch spriteBatch, GraphicsDevice graphics, float deltaTime) { - if (GameMain.Config.EnableSplashScreen) + if (GameSettings.CurrentConfig.EnableSplashScreen) { try { @@ -138,11 +137,11 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Playing splash screen video failed", e); - GameMain.Config.EnableSplashScreen = false; + DisableSplashScreen(); } } - - var titleStyle = GUI.Style?.GetComponentStyle("TitleText"); + + var titleStyle = GUIStyle.GetComponentStyle("TitleText"); Sprite titleSprite = null; if (!WaitForLanguageSelection && titleStyle != null && titleStyle.Sprites.ContainsKey(GUIComponent.ComponentState.None)) { @@ -187,67 +186,58 @@ namespace Barotrauma } else if (DrawLoadingText) { - if (TextManager.Initialized) + LocalizedString loadText; + if (LoadState == 100.0f) { - string loadText; - if (LoadState == 100.0f) +#if DEBUG + if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || (GameSettings.CurrentConfig.TestScreenEnabled && GameMain.FirstLoad)) { -#if DEBUG - if (GameMain.Config.AutomaticQuickStartEnabled || GameMain.Config.AutomaticCampaignLoadEnabled || GameMain.Config.TestScreenEnabled && GameMain.FirstLoad) - { - loadText = "QUICKSTARTING ..."; - } - else - { -#endif - loadText = TextManager.Get("PressAnyKey"); -#if DEBUG - } -#endif + loadText = "QUICKSTARTING ..."; } else { - loadText = TextManager.Get("Loading"); - if (LoadState != null) - { - loadText += " " + (int)LoadState + " %"; +#endif + loadText = TextManager.Get("PressAnyKey"); +#if DEBUG + } +#endif + } + else + { + loadText = TextManager.Get("Loading"); + if (LoadState != null) + { + loadText += " " + (int)LoadState + " %"; #if DEBUG - if (GameMain.FirstLoad && GameMain.CancelQuickStart) - { - loadText += " (Quickstart aborted)"; - } -#endif + if (GameMain.FirstLoad && GameMain.CancelQuickStart) + { + loadText += " (Quickstart aborted)"; } - } - if (GUI.LargeFont != null) - { - GUI.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), - new Vector2(GameMain.GraphicsWidth / 2.0f - GUI.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), - Color.White); +#endif } } - - if (GUI.Font != null && selectedTip != null) + if (GUIStyle.LargeFont.HasValue) { - if (selectedTipRichTextUnparsed) - { - selectedTipRichTextData = RichTextData.GetRichTextData(selectedTip, out selectedTip); - selectedTipRichTextUnparsed = false; - } + GUIStyle.LargeFont.DrawString(spriteBatch, loadText.ToUpper(), + new Vector2(GameMain.GraphicsWidth / 2.0f - GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).X / 2.0f, GameMain.GraphicsHeight * 0.75f), + Color.White); + } - string wrappedTip = ToolBox.WrapText(selectedTip, GameMain.GraphicsWidth * 0.5f, GUI.Font); + if (GUIStyle.Font.HasValue && selectedTip != null) + { + string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.5f, GUIStyle.Font.Value); string[] lines = wrappedTip.Split('\n'); - float lineHeight = GUI.Font.MeasureString(selectedTip).Y; + float lineHeight = GUIStyle.Font.MeasureString(selectedTip).Y; - if (selectedTipRichTextData != null) + if (selectedTip.RichTextData != null) { int rtdOffset = 0; for (int i = 0; i < lines.Length; i++) { - GUI.Font.DrawStringWithColors(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, - 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTipRichTextData, rtdOffset); + GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i], + new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White, + 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTip.RichTextData.Value, rtdOffset); rtdOffset += lines[i].Length; } } @@ -255,8 +245,8 @@ namespace Barotrauma { for (int i = 0; i < lines.Length; i++) { - GUI.Font.DrawString(spriteBatch, lines[i], - new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUI.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); + GUIStyle.Font.DrawString(spriteBatch, lines[i], + new Vector2((int)(GameMain.GraphicsWidth / 2.0f - GUIStyle.Font.MeasureString(lines[i]).X / 2.0f), (int)(GameMain.GraphicsHeight * 0.8f + i * lineHeight)), Color.White); } } } @@ -280,7 +270,7 @@ namespace Barotrauma if (noiseVal < 0.2f) { //SCP-CB reference - randText = (new string[] { "NIL", "black white gray", "Sometimes we would have had time to scream", "e8m106]af", "NO" }).GetRandom(); + randText = (new string[] { "NIL", "black white gray", "Sometimes we would have had time to scream", "e8m106]af", "NO" }).GetRandomUnsynced(); } else if (noiseVal < 0.3f) { @@ -295,15 +285,20 @@ namespace Barotrauma Rand.Int(100).ToString().PadLeft(2, '0'); } - GUI.LargeFont?.DrawString(spriteBatch, randText, - new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), - Color.White * (1.0f - noiseVal)); + if (GUIStyle.LargeFont.HasValue) + { + GUIStyle.LargeFont.DrawString(spriteBatch, randText, + new Vector2(GameMain.GraphicsWidth - decorativeMap.FrameSize.X * decorativeScale.X * 0.8f, GameMain.GraphicsHeight * 0.57f), + Color.White * (1.0f - noiseVal)); + } spriteBatch.End(); } private void DrawLanguageSelectionPrompt(SpriteBatch spriteBatch, GraphicsDevice graphicsDevice) { + if (AvailableLanguages is null) { return; } + if (languageSelectionFont == null) { languageSelectionFont = new ScalableFont("Content/Fonts/NotoSans/NotoSans-Bold.ttf", @@ -320,8 +315,8 @@ namespace Barotrauma } Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.3f); - Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / TextManager.AvailableLanguages.Count()); - foreach (string language in TextManager.AvailableLanguages) + Vector2 textSpacing = new Vector2(0.0f, (GameMain.GraphicsHeight * 0.5f) / AvailableLanguages.Length); + foreach (LanguageIdentifier language in AvailableLanguages) { string localizedLanguageName = TextManager.GetTranslatedLanguageName(language); var font = TextManager.IsCJK(localizedLanguageName) ? languageSelectionFontCJK : languageSelectionFont; @@ -335,11 +330,11 @@ namespace Barotrauma hover ? Color.White : Color.White * 0.6f); if (hover && PlayerInput.PrimaryMouseButtonClicked()) { - GameMain.Config.Language = language; + var config = GameSettings.CurrentConfig; + config.Language = language; + GameSettings.SetCurrentConfig(config); //reload tip in the selected language - SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); - GameMain.Config.SetDefaultBindings(legacy: false); - GameMain.Config.CheckBindings(useDefaults: true); + SetSelectedTip(TextManager.Get("LoadingScreenTip")); WaitForLanguageSelection = false; languageSelectionFont?.Dispose(); languageSelectionFont = null; languageSelectionFontCJK?.Dispose(); languageSelectionFontCJK = null; @@ -368,7 +363,7 @@ namespace Barotrauma } catch (Exception e) { - GameMain.Config.EnableSplashScreen = false; + DisableSplashScreen(); DebugConsole.ThrowError("Playing the splash screen \"" + fileName + "\" failed.", e); PendingSplashScreens.Clear(); currSplashScreen = null; @@ -425,13 +420,20 @@ namespace Barotrauma } } + private void DisableSplashScreen() + { + var config = GameSettings.CurrentConfig; + config.EnableSplashScreen = false; + GameSettings.SetCurrentConfig(config); + } + bool drawn; public IEnumerable DoLoading(IEnumerable loader) { drawn = false; LoadState = null; - SetSelectedTip(TextManager.Get("LoadingScreenTip", true)); - currentBackgroundTexture = LocationType.List.GetRandom()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; + SetSelectedTip(TextManager.Get("LoadingScreenTip")); + currentBackgroundTexture = LocationType.Prefabs.GetRandomUnsynced()?.GetPortrait(Rand.Int(int.MaxValue))?.Texture; while (!drawn) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 4596b3c7e..fb87b3639 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -50,7 +50,7 @@ namespace Barotrauma Afflictions = new List(); } - public PendingAfflictionElement? FindAfflictionElement(MedicalClinic.NetAffliction target) => Afflictions.FirstOrNull(element => element.Target.Identifier.Equals(target.Identifier, StringComparison.OrdinalIgnoreCase)); + public PendingAfflictionElement? FindAfflictionElement(MedicalClinic.NetAffliction target) => Afflictions.FirstOrNull(element => element.Target.Identifier == target.Identifier); } // Represents an affliction on the left side crew entry @@ -269,11 +269,11 @@ namespace Barotrauma int totalCost = medicalClinic.GetTotalCost(); healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); - healList.PriceBlock.TextColor = GUI.Style.Red; + healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; if (medicalClinic.GetMoney() > totalCost) { - healList.PriceBlock.TextColor = GUI.Style.TextColor; + healList.PriceBlock.TextColor = GUIStyle.TextColorNormal; if (medicalClinic.PendingHeals.Any()) { healList.HealButton.Enabled = true; @@ -443,7 +443,7 @@ namespace Barotrauma GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); - GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUI.LargeFont); + GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont); GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); @@ -459,13 +459,13 @@ namespace Barotrauma }; GUILayoutGroup balanceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), crewContent.RectTransform)); - GUITextBlock balanceLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), TextManager.Get("campaignstore.balance"), textAlignment: Alignment.BottomRight, font: GUI.Font) + GUITextBlock balanceLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), TextManager.Get("campaignstore.balance"), textAlignment: Alignment.BottomRight, font: GUIStyle.Font) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUI.Style.SubHeadingFont) + GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetMoney()), AutoScaleVertical = true, @@ -519,18 +519,18 @@ namespace Barotrauma GUILayoutGroup healthLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.1f, 1f), crewLayout.RectTransform), isHorizontal: true, Anchor.Center); - new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { TextGetter = () => $"{(int)(info.Character?.HealthPercentage ?? 100f)}%", - TextColor = GUI.Style.Green + TextColor = GUIStyle.Green }; GUITextBlock overflowIndicator = - new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), afflictionList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), text: "+", textAlignment: Alignment.Center, font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), afflictionList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), text: "+", textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { Visible = false, CanBeFocused = false, - TextColor = GUI.Style.Red + TextColor = GUIStyle.Red }; MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; @@ -552,13 +552,13 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), pendingHealContainer.RectTransform), TextManager.Get("medicalclinic.pendingheals"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), pendingHealContainer.RectTransform), TextManager.Get("medicalclinic.pendingheals"), font: GUIStyle.SubHeadingFont); GUIFrame healListContainer = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), pendingHealContainer.RectTransform), style: null); GUITextBlock? errorBlock = null; if (!GameMain.IsSingleplayer) { - errorBlock = new GUITextBlock(new RectTransform(Vector2.One, healListContainer.RectTransform), text: TextManager.Get("pleasewaitupnp"), font: GUI.LargeFont, textAlignment: Alignment.Center); + errorBlock = new GUITextBlock(new RectTransform(Vector2.One, healListContainer.RectTransform), text: TextManager.Get("pleasewaitupnp"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); } GUIListBox healList = new GUIListBox(new RectTransform(Vector2.One, healListContainer.RectTransform)) @@ -571,7 +571,7 @@ namespace Barotrauma GUILayoutGroup priceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true); GUITextBlock priceLabelBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.Get("campaignstore.total")); - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUI.SubHeadingFont, + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); @@ -679,12 +679,12 @@ namespace Barotrauma GUILayoutGroup textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentLayout.RectTransform), isHorizontal: true); - string name = prefab.Name; + LocalizedString name = prefab.Name; 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: GUI.SubHeadingFont); + 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), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUI.LargeFont) + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { Padding = Vector4.Zero }; @@ -702,7 +702,7 @@ namespace Barotrauma } }; - EnsureTextDoesntOverflow(name, afflictionName, textContainer.Rect, ImmutableArray.Create(textLayout, parentLayout)); + EnsureTextDoesntOverflow(name.Value, afflictionName, textContainer.Rect, ImmutableArray.Create(textLayout, parentLayout)); healElement.Afflictions.Add(new PendingAfflictionElement(affliction, backgroundFrame, healCost)); @@ -720,8 +720,8 @@ namespace Barotrauma GUILayoutGroup textGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), parent.RectTransform)); - string? characterName = info.Name, - jobName = null; + string? characterName = info.Name; + LocalizedString? jobName = null; GUITextBlock? nameBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), textGroup.RectTransform), characterName), jobBlock = null; @@ -741,7 +741,7 @@ namespace Barotrauma if (jobBlock is null) { return; } - EnsureTextDoesntOverflow(jobName, jobBlock, parent.Rect, layoutGroups); + EnsureTextDoesntOverflow(jobName?.Value, jobBlock, parent.Rect, layoutGroups); } } @@ -766,14 +766,14 @@ namespace Barotrauma mainFrame.RectTransform.ScreenSpaceOffset = new Point((int)location.X, GameMain.GraphicsHeight - mainFrame.Rect.Height); } - GUITextBlock feedbackBlock = new GUITextBlock(new RectTransform(Vector2.One, mainFrame.RectTransform), TextManager.Get("pleasewaitupnp"), textAlignment: Alignment.Center, font: GUI.LargeFont, wrap: true) + GUITextBlock feedbackBlock = new GUITextBlock(new RectTransform(Vector2.One, mainFrame.RectTransform), TextManager.Get("pleasewaitupnp"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont, wrap: true) { Visible = true }; GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) { - Font = GUI.SubHeadingFont, + Font = GUIStyle.SubHeadingFont, Visible = false }; @@ -793,7 +793,7 @@ namespace Barotrauma if (request.Result != MedicalClinic.RequestResult.Success) { feedbackBlock.Text = GetErrorText(request.Result); - feedbackBlock.TextColor = GUI.Style.Red; + feedbackBlock.TextColor = GUIStyle.Red; return; } @@ -844,11 +844,11 @@ namespace Barotrauma GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); - GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUI.SubHeadingFont); + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont); - Color textColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, (int)affliction.AfflictionSeverity / 2f); + Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, (int)affliction.AfflictionSeverity / 2f); - string vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); + LocalizedString vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center) { TextColor = textColor, @@ -857,8 +857,8 @@ namespace Barotrauma AutoScaleHorizontal = true }; - string severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); - GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + LocalizedString severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); + GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { TextColor = textColor, DisabledTextColor = textColor * 0.5f, @@ -866,17 +866,17 @@ namespace Barotrauma AutoScaleHorizontal = true }; - EnsureTextDoesntOverflow(prefab.Name, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + EnsureTextDoesntOverflow(prefab.Name.Value, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); 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, GUI.IntScale(64)), wrap: true) + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUIStyle.Font, GUI.IntScale(64)), wrap: true) { ToolTip = prefab.Description }; - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUI.LargeFont); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); @@ -968,7 +968,7 @@ namespace Barotrauma if (GameMain.IsSingleplayer || !(pendingHealList is { ErrorBlock: { } errorBlock, HealList: { } healList })) { return; } errorBlock.Visible = true; - errorBlock.TextColor = GUI.Style.TextColor; + errorBlock.TextColor = GUIStyle.TextColorNormal; errorBlock.Text = TextManager.Get("pleasewaitupnp"); healList.Visible = false; @@ -983,7 +983,7 @@ namespace Barotrauma if (request.Result != MedicalClinic.RequestResult.Success) { errorBlock.Text = GetErrorText(request.Result); - errorBlock.TextColor = GUI.Style.Red; + errorBlock.TextColor = GUIStyle.Red; return; } @@ -1011,7 +1011,7 @@ namespace Barotrauma selectedCrewAfflictionList = null; } - private static string GetErrorText(MedicalClinic.RequestResult result) + private static LocalizedString GetErrorText(MedicalClinic.RequestResult result) { return result switch { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 4d2e47df8..47df0dea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -10,6 +10,25 @@ namespace Barotrauma { class Store { + class ItemQuantity + { + public int Total { get; private set; } + public int NonEmpty { get; private set; } + public bool AllNonEmpty => NonEmpty == Total; + + public ItemQuantity(int total, bool areNonEmpty = true) + { + Total = total; + NonEmpty = areNonEmpty ? total : 0; + } + + public void Add(int amount, bool areNonEmpty) + { + Total += amount; + if (areNonEmpty) { NonEmpty += amount; } + } + } + private readonly CampaignUI campaignUI; private readonly GUIComponent parentComponent; private readonly List storeTabButtons = new List(); @@ -44,7 +63,7 @@ namespace Barotrauma private Point resolutionWhenCreated; - private Dictionary OwnedItems { get; } = new Dictionary(); + private Dictionary OwnedItems { get; } = new Dictionary(); private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; @@ -302,10 +321,10 @@ namespace Barotrauma }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreTradingIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont) { CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; // Merchant balance ------------------------------------------------ @@ -319,13 +338,13 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - TextManager.Get("campaignstore.storebalance"), font: GUI.Font, textAlignment: Alignment.BottomLeft) + TextManager.Get("campaignstore.storebalance"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; merchantBalanceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont) + "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, TextScale = 1.1f, @@ -343,11 +362,11 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), - TextManager.Get("campaignstore.sellvalue"), font: GUI.Font, textAlignment: Alignment.BottomLeft) + TextManager.Get("campaignstore.sellvalue"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { AutoScaleVertical = true, CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; var valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) @@ -356,9 +375,9 @@ namespace Barotrauma RelativeSpacing = 0.02f }; float blockWidth = GUI.IsFourByThree() ? 0.32f : 0.28f; - Point blockMaxSize = new Point((int)(GameSettings.TextScale * 60), valueChangeGroup.Rect.Height); + 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: GUI.SubHeadingFont) + "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, CanBeFocused = false, @@ -416,7 +435,7 @@ namespace Barotrauma Visible = false }; newSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, - "", font: GUI.SubHeadingFont) + "", font: GUIStyle.SubHeadingFont) { AutoScaleVertical = true, CanBeFocused = false, @@ -435,7 +454,7 @@ namespace Barotrauma tabSortingMethods.Clear(); foreach (StoreTab tab in tabs) { - string text = tab switch + LocalizedString text = tab switch { StoreTab.SellSub => TextManager.Get("submarine"), _ => TextManager.Get("campaignstoretab." + tab) @@ -591,10 +610,10 @@ namespace Barotrauma }; imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreShoppingCrateIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaignstore.shoppingcrate"), font: GUI.LargeFont, textAlignment: Alignment.Right) + new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaignstore.shoppingcrate"), font: GUIStyle.LargeFont, textAlignment: Alignment.Right) { CanBeFocused = false, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; // Player balance ------------------------------------------------ @@ -603,13 +622,13 @@ namespace Barotrauma RelativeSpacing = 0.005f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - TextManager.Get("campaignstore.balance"), font: GUI.Font, textAlignment: Alignment.BottomRight) + TextManager.Get("campaignstore.balance"), font: GUIStyle.Font, textAlignment: Alignment.BottomRight) { AutoScaleVertical = true, - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), playerBalanceContainer.RectTransform), - "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.TopRight) + "", textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopRight) { AutoScaleVertical = true, TextScale = 1.1f, @@ -638,11 +657,11 @@ namespace Barotrauma { Stretch = true }; - relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUI.Font) + relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUIStyle.Font) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, TextScale = 1.1f, @@ -653,11 +672,11 @@ namespace Barotrauma { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUI.Font) + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUIStyle.Font) { CanBeFocused = false }; - shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, TextScale = 1.1f @@ -666,14 +685,14 @@ namespace Barotrauma var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight); confirmButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform)) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; SetConfirmButtonBehavior(); clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall")) { ClickSound = GUISoundType.DecreaseQuantity, Enabled = HasActiveTabPermissions(), - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => { if (!HasActiveTabPermissions()) { return false; } @@ -694,9 +713,9 @@ namespace Barotrauma resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } - private string GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); + private LocalizedString GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); - private string GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); + private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { @@ -709,7 +728,7 @@ namespace Barotrauma var iconWidth = (0.9f * dealsHeader.Rect.Height) / dealsHeader.Rect.Width; var dealsIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 0.9f), dealsHeader.RectTransform), "StoreDealIcon", scaleToFit: true); var text = TextManager.Get(parentList == storeBuyList ? "campaignstore.dailyspecials" : "campaignstore.requestedgoods"); - var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUI.LargeFont); + var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUIStyle.LargeFont); storeSpecialColor = dealsIcon.Color; dealsText.TextColor = storeSpecialColor; var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine"); @@ -811,7 +830,7 @@ namespace Barotrauma child.Visible = (IsBuying || item.Quantity > 0) && (!category.HasValue || item.ItemPrefab.Category.HasFlag(category.Value)) && - (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.ToLower().Contains(filter)); + (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)); } foreach (GUIButton btn in itemCategoryButtons) { @@ -892,7 +911,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = quantity; SetQuantityLabelText(StoreTab.Buy, itemFrame); - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetPriceGetters(itemFrame, true); } SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); @@ -967,7 +986,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; SetQuantityLabelText(StoreTab.Sell, itemFrame); - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetPriceGetters(itemFrame, false); } SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); @@ -1045,7 +1064,7 @@ namespace Barotrauma { (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; SetQuantityLabelText(StoreTab.SellSub, itemFrame); - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetPriceGetters(itemFrame, false); } SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); @@ -1185,7 +1204,7 @@ namespace Barotrauma numInput.Enabled = hasPermissions; numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab); } - SetOwnedLabelText(itemFrame); + SetOwnedText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions); } existingItemFrames.Add(itemFrame); @@ -1193,7 +1212,7 @@ namespace Barotrauma suppressBuySell = true; if (numInput != null) { - if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUI.Style.Green); } + if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); } numInput.IntValue = item.Quantity; } suppressBuySell = false; @@ -1421,14 +1440,8 @@ namespace Barotrauma width = parentComponent.Rect.Width; parent = parentComponent.RectTransform; } - string tooltip = pi.ItemPrefab.Name; - if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) - { - tooltip += $"\n{pi.ItemPrefab.Description}"; - } GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, (int)(GUI.yScale * 80)), parent: parent), style: "ListBoxElement") { - ToolTip = tooltip, UserData = pi }; @@ -1443,7 +1456,7 @@ namespace Barotrauma var iconRelativeWidth = 0.0f; var priceAndButtonRelativeWidth = 1.0f - nameAndIconRelativeWidth; - if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.sprite) is { } itemIcon) + if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.Sprite) is { } itemIcon) { iconRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; GUIImage img = new GUIImage(new RectTransform(new Vector2(iconRelativeWidth, 0.9f), mainGroup.RectTransform), itemIcon, scaleToFit: true) @@ -1468,7 +1481,7 @@ namespace Barotrauma bool locationHasDealOnItem = isSellingRelatedList ? CurrentLocation.RequestedGoods.Contains(pi.ItemPrefab) : CurrentLocation.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), - pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) + pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1498,7 +1511,7 @@ namespace Barotrauma if (isParentOnLeftSideOfInterface) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform), - CreateQuantityLabelText(containingTab, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) + CreateQuantityLabelText(containingTab, pi.Quantity), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1545,8 +1558,7 @@ namespace Barotrauma var rectTransform = shoppingCrateAmountGroup == null ? new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform) : new RectTransform(new Vector2(0.6f, 1.0f), shoppingCrateAmountGroup.RectTransform); - new GUITextBlock(rectTransform, CreateOwnedLabelText(OwnedItems.GetValueOrDefault(pi.ItemPrefab, 0)), font: GUI.Font, - textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft) + var ownedLabel = new GUITextBlock(rectTransform, string.Empty, font: GUIStyle.Font, textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft) { CanBeFocused = false, Shadow = locationHasDealOnItem, @@ -1554,6 +1566,7 @@ namespace Barotrauma TextScale = 0.85f, UserData = "owned" }; + SetOwnedText(frame, ownedLabel); shoppingCrateAmountGroup?.Recalculate(); var buttonRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; @@ -1563,7 +1576,7 @@ namespace Barotrauma CanBeFocused = false }; var priceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), priceFrame.RectTransform, anchor: Anchor.Center), - "0 MK", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + "0 MK", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, TextColor = locationHasDealOnItem ? storeSpecialColor : Color.White, @@ -1577,7 +1590,7 @@ namespace Barotrauma new RectTransform(new Vector2(1.0f, 0.25f), priceFrame.RectTransform, anchor: Anchor.Center) { AbsoluteOffset = new Point(0, priceBlock.RectTransform.ScaledSize.Y) - }, "", font: GUI.SmallFont, textAlignment: Alignment.Center) + }, "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { CanBeFocused = false, Strikethrough = new GUITextBlock.StrikethroughSettings(color: priceBlock.TextColor, expand: 1), @@ -1593,7 +1606,7 @@ namespace Barotrauma { ClickSound = GUISoundType.IncreaseQuantity, Enabled = !forceDisable && pi.Quantity > 0, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = "addbutton", OnClicked = (button, userData) => AddToShoppingCrate(pi) }; @@ -1604,7 +1617,7 @@ namespace Barotrauma { ClickSound = GUISoundType.DecreaseQuantity, Enabled = !forceDisable, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = "removebutton", OnClicked = (button, userData) => ClearFromShoppingCrate(pi) }; @@ -1639,7 +1652,7 @@ namespace Barotrauma if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; } if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; } if (!ItemAndAllContainersInteractable(subItem)) { continue; } - AddToOwnedItems(subItem.Prefab); + AddOwnedItem(subItem); } } @@ -1650,11 +1663,11 @@ namespace Barotrauma var rootInventoryOwner = item.GetRootInventoryOwner(); var ownedByCrewMember = GameMain.GameSession.CrewManager.GetCharacters().Any(c => c == rootInventoryOwner); if (!ownedByCrewMember) { continue; } - AddToOwnedItems(item.Prefab); + AddOwnedItem(item); } // Add items already purchased - CargoManager?.PurchasedItems?.ForEach(pi => AddToOwnedItems(pi.ItemPrefab, amount: pi.Quantity)); + CargoManager?.PurchasedItems?.ForEach(pi => AddNonEmptyOwnedItems(pi)); ownedItemsUpdateTimer = 0.0f; @@ -1668,15 +1681,30 @@ namespace Barotrauma return true; } - void AddToOwnedItems(ItemPrefab itemPrefab, int amount = 1) + void AddOwnedItem(Item item) { - if (OwnedItems.ContainsKey(itemPrefab)) + if (!(item?.Prefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo)) { return; } + bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; + if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { - OwnedItems[itemPrefab] += amount; + OwnedItems[item.Prefab].Add(1, isNonEmpty); } else { - OwnedItems.Add(itemPrefab, amount); + OwnedItems.Add(item.Prefab, new ItemQuantity(1, areNonEmpty: isNonEmpty)); + } + } + + void AddNonEmptyOwnedItems(PurchasedItem purchasedItem) + { + if (purchasedItem == null) { return; } + if (OwnedItems.TryGetValue(purchasedItem.ItemPrefab, out ItemQuantity itemQuantity)) + { + OwnedItems[purchasedItem.ItemPrefab].Add(purchasedItem.Quantity, true); + } + else + { + OwnedItems.Add(purchasedItem.ItemPrefab, new ItemQuantity(purchasedItem.Quantity)); } } } @@ -1692,7 +1720,7 @@ namespace Barotrauma { icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f: 0.5f); } - else if (pi.ItemPrefab?.sprite != null) + else if (pi.ItemPrefab?.Sprite != null) { icon.Color = pi.ItemPrefab.SpriteColor * (enabled ? 1.0f : 0.5f); } @@ -1737,35 +1765,89 @@ namespace Barotrauma itemFrame.UserData = pi; } - private void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) + private static void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame) { - if (itemFrame == null) { return; } - if (itemFrame.FindChild("quantitylabel", recursive: true) is GUITextBlock label) + if (itemFrame?.FindChild("quantitylabel", recursive: true) is GUITextBlock label) { label.Text = CreateQuantityLabelText(mode, (itemFrame.UserData as PurchasedItem).Quantity); } } - private string CreateQuantityLabelText(StoreTab mode, int quantity) => mode != StoreTab.Buy ? - TextManager.GetWithVariable("campaignstore.quantity", "[amount]", quantity.ToString()) : - TextManager.GetWithVariable("campaignstore.instock", "[amount]", quantity.ToString()); - - private void SetOwnedLabelText(GUIComponent itemComponent) + private static LocalizedString CreateQuantityLabelText(StoreTab mode, int quantity) { - if (itemComponent == null) { return; } - var itemCount = 0; - if (itemComponent.UserData is PurchasedItem pi) + try { - itemCount = OwnedItems.GetValueOrDefault(pi.ItemPrefab, itemCount); + string textTag = mode switch + { + StoreTab.Buy => "campaignstore.instock", + StoreTab.Sell => "campaignstore.ownedinventory", + StoreTab.SellSub => "campaignstore.ownedsub", + _ => throw new NotImplementedException() + }; + return TextManager.GetWithVariable(textTag, "[amount]", quantity.ToString()); } - if (itemComponent.FindChild("owned", recursive: true) is GUITextBlock label) + catch (NotImplementedException e) { - label.Text = CreateOwnedLabelText(itemCount); + string errorMsg = $"Error creating a store quantity label text: unknown store tab.\n{e.StackTrace.CleanupStackTrace()}"; +#if DEBUG + DebugConsole.ShowError(errorMsg); +#else + DebugConsole.AddWarning(errorMsg); +#endif } + return string.Empty; } - private string CreateOwnedLabelText(int itemCount) => itemCount > 0 ? - TextManager.GetWithVariable("campaignstore.owned", "[amount]", itemCount.ToString()) : ""; + private void SetOwnedText(GUIComponent itemComponent, GUITextBlock ownedLabel = null) + { + ownedLabel ??= itemComponent?.FindChild("owned", recursive: true) as GUITextBlock; + if (itemComponent == null && ownedLabel == null) { return; } + PurchasedItem purchasedItem = itemComponent?.UserData as PurchasedItem; + ItemQuantity itemQuantity = null; + LocalizedString ownedLabelText = string.Empty; + if (purchasedItem != null && OwnedItems.TryGetValue(purchasedItem.ItemPrefab, out itemQuantity) && itemQuantity.Total > 0) + { + if (itemQuantity.AllNonEmpty) + { + ownedLabelText = TextManager.GetWithVariable("campaignstore.owned", "[amount]", itemQuantity.Total.ToString()); + } + else + { + ownedLabelText = TextManager.GetWithVariables("campaignstore.ownedspecific", + ("[nonempty]", itemQuantity.NonEmpty.ToString()), + ("[total]", itemQuantity.Total.ToString())); + } + } + if (itemComponent != null) + { + LocalizedString toolTip = string.Empty; + if (purchasedItem.ItemPrefab != null) + { + toolTip = purchasedItem.ItemPrefab.Name; + if (!purchasedItem.ItemPrefab.Description.IsNullOrEmpty()) + { + toolTip += $"\n{purchasedItem.ItemPrefab.Description}"; + } + if (itemQuantity != null) + { + if (itemQuantity.AllNonEmpty) + { + toolTip += $"\n\n{ownedLabelText}"; + } + else + { + toolTip += $"\n\n{TextManager.GetWithVariable("campaignstore.ownednonempty", "[amount]", itemQuantity.NonEmpty.ToString())}"; + toolTip += $"\n{TextManager.GetWithVariable("campaignstore.ownedtotal", "[amount]", itemQuantity.Total.ToString())}"; + } + } + } + itemComponent.ToolTip = toolTip; + } + if (ownedLabel != null) + { + ownedLabel.Text = ownedLabelText; + } + } private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode) { @@ -1799,7 +1881,7 @@ namespace Barotrauma } } - private string GetCurrencyFormatted(int amount) => + private LocalizedString GetCurrencyFormatted(int amount) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); private bool ModifyBuyQuantity(PurchasedItem item, int quantity) @@ -1916,7 +1998,7 @@ namespace Barotrauma var dialog = new GUIMessageBox( TextManager.Get("newsupplies"), TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), - new string[] { TextManager.Get("Ok") }); + new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; return false; @@ -1995,7 +2077,7 @@ namespace Barotrauma var confirmDialog = new GUIMessageBox( TextManager.Get("FireWarningHeader"), TextManager.Get("CampaignStore.SellWarningText"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmDialog.Buttons[0].OnClicked = (b, o) => SellItems(); confirmDialog.Buttons[0].OnClicked += confirmDialog.Close; confirmDialog.Buttons[1].OnClicked = confirmDialog.Close; @@ -2040,12 +2122,12 @@ namespace Barotrauma ownedItemsUpdateTimer += deltaTime; if (ownedItemsUpdateTimer >= timerUpdateInterval) { - var prevOwnedItems = new Dictionary(OwnedItems); + var prevOwnedItems = new Dictionary(OwnedItems); UpdateOwnedItems(); var refresh = (prevOwnedItems.Count != OwnedItems.Count) || - (prevOwnedItems.Select(kvp => kvp.Value).Sum() != OwnedItems.Select(kvp => kvp.Value).Sum()) || - (OwnedItems.Any(kvp => kvp.Value > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || - prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out var itemCount) || kvp.Value != itemCount)); + (prevOwnedItems.Select(kvp => kvp.Value.Total).Sum() != OwnedItems.Select(kvp => kvp.Value.Total).Sum()) || + (OwnedItems.Any(kvp => kvp.Value.Total > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || + prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out ItemQuantity itemQuantity) || kvp.Value.Total != itemQuantity.Total)); if (refresh) { needsItemsToSellRefresh = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 1c687913c..2d8d5a598 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -31,19 +31,12 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private string purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; - public static readonly string[] DeliveryTextVariables = new string[] { "[submarinename1]", "[location1]", "[location2]", "[submarinename2]", "[amount]", "[currencyname]" }; - public static readonly string[] SwitchTextVariables = new string[] { "[submarinename1]", "[submarinename2]" }; - public static readonly string[] PurchaseAndSwitchTextVariables = new string[] { "[submarinename1]", "[amount]", "[currencyname]", "[submarinename2]" }; - public static readonly string[] PurchaseTextVariables = new string[] { "[submarinename]", "[amount]", "[currencyname]" }; - - private static readonly string[] notEnoughCreditsDeliveryTextVariables = new string[] { "[currencyname]", "[submarinename]", "[location1]", "[location2]" }; - private static readonly string[] notEnoughCreditsPurchaseTextVariables = new string[] { "[currencyname]", "[submarinename]" }; - private readonly string[] messageBoxOptions; + private readonly LocalizedString[] messageBoxOptions; public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; @@ -77,11 +70,11 @@ namespace Barotrauma if (GameMain.Client == null) { - messageBoxOptions = new string[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }; + messageBoxOptions = new LocalizedString[2] { TextManager.Get("Yes"), TextManager.Get("Cancel") }; } else { - messageBoxOptions = new string[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; + messageBoxOptions = new LocalizedString[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; } if (Submarine.MainSub?.Info == null) { return; } @@ -107,7 +100,7 @@ namespace Barotrauma } currencyShorthandText = TextManager.Get("currencyformat"); - currencyLongText = TextManager.Get("credit").ToLower(); + currencyLongText = TextManager.Get("credit").Value.ToLowerInvariant(); UpdateSubmarines(); missingPreviewText = TextManager.Get("SubPreviewImageNotFound"); @@ -135,9 +128,9 @@ namespace Barotrauma }; content = new GUILayoutGroup(new RectTransform(new Point(background.Rect.Width - HUDLayoutSettings.Padding * 4, background.Rect.Height - HUDLayoutSettings.Padding * 4), background.RectTransform, Anchor.Center)) { AbsoluteSpacing = (int)(HUDLayoutSettings.Padding * 1.5f) }; - GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUI.LargeFont); + GUITextBlock header = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), content.RectTransform), transferService ? TextManager.Get("switchsubmarineheader") : TextManager.GetWithVariable("outpostshipyard", "[location]", GameMain.GameSession.Map.CurrentLocation.Name), font: GUIStyle.LargeFont); header.CalculateHeightFromText(0, true); - GUITextBlock credits = new GUITextBlock(new RectTransform(Vector2.One, header.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight) + GUITextBlock credits = new GUITextBlock(new RectTransform(Vector2.One, header.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { TextGetter = CampaignUI.GetMoney }; @@ -159,7 +152,7 @@ namespace Barotrauma specsFrame = new GUIListBox(new RectTransform(new Vector2(0.39f, 1f), infoFrame.RectTransform), style: null) { Spacing = GUI.IntScale(5), Padding = new Vector4(HUDLayoutSettings.Padding / 2f, HUDLayoutSettings.Padding, 0, 0) }; new GUIFrame(new RectTransform(new Vector2(0.02f, 0.8f), infoFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, style: "VerticalLine"); 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: GUI.Font, wrap: true) { CanBeFocused = false }; + 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 }; @@ -180,7 +173,7 @@ namespace Barotrauma SetConfirmButtonState(false); pageIndicatorHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.5f), submarineControlsGroup.RectTransform), style: null); - pageIndicator = GUI.Style.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); + pageIndicator = GUIStyle.GetComponentStyle("GUIPageIndicator").GetDefaultSprite(); UpdatePaging(); for (int i = 0; i < submarineDisplays.Length; i++) @@ -191,9 +184,9 @@ namespace Barotrauma }; submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true); submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); - submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUI.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Center); - submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); + submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); + submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Center); + submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); submarineDisplayElement.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton") { @@ -342,7 +335,7 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - string amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); + LocalizedString amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); submarineDisplays[i].submarineFee.Text = priceText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -351,7 +344,7 @@ namespace Barotrauma { if (deliveryFee > 0) { - string amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); + LocalizedString amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); submarineDisplays[i].submarineFee.Text = deliveryFeeText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -535,7 +528,7 @@ namespace Barotrauma listBackground.Sprite = previewImage; listBackground.SetCrop(true); - ScalableFont font = GUI.Font; + GUIFont font = GUIStyle.Font; info.CreateSpecsWindow(specsFrame, font); descriptionTextBlock.Text = info.Description; descriptionTextBlock.CalculateHeightFromText(); @@ -590,8 +583,11 @@ namespace Barotrauma { if (GameMain.GameSession.Campaign.Money < deliveryFee && deliveryFee > 0) { - new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", notEnoughCreditsDeliveryTextVariables, - new string[] { currencyLongText, selectedSubmarine.DisplayName, deliveryLocationName, GameMain.GameSession.Map.CurrentLocation.Name })); + new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", + ("[currencyname]", currencyLongText), + ("[submarinename]", selectedSubmarine.DisplayName), + ("[location1]", deliveryLocationName), + ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); return; } @@ -599,13 +595,19 @@ namespace Barotrauma if (deliveryFee > 0) { - msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", DeliveryTextVariables, - new string[6] { selectedSubmarine.DisplayName, deliveryLocationName, GameMain.GameSession.Map.CurrentLocation.Name, CurrentOrPendingSubmarine().DisplayName, deliveryFee.ToString(), currencyLongText }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("deliveryrequesttext", + ("[submarinename1]", selectedSubmarine.DisplayName), + ("[location1]", deliveryLocationName), + ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), + ("[amount]", deliveryFee.ToString()), + ("[currencyname]", currencyLongText)), messageBoxOptions); } else { - msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", SwitchTextVariables, - new string[2] { CurrentOrPendingSubmarine().DisplayName, selectedSubmarine.DisplayName }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("switchsubmarineheader"), TextManager.GetWithVariables("switchsubmarinetext", + ("[submarinename1]", CurrentOrPendingSubmarine().DisplayName), + ("[submarinename2]", selectedSubmarine.DisplayName)), messageBoxOptions); } msgBox.Buttons[0].OnClicked = (applyButton, obj) => @@ -629,8 +631,9 @@ namespace Barotrauma { if (GameMain.GameSession.Campaign.Money < selectedSubmarine.Price) { - new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", notEnoughCreditsPurchaseTextVariables, - new string[2] { currencyLongText, selectedSubmarine.DisplayName })); + new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", + ("[currencyname]", currencyLongText), + ("[submarinename]", selectedSubmarine.DisplayName))); return; } @@ -638,8 +641,11 @@ namespace Barotrauma if (!purchaseOnly) { - msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", PurchaseAndSwitchTextVariables, - new string[4] { selectedSubmarine.DisplayName, selectedSubmarine.Price.ToString(), currencyLongText, CurrentOrPendingSubmarine().DisplayName }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", + ("[submarinename1]", selectedSubmarine.DisplayName), + ("[amount]", selectedSubmarine.Price.ToString()), + ("[currencyname]", currencyLongText), + ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { @@ -658,8 +664,10 @@ namespace Barotrauma } else { - msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", PurchaseTextVariables, - new string[3] { selectedSubmarine.DisplayName, selectedSubmarine.Price.ToString(), currencyLongText }), messageBoxOptions); + msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", + ("[submarinename]", selectedSubmarine.DisplayName), + ("[amount]", selectedSubmarine.Price.ToString()), + ("[currencyname]", currencyLongText)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 7be59a84f..aa22ddcd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -99,15 +99,15 @@ namespace Barotrauma { if (currentPing < lowPingThreshold) { - return GUI.Style.Green; + return GUIStyle.Green; } else if (currentPing < mediumPingThreshold) { - return GUI.Style.Yellow; + return GUIStyle.Yellow; } else { - return GUI.Style.Red; + return GUIStyle.Red; } } @@ -119,10 +119,10 @@ namespace Barotrauma public void Initialize() { - spectateIcon = GUI.Style.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; - disconnectedIcon = GUI.Style.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; - ownerIcon = GUI.Style.GetComponentStyle("OwnerIcon").GetDefaultSprite(); - moderatorIcon = GUI.Style.GetComponentStyle("ModeratorIcon").GetDefaultSprite(); + spectateIcon = GUIStyle.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; + disconnectedIcon = GUIStyle.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; + ownerIcon = GUIStyle.GetComponentStyle("OwnerIcon").GetDefaultSprite(); + moderatorIcon = GUIStyle.GetComponentStyle("ModeratorIcon").GetDefaultSprite(); initialized = true; } @@ -142,7 +142,7 @@ namespace Barotrauma talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f) { - talentApplyButton.Flash(GUI.Style.Orange); + talentApplyButton.Flash(GUIStyle.Orange); } } @@ -243,7 +243,7 @@ namespace Barotrauma var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation"); var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); - new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right, parseRichText: true) + new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right) { TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) }; @@ -353,7 +353,7 @@ namespace Barotrauma { if (teamIDs.Count > 1) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUI.Style.Green : GUI.Style.Orange) { ForceUpperCase = true }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUIStyle.Green : GUIStyle.Orange) { ForceUpperCase = ForceUpperCase.Yes }; } headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true) @@ -396,7 +396,7 @@ namespace Barotrauma for (int i = 0; i < teamIDs.Count; i++) { - headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUI.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f); + headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUIStyle.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f); if (!GameMain.IsMultiplayer) { @@ -446,9 +446,9 @@ namespace Barotrauma jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = true; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -493,7 +493,7 @@ namespace Barotrauma }; GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + 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, null)); } @@ -510,9 +510,9 @@ namespace Barotrauma characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = true; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -583,11 +583,11 @@ namespace Barotrauma else { GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); if (character is AICharacter) { - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = true })); + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes })); } else { @@ -677,16 +677,16 @@ namespace Barotrauma float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUI.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); + ToolBox.LimitString(client.Name, GUIStyle.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); float iconWidth = iconSize.X / (float)characterColumnWidth; - int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUI.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); + int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; } else { characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); + ToolBox.LimitString(client.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); } if (client.Character != null && client.Character.IsDead) @@ -724,14 +724,14 @@ namespace Barotrauma } else { - Vector2 stringOffset = GUI.GlobalFont.MeasureString(inLobbyString) / 2f; - GUI.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + Vector2 stringOffset = GUIStyle.GlobalFont.MeasureString(inLobbyString) / 2f; + GUIStyle.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); } } private void DrawDisconnectedIcon(SpriteBatch spriteBatch, Rectangle area) { - disconnectedIcon.Draw(spriteBatch, area, GUI.Style.Red); + disconnectedIcon.Draw(spriteBatch, area, GUIStyle.Red); } /// @@ -788,7 +788,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)); - ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; + GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font; var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { @@ -796,9 +796,9 @@ namespace Barotrauma Stretch = true }; - GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUI.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUI.Font) + GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUIStyle.Font) { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, Padding = Vector4.Zero }; @@ -885,22 +885,22 @@ namespace Barotrauma switch (type) { case PlayerConnectionChangeType.Joined: - textColor = GUI.Style.Green; + textColor = GUIStyle.Green; break; case PlayerConnectionChangeType.Kicked: - textColor = GUI.Style.Orange; + textColor = GUIStyle.Orange; break; case PlayerConnectionChangeType.Disconnected: - textColor = GUI.Style.Yellow; + textColor = GUIStyle.Yellow; break; case PlayerConnectionChangeType.Banned: - textColor = GUI.Style.Red; + textColor = GUIStyle.Red; break; } if (logList != null) { - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont, parseRichText: true) + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), RichString.Rich(line), wrap: true, font: GUIStyle.SmallFont) { TextColor = textColor, CanBeFocused = false, @@ -935,14 +935,14 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(10) }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Name, font: GUIStyle.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("Biome", fallBackTag: "location"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)Level.Loaded.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine") @@ -974,7 +974,7 @@ namespace Barotrauma if (GameMain.GameSession?.Missions != null) { int spacing = GUI.IntScale(5); - int iconSize = (int)(GUI.LargeFont.MeasureChar('T').Y + GUI.Font.MeasureChar('T').Y * 4 + spacing * 4); + int iconSize = (int)(GUIStyle.LargeFont.MeasureChar('T').Y + GUIStyle.Font.MeasureChar('T').Y * 4 + spacing * 4); foreach (Mission mission in GameMain.GameSession.Missions) { @@ -983,28 +983,27 @@ namespace Barotrauma { AbsoluteSpacing = spacing }; - string descriptionText = mission.Description; - foreach (string missionMessage in mission.ShownMessages) + LocalizedString descriptionText = mission.Description; + foreach (LocalizedString missionMessage in mission.ShownMessages) { descriptionText += "\n\n" + missionMessage; } - string rewardText = mission.GetMissionRewardText(Submarine.MainSub); - string reputationText = mission.GetReputationRewardText(mission.Locations[0]); + RichString rewardText = mission.GetMissionRewardText(Submarine.MainSub); + RichString reputationText = mission.GetReputationRewardText(mission.Locations[0]); - var missionNameRichTextData = RichTextData.GetRichTextData(mission.Name, out string missionNameString); - var missionRewardRichTextData = RichTextData.GetRichTextData(rewardText, out string missionRewardString); - var missionReputationRichTextData = RichTextData.GetRichTextData(reputationText, out string missionReputationString); - var missionDescriptionRichTextData = RichTextData.GetRichTextData(descriptionText, out string missionDescriptionString); + Func wrapMissionText(GUIFont font) + { + return (str) => ToolBox.WrapText(str, missionTextGroup.Rect.Width, font.Value); + } + RichString missionNameString = RichString.Rich(mission.Name, wrapMissionText(GUIStyle.LargeFont)); + RichString missionRewardString = RichString.Rich(rewardText, wrapMissionText(GUIStyle.Font)); + RichString missionReputationString = RichString.Rich(reputationText, wrapMissionText(GUIStyle.Font)); + RichString missionDescriptionString = RichString.Rich(descriptionText, wrapMissionText(GUIStyle.Font)); - missionNameString = ToolBox.WrapText(missionNameString, missionTextGroup.Rect.Width, GUI.LargeFont); - missionRewardString = ToolBox.WrapText(missionRewardString, missionTextGroup.Rect.Width, GUI.Font); - missionReputationString = ToolBox.WrapText(missionReputationString, missionTextGroup.Rect.Width, GUI.Font); - missionDescriptionString = ToolBox.WrapText(missionDescriptionString, missionTextGroup.Rect.Width, GUI.Font); - - Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); - Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); - Vector2 missionReputationSize = GUI.Font.MeasureString(missionReputationString); + Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); + Vector2 missionRewardSize = GUIStyle.Font.MeasureString(missionRewardString); + Vector2 missionReputationSize = GUIStyle.Font.MeasureString(missionReputationString); float ySize = missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y + missionReputationSize.Y + missionTextGroup.AbsoluteSpacing * 4; bool displayDifficulty = mission.Difficulty.HasValue; @@ -1030,7 +1029,7 @@ namespace Barotrauma UpdateMissionStateIcon(mission, icon); mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon(mission, icon); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameRichTextData, missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); GUILayoutGroup difficultyIndicatorGroup = null; if (displayDifficulty) { @@ -1048,20 +1047,20 @@ namespace Barotrauma }; } } - var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardRichTextData, missionRewardString); + var rewardTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); if (difficultyIndicatorGroup != null) { difficultyIndicatorGroup.RectTransform.Resize(new Point((int)(difficultyIndicatorGroup.Rect.Width - rewardTextBlock.Padding.X - rewardTextBlock.Padding.Z), difficultyIndicatorGroup.Rect.Height)); difficultyIndicatorGroup.RectTransform.AbsoluteOffset = new Point((int)rewardTextBlock.Padding.X, 0); } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationRichTextData, missionReputationString); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionRichTextData, missionDescriptionString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionReputationString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } } else { GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUIStyle.LargeFont); } } @@ -1101,11 +1100,11 @@ namespace Barotrauma GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, style: null); GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.319f, 0f) }, false, childAnchor: Anchor.TopLeft); - string missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUI.LargeFont); - string missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUI.Font); + LocalizedString missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUIStyle.LargeFont); + LocalizedString missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUIStyle.Font); - Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + Vector2 missionNameSize = GUIStyle.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUIStyle.Font.MeasureString(missionDescriptionString); missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y)); missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); @@ -1118,7 +1117,7 @@ namespace Barotrauma new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), traitorMission.Icon, null, true) { Color = traitorMission.IconColor }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); } @@ -1164,21 +1163,21 @@ namespace Barotrauma var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform)); - string className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}") : TextManager.Get("shuttle"); + LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}") : TextManager.Get("shuttle"); - int nameHeight = (int)GUI.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; - int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; + int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y; + int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; - var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false }; submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); - var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); if (GameMain.GameSession?.GameMode is CampaignMode campaign) { GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.09f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0f, 0.43f) }, isHorizontal: true) { Stretch = true }; GUIImage headerIcon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SubmarineIcon"); - new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUIStyle.LargeFont); var upgradeRootLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.48f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), isHorizontal: true); @@ -1206,7 +1205,7 @@ namespace Barotrauma else { var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft)); - sub.Info.CreateSpecsWindow(specsListBox, GUI.Font, includeTitle: false, includeClass: false, includeDescription: true); + sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true); } } private Color unselectedColor = new Color(240, 255, 255, 225); @@ -1216,8 +1215,8 @@ namespace Barotrauma private Color pressedColor = new Color(60, 60, 60, 225); private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>(); - private readonly List<(string talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(string talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); - private List selectedTalents = new List(); + private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>(); + private List selectedTalents = new List(); private GUITextBlock experienceText; private GUIProgressBar experienceBar; @@ -1231,11 +1230,11 @@ namespace Barotrauma private readonly ImmutableDictionary talentStageStyles = new Dictionary { - { TalentTree.TalentTreeStageState.Invalid, GUI.Style.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Locked, GUI.Style.GetComponentStyle("TalentTreeLocked") }, - { TalentTree.TalentTreeStageState.Unlocked, GUI.Style.GetComponentStyle("TalentTreePurchased") }, - { TalentTree.TalentTreeStageState.Available, GUI.Style.GetComponentStyle("TalentTreeUnlocked") }, - { TalentTree.TalentTreeStageState.Highlighted, GUI.Style.GetComponentStyle("TalentTreeAvailable") }, + { TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") }, + { TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") }, + { TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") }, + { TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") }, + { TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") }, }.ToImmutableDictionary(); private readonly ImmutableDictionary talentStageBackgroundColors = new Dictionary @@ -1287,7 +1286,7 @@ namespace Barotrauma { AbsoluteSpacing = GUI.IntScale(5) }; - + GUILayoutGroup talentInfoLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), talentFrameLayoutGroup.RectTransform, Anchor.Center), isHorizontal: true); CharacterInfo info = controlledCharacter.Info; @@ -1300,18 +1299,18 @@ namespace Barotrauma }); GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; - - Vector2 nameSize = GUI.SubHeadingFont.MeasureString(info.Name); - GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUI.SubHeadingFont) { TextColor = job.Prefab.UIColor }; + + 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 }; nameBlock.RectTransform.NonScaledSize = nameSize.Pad(nameBlock.Padding).ToPoint(); - Vector2 jobSize = GUI.SmallFont.MeasureString(job.Name); - GUITextBlock jobBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), job.Name, font: GUI.SmallFont) { 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(); - string traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); - Vector2 traitSize = GUI.SmallFont.MeasureString(traitString); - GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUI.SmallFont); + LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + info.PersonalityTrait.Name.Replace(" ", ""))); + Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString); + 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); @@ -1365,7 +1364,7 @@ namespace Barotrauma } } - IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(e, StringComparison.OrdinalIgnoreCase))); + IEnumerable endocrineTalents = info.GetEndocrineTalents().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)); if (endocrineTalents.Count() > 0) { @@ -1377,15 +1376,15 @@ namespace Barotrauma GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), talentInfoLayoutGroup.RectTransform)) { Stretch = true }; - string skillString = TextManager.Get("skills"); - Vector2 skillSize = GUI.SubHeadingFont.MeasureString(skillString); - GUITextBlock skillBlock = new GUITextBlock(new RectTransform(Vector2.One, skillLayout.RectTransform), skillString, font: GUI.SubHeadingFont); + LocalizedString skillString = TextManager.Get("skills"); + Vector2 skillSize = GUIStyle.SubHeadingFont.MeasureString(skillString); + GUITextBlock skillBlock = new GUITextBlock(new RectTransform(Vector2.One, skillLayout.RectTransform), skillString, font: GUIStyle.SubHeadingFont); 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); - if (!TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + 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"); @@ -1401,7 +1400,7 @@ namespace Barotrauma 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: GUI.SubHeadingFont, textAlignment: Alignment.Center)); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center)); for (int i = 0; i < 4; i++) { @@ -1439,7 +1438,7 @@ namespace Barotrauma GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) { - ToolTip = $"{talent.DisplayName}\n\n{talent.Description}", + ToolTip = RichString.Rich(talent.DisplayName + "\n\n" + talent.Description), UserData = talent.Identifier, PressedColor = pressedColor, OnClicked = (button, userData) => @@ -1447,7 +1446,7 @@ namespace Barotrauma // deselect other buttons in tier by removing their selected talents from pool foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) { - if (guiButton.UserData is string otherTalentIdentifier && guiButton != button) + if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button) { if (!controlledCharacter.HasTalent(otherTalentIdentifier)) { @@ -1455,7 +1454,7 @@ namespace Barotrauma } } } - string talentIdentifier = userData as string; + Identifier talentIdentifier = (Identifier)userData; if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents)) { @@ -1479,10 +1478,10 @@ namespace Barotrauma GUIComponent iconImage; if (talent.Icon is null) { - iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) + iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null) { - OutlineColor = GUI.Style.Red, - TextColor = GUI.Style.Red, + OutlineColor = GUIStyle.Red, + TextColor = GUIStyle.Red, PressedColor = unselectableColor, CanBeFocused = false, }; @@ -1511,18 +1510,18 @@ namespace Barotrauma 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: GUI.Style.Green) + barSize: controlledCharacter.Info.GetProgressTowardsNextLevel(), color: GUIStyle.Green) { IsHorizontal = true, }; - - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textAlignment: Alignment.CenterRight) + + 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: GUI.SubHeadingFont, parseRichText: true, 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") { @@ -1545,22 +1544,23 @@ namespace Barotrauma { 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}", returnNull: true) ?? skill.Identifier)); + 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); if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) { int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level); + //TODO: if/when we upgrade to C# 9, do neater pattern matching here string stringColor = true switch { - true when skillChange > 0 => XMLExtensions.ColorToString(GUI.Style.Green), - true when skillChange < 0 => XMLExtensions.ColorToString(GUI.Style.Red), - _ => XMLExtensions.ColorToString(GUI.Style.TextColor) + true when skillChange > 0 => XMLExtensions.ColorToString(GUIStyle.Green), + true when skillChange < 0 => XMLExtensions.ColorToString(GUIStyle.Red), + _ => XMLExtensions.ColorToString(GUIStyle.TextColorNormal) }; - string changeText = $"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"; - new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText, parseRichText: true) { Padding = Vector4.Zero }; + RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)"); + new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero }; } skillContainer.Recalculate(); } @@ -1601,8 +1601,8 @@ namespace Barotrauma } else if (talentCount > 0) { - string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUI.Style.Red)}‖{-talentCount}‖color:end‖"; - string localizedString = TextManager.GetWithVariables("talentmenu.points.spending", new []{ "[amount]", "[used]" }, new []{ pointsLeft, pointsUsed}); + string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖"; + LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed)); talentPointText.SetRichText(localizedString); } else @@ -1622,19 +1622,19 @@ namespace Barotrauma foreach (var talentButton in talentButtons) { - string talentIdentifier = talentButton.button.UserData as string; + Identifier talentIdentifier = (Identifier)talentButton.button.UserData; bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier); Color newTalentColor = unselectable ? unselectableColor : unselectedColor; Color hoverColor = Color.White; if (controlledCharacter.HasTalent(talentIdentifier)) { - newTalentColor = GUI.Style.Green; + newTalentColor = GUIStyle.Green; } else if (selectedTalents.Contains(talentIdentifier)) { - newTalentColor = GUI.Style.Orange; - hoverColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.7f); + newTalentColor = GUIStyle.Orange; + hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f); } talentButton.icon.Color = newTalentColor; @@ -1647,7 +1647,7 @@ namespace Barotrauma private void ApplyTalents(Character controlledCharacter) { selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents); - foreach (string talent in selectedTalents) + foreach (Identifier talent in selectedTalents) { controlledCharacter.GiveTalent(talent); if (GameMain.Client != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs index 4d79d57fd..54c5ee076 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UISprite.cs @@ -19,11 +19,7 @@ namespace Barotrauma private set; } - public bool Slice - { - get; - set; - } + public bool Slice => Slices != null; public Rectangle[] Slices { @@ -54,7 +50,7 @@ namespace Barotrauma public TransitionMode TransitionMode { get; private set; } - public UISprite(XElement element) + public UISprite(ContentXElement element) { Sprite = new Sprite(element); MaintainAspectRatio = element.GetAttributeBool("maintainaspectratio", false); @@ -69,6 +65,7 @@ namespace Barotrauma } Vector4 sliceVec = element.GetAttributeVector4("slice", Vector4.Zero); + Slices = null; if (sliceVec != Vector4.Zero) { minBorderScale = element.GetAttributeFloat("minborderscale", 0.1f); @@ -76,7 +73,6 @@ namespace Barotrauma Rectangle slice = new Rectangle((int)sliceVec.X, (int)sliceVec.Y, (int)(sliceVec.Z - sliceVec.X), (int)(sliceVec.W - sliceVec.Y)); - Slice = true; Slices = new Rectangle[9]; //top-left diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 258c55b37..a5be0901b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -245,7 +245,7 @@ namespace Barotrauma * |----------------------------| */ GUILayoutGroup tooltipLayout = new GUILayoutGroup(rectT(0.95f,0.95f, ItemInfoFrame, Anchor.Center)) { Stretch = true }; - new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty, font: GUI.SubHeadingFont) { UserData = "itemname" }; + new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty, font: GUIStyle.SubHeadingFont) { UserData = "itemname" }; new GUITextBlock(rectT(1, 0, tooltipLayout), TextManager.Get("UpgradeUITooltip.UpgradeListHeader")); new GUIListBox(rectT(1, 0.5f, tooltipLayout), style: null) { ScrollBarVisible = false, AutoHideScrollBar = false, SmoothScroll = true, UserData = "upgradelist"}; new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" }; @@ -268,7 +268,7 @@ namespace Barotrauma GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f }; GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true); GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true); - new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUI.LargeFont); + new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont); categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true }; GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade }; GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs }; @@ -285,9 +285,9 @@ namespace Barotrauma */ GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; - new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUI.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; - new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; + new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); + new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; + new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => { @@ -351,7 +351,7 @@ namespace Barotrauma if (schematicsSprite == null) { return; } float schematicsScale = Math.Min(component.Rect.Width / 2 / schematicsSprite.size.X, component.Rect.Height / schematicsSprite.size.Y); Vector2 center = new Vector2(component.Rect.Center.X, component.Rect.Center.Y); - schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUI.Style.Green, new Vector2(0, schematicsSprite.size.Y / 2), + schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2), scale: schematicsScale); var swappableItemList = selectedUpgradeCategoryLayout?.FindChild("prefablist", true) as GUIListBox; @@ -359,10 +359,10 @@ namespace Barotrauma ItemPrefab swapTo = highlightedElement?.UserData as ItemPrefab ?? selectedItem.PendingItemSwap; if (swapTo?.SwappableItem == null) { return; } Sprite? schematicsSprite2 = swapTo.SwappableItem?.SchematicSprite; - schematicsSprite2?.Draw(spriteBatch, new Vector2(component.Rect.Right, center.Y), GUI.Style.Orange, new Vector2(schematicsSprite2.size.X, schematicsSprite2.size.Y / 2), + schematicsSprite2?.Draw(spriteBatch, new Vector2(component.Rect.Right, center.Y), GUIStyle.Orange, new Vector2(schematicsSprite2.size.X, schematicsSprite2.size.Y / 2), scale: Math.Min(component.Rect.Width / 2 / schematicsSprite2.size.X, component.Rect.Height / schematicsSprite2.size.Y)); - var arrowSprite = GUI.Style?.GetComponentStyle("GUIButtonToggleRight")?.GetDefaultSprite(); + var arrowSprite = GUIStyle.GetComponentStyle("GUIButtonToggleRight")?.GetDefaultSprite(); if (arrowSprite != null) { arrowSprite.Draw(spriteBatch, center, scale: GUI.Scale); @@ -428,7 +428,7 @@ namespace Barotrauma if (AvailableMoney >= hullRepairCost) { - string body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); + LocalizedString body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { if (AvailableMoney >= hullRepairCost) @@ -463,7 +463,7 @@ namespace Barotrauma { if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) { - string body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); + LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) @@ -493,7 +493,7 @@ namespace Barotrauma { foreach (var (item, itemFrame) in itemPreviews) { - itemFrame.OutlineColor = itemFrame.Color = isHovered && item.GetComponent() == null ? GUI.Style.Orange : previewWhite; + itemFrame.OutlineColor = itemFrame.Color = isHovered && item.GetComponent() == null ? GUIStyle.Orange : previewWhite; } return true; }); @@ -509,7 +509,7 @@ namespace Barotrauma if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { - string body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); + LocalizedString body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) @@ -540,7 +540,7 @@ namespace Barotrauma { if (subInfo.LeftBehindDockingPortIDs.Contains(item.ID)) { - itemFrame.OutlineColor = itemFrame.Color = subInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUI.Style.Red : GUI.Style.Green; + itemFrame.OutlineColor = itemFrame.Color = subInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUIStyle.Red : GUIStyle.Green; } else { @@ -551,7 +551,7 @@ namespace Barotrauma }, disableElement: true); } - private void CreateRepairEntry(GUIComponent parent, string title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func? onHover = null, bool disableElement = false) + private void CreateRepairEntry(GUIComponent parent, LocalizedString title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func? onHover = null, bool disableElement = false) { GUIFrame frameChild = new GUIFrame(rectT(new Point(parent.Rect.Width, (int) (96 * GUI.Scale)), parent), style: "UpgradeUIFrame"); frameChild.SelectedColor = frameChild.Color; @@ -569,7 +569,7 @@ namespace Barotrauma GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center), isHorizontal: true); var repairIcon = new GUIFrame(rectT(new Point(contentLayout.Rect.Height, contentLayout.Rect.Height), contentLayout), style: imageStyle); GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; - new GUITextBlock(rectT(1, 0, textLayout), title, font: GUI.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; + new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), 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 = AvailableMoney >= price && !isDisabled, OnClicked = onPressed }; @@ -661,7 +661,7 @@ namespace Barotrauma * |-----------------------------|--------------------------| */ GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center)); - var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUI.SubHeadingFont) { CanBeFocused = false }; + var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; GUILayoutGroup indicatorLayout = new GUILayoutGroup(rectT(0.5f, 0.25f, contentLayout, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.TopRight) { UserData = "indicators", IgnoreLayoutGroups = true, RelativeSpacing = 0.01f }; foreach (var prefab in prefabs) @@ -742,7 +742,7 @@ namespace Barotrauma GUIComponent[] categoryFrames = GetFrames(category); foreach (GUIComponent itemFrame in itemPreviews.Values) { - itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUIStyle.Orange : previewWhite; itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } @@ -790,7 +790,7 @@ namespace Barotrauma GUIComponent[] categoryFrames = GetFrames(category); foreach (GUIComponent itemFrame in itemPreviews.Values) { - itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUIStyle.Orange : previewWhite; itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } return true; @@ -851,8 +851,8 @@ namespace Barotrauma if (linkedItems.Min(it => it.ID) < item.ID) { return; } var currentOrPending = item.PendingItemSwap ?? item.Prefab; - string name = currentOrPending.Name; - string nameWithQuantity = ""; + LocalizedString name = currentOrPending.Name; + LocalizedString nameWithQuantity = ""; if (linkedItems.Count > 1) { foreach (ItemPrefab distinctItem in linkedItems.Select(it => it.Prefab).Distinct()) @@ -881,7 +881,7 @@ namespace Barotrauma }; GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true); - string slotText = ""; + LocalizedString slotText = ""; if (linkedItems.Count > 1) { slotText = TextManager.GetWithVariable("weaponslot", "[number]", string.Join(", ", linkedItems.Select(it => (swappableEntities.IndexOf(it) + 1).ToString()))); @@ -891,13 +891,13 @@ namespace Barotrauma slotText = TextManager.GetWithVariable("weaponslot", "[number]", (swappableEntities.IndexOf(item) + 1).ToString()); } - new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: slotText, font: GUI.SubHeadingFont); + new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: slotText, font: GUIStyle.SubHeadingFont); GUILayoutGroup group = new GUILayoutGroup(rectT(0.7f, 1f, buttonLayout), isHorizontal: true) { Stretch = true }; - string title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : nameWithQuantity; - GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: title, font: GUI.SubHeadingFont, textAlignment: Alignment.Right, parseRichText: true) + var title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : nameWithQuantity; + GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: RichString.Rich(title), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { - TextColor = GUI.Style.Orange + TextColor = GUIStyle.Orange }; GUIImage arrowImage = new GUIImage(rectT(0.5f, 1f, group, scaleBasis: ScaleBasis.BothHeight), style: "SlideDownArrow", scaleToFit: true); @@ -911,7 +911,7 @@ namespace Barotrauma List frames = new List(); if (currentOrPending != null) { - bool canUninstall = item.PendingItemSwap != null || !string.IsNullOrEmpty(currentOrPending.SwappableItem?.ReplacementOnUninstall); + bool canUninstall = item.PendingItemSwap != null || !(currentOrPending.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true); bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall; if (isUninstallPending) { canUninstall = false; } @@ -928,9 +928,7 @@ namespace Barotrauma { string textTag = item.PendingItemSwap != null ? "upgrades.cancelitemswappromptbody" : "upgrades.itemuninstallpromptbody"; if (isUninstallPending) { textTag = "upgrades.cancelitemuninstallpromptbody"; } - string promptBody = TextManager.GetWithVariables(textTag, - new[] { "[itemtouninstall]" }, - new[] { isUninstallPending ? item.Name : currentOrPending.Name }); + LocalizedString promptBody = TextManager.GetWithVariable(textTag, "[itemtouninstall]", isUninstallPending ? item.Name : currentOrPending.Name); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("upgrades.refundprompttitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -970,9 +968,9 @@ namespace Barotrauma buyButton.Enabled = true; buyButton.OnClicked += (button, o) => { - string promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", - new[] { "[itemtoinstall]", "[amount]" }, - new[] { replacement.Name, (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString() }); + LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", + ("[itemtoinstall]", replacement.Name), + ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1019,7 +1017,7 @@ namespace Barotrauma var linkedItems = Campaign.UpgradeManager.GetLinkedItemsToSwap(item); foreach (var itemPreview in itemPreviews) { - itemPreview.Value.OutlineColor = itemPreview.Value.Color = linkedItems.Contains(itemPreview.Key) ? GUI.Style.Orange : previewWhite; + itemPreview.Value.OutlineColor = itemPreview.Value.Color = linkedItems.Contains(itemPreview.Key) ? GUIStyle.Orange : previewWhite; } foreach (GUIComponent otherComponent in toggleButton.Parent.Children) { @@ -1041,7 +1039,7 @@ namespace Barotrauma foreach (var itemPreview in itemPreviews) { if (currentStoreLayout?.SelectedData is CategoryData categoryData && !categoryData.Category.ItemTags.Any(t => itemPreview.Key.HasTag(t))) { continue; } - itemPreview.Value.OutlineColor = itemPreview.Value.Color = GUI.Style.Orange; + itemPreview.Value.OutlineColor = itemPreview.Value.Color = GUIStyle.Orange; } } activeItemSwapSlideDown = toggleButton.Selected ? toggleButton : null; @@ -1058,7 +1056,7 @@ namespace Barotrauma return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category)); } - public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, string title, string body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab upgradePrefab = null, int currentLevel = 0) + public static GUIFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0) { float progressBarHeight = 0.25f; @@ -1080,29 +1078,29 @@ namespace Barotrauma GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center); var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false }; GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout)); - var name = new GUITextBlock(rectT(1, 0.25f, textLayout), title, font: GUI.SubHeadingFont, parseRichText: true) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; + var name = new GUITextBlock(rectT(1, 0.25f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero }; GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout)); - var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; GUILayoutGroup? progressLayout = null; GUILayoutGroup? buyButtonLayout = null; if (addProgressBar) { progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = "progressbar" }; - new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUI.Style.Orange); - new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUI.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + new GUIProgressBar(rectT(0.8f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange); + new GUITextBlock(rectT(0.2f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; } if (addBuyButton) { - string formattedPrice = FormatCurrency(Math.Abs(price)); + var formattedPrice = FormatCurrency(Math.Abs(price)); //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; var priceText = new GUITextBlock(rectT(1, 0.2f, buyButtonLayout), formattedPrice, textAlignment: Alignment.Center); if (price < 0) { - priceText.TextColor = GUI.Style.Green; + priceText.TextColor = GUIStyle.Green; } else if (price == 0) { @@ -1120,8 +1118,8 @@ namespace Barotrauma // cut the description if it overflows and add a tooltip to it for (int i = 100; i > 0 && description.Rect.Height > descriptionLayout.Rect.Height; i--) { - string[] lines = description.WrappedText.Split('\n'); - var newString = string.Join('\n', lines.Take(lines.Length - 1)); + var lines = description.WrappedText.Split('\n'); + var newString = string.Join('\n', lines.Take(lines.Count - 1)); if (0 >= newString.Length - 4) { break; } description.Text = newString.Substring(0, newString.Length - 4) + "..."; @@ -1185,7 +1183,9 @@ namespace Barotrauma buyButton.OnClicked += (button, o) => { - string promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", new []{ "[upgradename]", "[amount]"}, new []{ prefab.Name, prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString() }); + LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", + ("[upgradename]", prefab.Name), + ("[amount]", prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => { if (GameMain.NetworkMember != null) @@ -1225,7 +1225,7 @@ namespace Barotrauma itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls"); if (slotIndex > -1) { - itemName.Text = TextManager.GetWithVariables("weaponslotwithname", new string[] { "[number]", "[weaponname]" }, new string[] { slotIndex.ToString(), itemName.Text }); + itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); } upgradeList.Content.ClearChildren(); for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) @@ -1246,7 +1246,7 @@ namespace Barotrauma { if (textBlock.UserData is Tuple tuple && tuple.Item2 == prefab) { - string tooltip = CreateListEntry(tuple.Item2.Name, level + tuple.Item1); + var tooltip = CreateListEntry(tuple.Item2.Name, level + tuple.Item1); textBlock.Text = tooltip; found = true; break; @@ -1275,7 +1275,7 @@ namespace Barotrauma moreIndicator.CalculateHeightFromText(); layout.Recalculate(); - static string CreateListEntry(string name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", new[] { "[upgradename]", "[level]" }, new[] { name, $"{level}" }); + static LocalizedString CreateListEntry(LocalizedString name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", ("[upgradename]", name), ("[level]", $"{level}")); } public static IEnumerable GetApplicableCategories(Submarine drawnSubmarine) @@ -1343,7 +1343,7 @@ namespace Barotrauma if (selectedUpgradeCategoryLayout != null) { var linkedItems = HoveredItem is Item ? Campaign.UpgradeManager.GetLinkedItemsToSwap((Item)HoveredItem) : new List(); - if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem || linkedItems.Contains(c.UserData as Item), recursive: true) is GUIButton itemElement) + if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem || linkedItems.Contains((Item)c.UserData), recursive: true) is GUIButton itemElement) { if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); @@ -1417,9 +1417,9 @@ namespace Barotrauma */ submarineInfoFrame = new GUILayoutGroup(rectT(0.25f, 0.2f, mainStoreLayout, Anchor.TopRight)) { IgnoreLayoutGroups = true }; // submarine name - new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUI.LargeFont); + new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont); // submarine class - new GUITextBlock(rectT(1, 0, submarineInfoFrame), $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}", textAlignment: Alignment.Right, font: GUI.Font); + new GUITextBlock(rectT(1, 0, submarineInfoFrame), $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}", textAlignment: Alignment.Right, font: GUIStyle.Font); var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true); submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale)); @@ -1448,7 +1448,7 @@ namespace Barotrauma Point size = new Point((int) (spriteSize * item.Scale / dockedBorders.Width * hullContainer.Rect.Width)); itemFrame = new GUIImage(rectT(size, component, Anchor.Center), icon, scaleToFit: true) { - SelectedColor = GUI.Style.Orange, + SelectedColor = GUIStyle.Orange, Color = previewWhite, HoverCursor = CursorState.Hand, SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None @@ -1457,7 +1457,7 @@ namespace Barotrauma { new GUIImage(new RectTransform(new Vector2(0.8f), itemFrame.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(-0.2f) }, "WeaponSwitchIcon.DropShadow", scaleToFit: true) { - SelectedColor = GUI.Style.Orange, + SelectedColor = GUIStyle.Orange, Color = previewWhite, CanBeFocused = false }; @@ -1468,7 +1468,7 @@ namespace Barotrauma Point size = new Point((int) (item.Rect.Width * item.Scale / dockedBorders.Width * hullContainer.Rect.Width), (int) (item.Rect.Height * item.Scale / dockedBorders.Height * hullContainer.Rect.Height)); itemFrame = new GUIFrame(rectT(size, component, Anchor.Center), style: "ScanLines") { - SelectedColor = GUI.Style.Orange, + SelectedColor = GUIStyle.Orange, OutlineColor = previewWhite, Color = previewWhite, OutlineThickness = 2, @@ -1540,7 +1540,7 @@ namespace Barotrauma // calculate the center point so we can draw a line from X to Y instead of drawing a rotated rectangle that is filled Vector2 point1 = hullVertex[1] + (hullVertex[2] - hullVertex[1]) / 2; Vector2 point2 = hullVertex[0] + (hullVertex[3] - hullVertex[0]) / 2; - GUI.DrawLine(spriteBatch, point1, point2, (highlightWalls ? GUI.Style.Orange * 0.6f : Color.DarkCyan * 0.3f), width: 10); + GUI.DrawLine(spriteBatch, point1, point2, (highlightWalls ? GUIStyle.Orange * 0.6f : Color.DarkCyan * 0.3f), width: 10); if (GameMain.DebugDraw) { // the "collision box" is a bit bigger than the line we draw so this can be useful data (maybe) @@ -1553,14 +1553,14 @@ namespace Barotrauma { int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category); - string progressText = TextManager.GetWithVariables("upgrades.progressformat", new[] { "[level]", "[maxlevel]" }, new[] { currentLevel.ToString(), prefab.MaxLevel.ToString() }); + LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", prefab.MaxLevel.ToString())); if (prefabFrame.FindChild("progressbar", true) is { } progressParent) { GUIProgressBar bar = progressParent.GetChild(); if (bar != null) { bar.BarSize = currentLevel / (float) prefab.MaxLevel; - bar.Color = currentLevel >= prefab.MaxLevel ? GUI.Style.Green : GUI.Style.Orange; + bar.Color = currentLevel >= prefab.MaxLevel ? GUIStyle.Green : GUIStyle.Orange; } GUITextBlock block = progressParent.GetChild(); @@ -1620,7 +1620,7 @@ namespace Barotrauma else { parent.Enabled = false; - parent.SelectedColor = GUI.Style.Red * 0.5f; + parent.SelectedColor = GUIStyle.Red * 0.5f; } } @@ -1632,12 +1632,12 @@ namespace Barotrauma { if (component.UserData != prefab) { continue; } - Dictionary styles = GUI.Style.GetComponentStyle("upgradeindicator").ChildStyles; + Dictionary styles = GUIStyle.GetComponentStyle("upgradeindicator").ChildStyles; if (!styles.ContainsKey("upgradeindicatoron") || !styles.ContainsKey("upgradeindicatordim") || !styles.ContainsKey("upgradeindicatoroff")) { continue; } - GUIComponentStyle onStyle = styles["upgradeindicatoron"]; - GUIComponentStyle dimStyle = styles["upgradeindicatordim"]; - GUIComponentStyle offStyle = styles["upgradeindicatoroff"]; + GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()]; + GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()]; + GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()]; if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= prefab.MaxLevel) { @@ -1694,7 +1694,7 @@ namespace Barotrauma private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); - public static string FormatCurrency(int money, bool format = true) + public static LocalizedString FormatCurrency(int money, bool format = true) { return TextManager.GetWithVariable("CurrencyFormat", "[credits]", format ? string.Format(CultureInfo.InvariantCulture, "{0:N0}", money) : money.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs index d3feabe8f..5b596f54d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs @@ -34,23 +34,29 @@ namespace Barotrauma public class TextSettings { - public string Text; + public LocalizedString Text; public int Width; + public TextSettings(Identifier textTag, int width) + { + Text = TextManager.GetFormatted(textTag); + Width = width; + } + public TextSettings(XElement element) { - Text = TextManager.GetFormatted(element.GetAttributeString("text", string.Empty), true); + Text = TextManager.GetFormatted(element.GetAttributeIdentifier("text", Identifier.Empty)); Width = element.GetAttributeInt("width", 450); } } public class VideoSettings { - public string File; + public readonly string File; - public VideoSettings(XElement element) + public VideoSettings(string file) { - File = element.GetAttributeString("file", string.Empty); + File = file; } } @@ -75,13 +81,13 @@ namespace Barotrauma } videoView = new GUICustomComponent(new RectTransform(Point.Zero, videoFrame.RectTransform, Anchor.Center), (spriteBatch, guiCustomComponent) => { DrawVideo(spriteBatch, guiCustomComponent.Rect); }); - title = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUI.LargeFont, textColor: new Color(253, 174, 0), textAlignment: Alignment.Left); + title = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUIStyle.LargeFont, textColor: new Color(253, 174, 0), textAlignment: Alignment.Left); - textContent = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUI.Font, textAlignment: Alignment.TopLeft); + textContent = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUIStyle.Font, textAlignment: Alignment.TopLeft); - objectiveTitle = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight, textColor: Color.White); + objectiveTitle = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight, textColor: Color.White); objectiveTitle.Text = TextManager.Get("Tutorial.NewObjective"); - objectiveText = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUI.SubHeadingFont, textColor: new Color(4, 180, 108), textAlignment: Alignment.CenterRight); + objectiveText = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUIStyle.SubHeadingFont, textColor: new Color(4, 180, 108), textAlignment: Alignment.CenterRight); objectiveTitle.Visible = objectiveText.Visible = false; } @@ -120,7 +126,12 @@ namespace Barotrauma background.AddToGUIUpdateList(ignoreChildren, order); } - public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, string contentId, bool startPlayback, string objective = "", Action callback = null) + public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback) + { + LoadContent(contentPath, videoSettings, textSettings, contentId, startPlayback, new RawLString(""), null); + } + + public void LoadContent(string contentPath, VideoSettings videoSettings, TextSettings textSettings, Identifier contentId, bool startPlayback, LocalizedString objective, Action callback = null) { callbackOnStop = callback; filePath = contentPath + videoSettings.File; @@ -183,10 +194,10 @@ namespace Barotrauma title.RectTransform.NonScaledSize = new Point(scaledTextWidth, scaledTitleHeight); title.RectTransform.AbsoluteOffset = new Point((int)(5 * GUI.Scale), (int)(10 * GUI.Scale)); - if (textSettings != null && !string.IsNullOrEmpty(textSettings.Text)) + if (textSettings != null && !textSettings.Text.IsNullOrEmpty()) { - textSettings.Text = ToolBox.WrapText(textSettings.Text, scaledTextWidth, GUI.Font); - int wrappedHeight = textSettings.Text.Split('\n').Length * scaledTextHeight; + textSettings.Text = ToolBox.WrapText(textSettings.Text, scaledTextWidth, GUIStyle.Font); + int wrappedHeight = textSettings.Text.Value.Split('\n').Length * scaledTextHeight; textFrame.RectTransform.NonScaledSize = new Point(scaledTextWidth + scaledBorderSize, wrappedHeight + scaledBorderSize + scaledButtonSize.Y + scaledTitleHeight); @@ -203,7 +214,7 @@ namespace Barotrauma textContent.RectTransform.AbsoluteOffset = new Point(0, scaledBorderSize + scaledTitleHeight); } - if (!string.IsNullOrEmpty(objectiveText.Text)) + if (!objectiveText.Text.IsNullOrEmpty()) { int scaledXOffset = (int)(-10 * GUI.Scale); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 11489f117..4f5541602 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -19,12 +18,11 @@ namespace Barotrauma private Func getYesVotes, getNoVotes, getMaxVotes; private bool votePassed; - private string votingOnText; - private List votingOnTextData; + private RichString votingOnText; private float votingTime = 100f; private float timer; private VoteType currentVoteType; - private Color submarineColor => GUI.Style.Orange; + private Color submarineColor => GUIStyle.Orange; private Point createdForResolution; public VotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) @@ -60,14 +58,14 @@ namespace Barotrauma int yOffset = padding; int paddedWidth = frame.Rect.Width - padding * 2; - votingTextBlock = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), votingOnTextData, votingOnText, wrap: true); + votingTextBlock = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), votingOnText, wrap: true); votingTextBlock.RectTransform.NonScaledSize = votingTextBlock.RectTransform.MinSize = votingTextBlock.RectTransform.MaxSize = new Point(votingTextBlock.Rect.Width, votingTextBlock.Rect.Height); votingTextBlock.RectTransform.IsFixedSize = true; votingTextBlock.RectTransform.AbsoluteOffset = new Point(padding, yOffset); yOffset += votingTextBlock.Rect.Height + spacing; - voteCounter = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), "(0/0)", GUI.Style.Green, textAlignment: Alignment.Center); + voteCounter = new GUITextBlock(new RectTransform(new Point(paddedWidth, 0), frame.RectTransform), "(0/0)", GUIStyle.Green, textAlignment: Alignment.Center); voteCounter.RectTransform.NonScaledSize = voteCounter.RectTransform.MinSize = voteCounter.RectTransform.MaxSize = new Point(voteCounter.Rect.Width, voteCounter.Rect.Height); voteCounter.RectTransform.IsFixedSize = true; voteCounter.RectTransform.AbsoluteOffset = new Point(padding, yOffset); @@ -150,26 +148,41 @@ namespace Barotrauma switch (type) { case VoteType.PurchaseAndSwitchSub: - votingOnText = TextManager.GetWithVariables("submarinepurchaseandswitchvote", new string[] { "[playername]", "[submarinename]", "[amount]", "[currencyname]" }, new string[] { characterRichString, submarineRichString, info.Price.ToString(), TextManager.Get("credit").ToLower() }); + votingOnText = TextManager.GetWithVariables("submarinepurchaseandswitchvote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.PurchaseSub: - votingOnText = TextManager.GetWithVariables("submarinepurchasevote", new string[] { "[playername]", "[submarinename]", "[amount]", "[currencyname]" }, new string[] { characterRichString, submarineRichString, info.Price.ToString(), TextManager.Get("credit").ToLower() }); + votingOnText = TextManager.GetWithVariables("submarinepurchasevote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.SwitchSub: int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - votingOnText = TextManager.GetWithVariables("submarineswitchfeevote", new string[] { "[playername]", "[submarinename]", "[locationname]", "[amount]", "[currencyname]" }, new string[] { characterRichString, submarineRichString, endLocation.Name, deliveryFee.ToString(), TextManager.Get("credit").ToLower() }); + votingOnText = TextManager.GetWithVariables("submarineswitchfeevote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString), + ("[locationname]", endLocation.Name), + ("[amount]", deliveryFee.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower())); } else { - votingOnText = TextManager.GetWithVariables("submarineswitchnofeevote", new string[] { "[playername]", "[submarinename]" }, new string[] { characterRichString, submarineRichString }); + votingOnText = TextManager.GetWithVariables("submarineswitchnofeevote", + ("[playername]", characterRichString), + ("[submarinename]", submarineRichString)); } break; } - votingOnTextData = RichTextData.GetRichTextData(votingOnText, out votingOnText); + votingOnText = RichString.Rich(votingOnText); } private int SubmarineYesVotes() @@ -189,31 +202,50 @@ namespace Barotrauma private void SendSubmarineVoteEndMessage(SubmarineInfo info, VoteType type) { - GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes.ToString(), noVotes.ToString(), votePassed), ChatMessageType.Server); + GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes.ToString(), noVotes.ToString(), votePassed).Value, ChatMessageType.Server); } - public static string GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, string yesVoteString, string noVoteString, bool votePassed) + public static LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, string yesVoteString, string noVoteString, bool votePassed) { - string result = string.Empty; + LocalizedString result = string.Empty; switch (type) { case VoteType.PurchaseAndSwitchSub: - result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", new string[] { "[submarinename]", "[amount]", "[currencyname]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, info.Price.ToString(), TextManager.Get("credit").ToLower(), yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", + ("[submarinename]", info.DisplayName), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower()), + ("[yesvotecount]", yesVoteString), + ("[novotecount]" , noVoteString)); break; case VoteType.PurchaseSub: - result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", new string[] { "[submarinename]", "[amount]", "[currencyname]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, info.Price.ToString(), TextManager.Get("credit").ToLower(), yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", + ("[submarinename]", info.DisplayName), + ("[amount]", info.Price.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower()), + ("[yesvotecount]", yesVoteString), + ("[novotecount]", noVoteString)); break; case VoteType.SwitchSub: int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); if (deliveryFee > 0) { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", new string[] { "[submarinename]", "[locationname]", "[amount]", "[currencyname]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, endLocation.Name, deliveryFee.ToString(), TextManager.Get("credit").ToLower(), yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", + ("[submarinename]", info.DisplayName), + ("[locationname]", endLocation.Name), + ("[amount]", deliveryFee.ToString()), + ("[currencyname]", TextManager.Get("credit").ToLower()), + ("[yesvotecount]", yesVoteString), + ("[novotecount]", noVoteString)); } else { - result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", new string[] { "[submarinename]", "[yesvotecount]", "[novotecount]" }, new string[] { info.DisplayName, yesVoteString, noVoteString }); + result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", + ("[submarinename]", info.DisplayName), + ("[yesvotecount]", yesVoteString), + ("[novotecount]", noVoteString)); } break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs index 2fdbdf0d8..a7a5216d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs @@ -17,7 +17,7 @@ namespace Barotrauma } public Shape shape; - public string tooltip; + public LocalizedString tooltip; public bool showTooltip = true; public Rectangle DrawRect => new Rectangle((int)(DrawPos.X - (float)size / 2), (int)(DrawPos.Y - (float)size / 2), size, size); public Rectangle InputRect @@ -42,7 +42,7 @@ namespace Barotrauma /// public bool isFilled; public int inputAreaMargin; - public Color color = GUI.Style.Red; + public Color color = GUIStyle.Red; public Color? secondaryColor; public Color textColor = Color.White; public Color textBackgroundColor = Color.Black * 0.5f; @@ -183,7 +183,7 @@ namespace Barotrauma } if (IsSelected) { - if (showTooltip && !string.IsNullOrEmpty(tooltip)) + if (showTooltip && !tooltip.IsNullOrEmpty()) { var offset = tooltipOffset ?? new Vector2(size, -size / 2f); GUI.DrawString(spriteBatch, DrawPos + offset, tooltip, textColor, textBackgroundColor); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index 8f0c177f2..e8069b34b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -20,8 +20,8 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(15) }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentheader"), font: GUI.SubHeadingFont, textColor: Color.White); - var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsenttext"), wrap: true, parseRichText: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("statisticsconsentheader"), font: GUIStyle.SubHeadingFont, textColor: Color.White); + var mainText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), RichString.Rich(TextManager.Get("statisticsconsenttext")), wrap: true); foreach (var data in mainText.RichTextData) { @@ -93,7 +93,7 @@ namespace Barotrauma { if (child is GUITextBlock textBlock) { - textBlock.TextScale = MathHelper.Min(1.0f, 1.0f / GameSettings.TextScale); + textBlock.TextScale = MathHelper.Min(1.0f, 1.0f / GameSettings.CurrentConfig.Graphics.TextScale); textBlock.RectTransform.MinSize = new Point(0, (int)textBlock.TextSize.Y); textBlock.RectTransform.MaxSize = new Point(int.MaxValue, (int)textBlock.TextSize.Y); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index f908466c8..c1ed2807f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -45,8 +45,17 @@ namespace Barotrauma public static MainMenuScreen MainMenuScreen; public static NetLobbyScreen NetLobbyScreen; + public static ModDownloadScreen ModDownloadScreen; + + public static void ResetNetLobbyScreen() + { + NetLobbyScreen?.Release(); + NetLobbyScreen = new NetLobbyScreen(); + ModDownloadScreen?.Release(); + ModDownloadScreen = new ModDownloadScreen(); + } + public static ServerListScreen ServerListScreen; - public static SteamWorkshopScreen SteamWorkshopScreen; public static SubEditorScreen SubEditorScreen; public static TestScreen TestScreen; @@ -64,19 +73,7 @@ namespace Barotrauma public static Thread MainThread { get; private set; } - private static ContentPackage vanillaContent; - public static ContentPackage VanillaContent - { - get - { - if (vanillaContent == null) - { - // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.CorePackages.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); - } - return vanillaContent; - } - } + public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; private static GameSession gameSession; public static GameSession GameSession @@ -94,7 +91,6 @@ namespace Barotrauma } public static ParticleManager ParticleManager; - public static DecalManager DecalManager; private static World world; public static World World @@ -110,10 +106,8 @@ namespace Barotrauma public static LoadingScreen TitleScreen; private bool loadingScreenOpen; - public static GameSettings Config; - private CoroutineHandle loadingCoroutine; - private bool hasLoaded; + public bool HasLoaded { get; private set; } private readonly GameTime fixedTime; @@ -232,9 +226,9 @@ namespace Barotrauma throw new Exception("Content folder not found. If you are trying to compile the game from the source code and own a legal copy of the game, you can copy the Content folder from the game's files to BarotraumaShared/Content."); } - Config = new GameSettings(); - - Md5Hash.LoadCache(); + GameSettings.Init(); + + Md5Hash.Cache.Load(); ConsoleArguments = args; @@ -290,24 +284,42 @@ namespace Barotrauma public void ApplyGraphicsSettings() { - GraphicsWidth = Config.GraphicsWidth; - GraphicsHeight = Config.GraphicsHeight; - switch (Config.WindowMode) + void updateConfig() + { + var config = GameSettings.CurrentConfig; + config.Graphics.Width = GraphicsWidth; + config.Graphics.Height = GraphicsHeight; + GameSettings.SetCurrentConfig(config); + } + + GraphicsWidth = GameSettings.CurrentConfig.Graphics.Width; + GraphicsHeight = GameSettings.CurrentConfig.Graphics.Height; + + if (GraphicsWidth <= 0 || GraphicsHeight <= 0) + { + GraphicsWidth = GraphicsDevice.DisplayMode.Width; + GraphicsHeight = GraphicsDevice.DisplayMode.Height; + updateConfig(); + } + + switch (GameSettings.CurrentConfig.Graphics.DisplayMode) { case WindowMode.BorderlessWindowed: GraphicsWidth = GraphicsDevice.DisplayMode.Width; GraphicsHeight = GraphicsDevice.DisplayMode.Height; + updateConfig(); break; case WindowMode.Windowed: GraphicsWidth = Math.Min(GraphicsDevice.DisplayMode.Width, GraphicsWidth); GraphicsHeight = Math.Min(GraphicsDevice.DisplayMode.Height, GraphicsHeight); + updateConfig(); break; } GraphicsDeviceManager.GraphicsProfile = GfxProfile; GraphicsDeviceManager.PreferredBackBufferFormat = SurfaceFormat.Color; GraphicsDeviceManager.PreferMultiSampling = false; - GraphicsDeviceManager.SynchronizeWithVerticalRetrace = Config.VSyncEnabled; - SetWindowMode(Config.WindowMode); + GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync; + SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode); defaultViewport = GraphicsDevice.Viewport; @@ -317,8 +329,8 @@ namespace Barotrauma public void SetWindowMode(WindowMode windowMode) { WindowMode = windowMode; - GraphicsDeviceManager.HardwareModeSwitch = Config.WindowMode != WindowMode.BorderlessWindowed; - GraphicsDeviceManager.IsFullScreen = Config.WindowMode == WindowMode.Fullscreen || Config.WindowMode == WindowMode.BorderlessWindowed; + GraphicsDeviceManager.HardwareModeSwitch = windowMode != WindowMode.BorderlessWindowed; + GraphicsDeviceManager.IsFullScreen = windowMode == WindowMode.Fullscreen || windowMode == WindowMode.BorderlessWindowed; Window.IsBorderless = !GraphicsDeviceManager.HardwareModeSwitch; GraphicsDeviceManager.PreferredBackBufferWidth = GraphicsWidth; @@ -390,7 +402,7 @@ namespace Barotrauma loadingScreenOpen = true; TitleScreen = new LoadingScreen(GraphicsDevice) { - WaitForLanguageSelection = Config.ShowLanguageSelectionPrompt + WaitForLanguageSelection = GameSettings.CurrentConfig.Language == LanguageIdentifier.None }; bool canLoadInSeparateThread = true; @@ -407,27 +419,31 @@ namespace Barotrauma private IEnumerable Load(bool isSeparateThread) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("LOADING COROUTINE", Color.Lime); } - while (TitleScreen.WaitForLanguageSelection) + ContentPackageManager.LoadVanillaFileList(); + + if (TitleScreen.WaitForLanguageSelection) { - yield return CoroutineStatus.Running; + ContentPackageManager.VanillaCorePackage.LoadFilesOfType(); + TitleScreen.AvailableLanguages = TextManager.AvailableLanguages.ToArray(); + while (TitleScreen.WaitForLanguageSelection) + { + yield return CoroutineStatus.Running; + } + ContentPackageManager.VanillaCorePackage.UnloadFilesOfType(); } SoundManager = new Sounds.SoundManager(); - SoundManager.SetCategoryGainMultiplier("default", Config.SoundVolume, 0); - SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume, 0); - SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume, 0); - SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume, 0); - SoundManager.SetCategoryGainMultiplier("voip", Math.Min(Config.VoiceChatVolume, 1.0f), 0); + SoundManager.ApplySettings(); - if (Config.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) + if (GameSettings.CurrentConfig.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) { var pendingSplashScreens = TitleScreen.PendingSplashScreens; - float baseVolume = MathHelper.Clamp(Config.SoundVolume * 2.0f, 0.0f, 1.0f); + float baseVolume = MathHelper.Clamp(GameSettings.CurrentConfig.Audio.SoundVolume * 2.0f, 0.0f, 1.0f); pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_UTG.webm", baseVolume * 0.5f)); pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_FF.webm", baseVolume)); pendingSplashScreens?.Enqueue(new LoadingScreen.PendingSplashScreen("Content/SplashScreens/Splash_Daedalic.webm", baseVolume * 0.1f)); @@ -443,153 +459,52 @@ namespace Barotrauma } } - GUI.Init(Window, Config.AllEnabledPackages, GraphicsDevice); + GUI.Init(); + + yield return CoroutineStatus.Running; + + var contentPackageLoadRoutine = ContentPackageManager.Init(); + foreach (var progress in contentPackageLoadRoutine) + { + const float min = 1f, max = 70f; + TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value); + yield return CoroutineStatus.Running; + } + DebugConsole.Init(); - if (Config.AutoUpdateWorkshopItems) - { - Config.WaitingForAutoUpdate = true; - TaskPool.Add("AutoUpdateWorkshopItemsAsync", - SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => - { - if (!task.TryGetResult(out bool result)) { return; } - - Config.WaitingForAutoUpdate = false; - }); - - while (Config.WaitingForAutoUpdate) { yield return CoroutineStatus.Running; } - } - -#if DEBUG - if (Config.ModBreakerMode) - { - Config.SelectCorePackage(ContentPackage.CorePackages.GetRandom()); - foreach (var regularPackage in ContentPackage.RegularPackages) - { - if (Rand.Range(0.0, 1.0) <= 0.5) - { - Config.EnableRegularPackage(regularPackage); - } - else - { - Config.DisableRegularPackage(regularPackage); - } - } - ContentPackage.SortContentPackages(p => - { - return Rand.Int(int.MaxValue); - }); - } -#endif - - if (Config.AllEnabledPackages.None()) - { - DebugConsole.Log("No content packages selected"); - } - else - { - DebugConsole.Log("Selected content packages: " + string.Join(", ", Config.AllEnabledPackages.Select(cp => cp.Name))); - } - #if !DEBUG && !OSX GameAnalyticsManager.InitIfConsented(); #endif - yield return CoroutineStatus.Running; - - Debug.WriteLine("sounds"); - - int i = 0; - foreach (CoroutineStatus status in SoundPlayer.Init()) - { - if (status == CoroutineStatus.Success) break; - - i++; - TitleScreen.LoadState = SoundPlayer.SoundCount == 0 ? - 1.0f : - Math.Min(40.0f * i / Math.Max(SoundPlayer.SoundCount, 1), 40.0f); - - yield return CoroutineStatus.Running; - } - - TitleScreen.LoadState = 40.0f; - yield return CoroutineStatus.Running; - - LightManager = new Lights.LightManager(base.GraphicsDevice, Content); - - TitleScreen.LoadState = 41.0f; - yield return CoroutineStatus.Running; - - GUI.LoadContent(); - TitleScreen.LoadState = 42.0f; - - yield return CoroutineStatus.Running; - TaskPool.Add("InitRelayNetworkAccess", SteamManager.InitRelayNetworkAccess(), (t) => { }); - FactionPrefab.LoadFactions(); - NPCSet.LoadSets(); - CharacterPrefab.LoadAll(); - MissionPrefab.Init(); - TraitorMissionPrefab.Init(); - MapEntityPrefab.Init(); - Tutorials.Tutorial.Init(); - MapGenerationParams.Init(); - LevelGenerationParams.LoadPresets(); - CaveGenerationParams.LoadPresets(); - OutpostGenerationParams.LoadPresets(); - WreckAIConfig.LoadAll(); - EventSet.LoadPrefabs(); - ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); - AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); - SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); - TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); - TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); - Order.Init(); - EventManagerSettings.Init(); - BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); HintManager.Init(); - TitleScreen.LoadState = 50.0f; yield return CoroutineStatus.Running; - - StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); - TitleScreen.LoadState = 55.0f; - yield return CoroutineStatus.Running; - - UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules)); - TitleScreen.LoadState = 56.0f; - yield return CoroutineStatus.Running; - - JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); - CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); - - NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); - - ItemAssemblyPrefab.LoadAll(); - TitleScreen.LoadState = 60.0f; - yield return CoroutineStatus.Running; - + CoreEntityPrefab.InitCorePrefabs(); GameModePreset.Init(); SaveUtil.DeleteDownloadedSubs(); SubmarineInfo.RefreshSavedSubs(); - TitleScreen.LoadState = 65.0f; + TitleScreen.LoadState = 75.0f; yield return CoroutineStatus.Running; GameScreen = new GameScreen(GraphicsDeviceManager.GraphicsDevice, Content); - TitleScreen.LoadState = 68.0f; + ParticleManager = new ParticleManager(GameScreen.Cam); + LightManager = new Lights.LightManager(base.GraphicsDevice, Content); + + TitleScreen.LoadState = 80.0f; yield return CoroutineStatus.Running; MainMenuScreen = new MainMenuScreen(this); ServerListScreen = new ServerListScreen(); - TitleScreen.LoadState = 70.0f; + TitleScreen.LoadState = 85.0f; yield return CoroutineStatus.Running; #if USE_STEAM - SteamWorkshopScreen = new SteamWorkshopScreen(); if (SteamManager.IsInitialized) { Steamworks.SteamFriends.OnGameRichPresenceJoinRequested += OnInvitedToGame; @@ -599,25 +514,25 @@ namespace Barotrauma { //check the achievements too, so we don't consider people who've played the game before this "gamelaunchcount" stat was added as being 1st-time-players //(people who have played previous versions, but not unlocked any achievements, will be incorrectly considered 1st-time-players, but that should be a small enough group to not skew the statistics) - if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount") <= 0) + if (!achievements.Any() && SteamManager.GetStatInt("gamelaunchcount".ToIdentifier()) <= 0) { IsFirstLaunch = true; GameAnalyticsManager.AddDesignEvent("FirstLaunch"); } } - SteamManager.IncrementStat("gamelaunchcount", 1); + SteamManager.IncrementStat("gamelaunchcount".ToIdentifier(), 1); } #endif SubEditorScreen = new SubEditorScreen(); TestScreen = new TestScreen(); - TitleScreen.LoadState = 75.0f; + TitleScreen.LoadState = 90.0f; yield return CoroutineStatus.Running; ParticleEditorScreen = new ParticleEditorScreen(); - TitleScreen.LoadState = 80.0f; + TitleScreen.LoadState = 95.0f; yield return CoroutineStatus.Running; LevelEditorScreen = new LevelEditorScreen(); @@ -628,31 +543,20 @@ namespace Barotrauma yield return CoroutineStatus.Running; - TitleScreen.LoadState = 85.0f; - ParticleManager = new ParticleManager(GameScreen.Cam); - ParticleManager.LoadPrefabs(); - TitleScreen.LoadState = 88.0f; - LevelObjectPrefab.LoadAll(); - - TitleScreen.LoadState = 90.0f; - yield return CoroutineStatus.Running; - - DecalManager = new DecalManager(); - LocationType.Init(); MainMenuScreen.Select(); - foreach (string steamError in SteamManager.InitializationErrors) + foreach (Identifier steamError in SteamManager.InitializationErrors) { new GUIMessageBox(TextManager.Get("Error"), TextManager.Get(steamError)); } TitleScreen.LoadState = 100.0f; - hasLoaded = true; - if (GameSettings.VerboseLogging) + HasLoaded = true; + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("LOADING COROUTINE FINISHED", Color.Lime); } - yield return CoroutineStatus.Success; + yield return CoroutineStatus.Success; } @@ -670,23 +574,6 @@ namespace Barotrauma MainThread = null; } - /// - /// Returns the file paths of all files of the given type in the content packages. - /// - /// - /// If true, also returns files in content packages that are installed but not currently selected. - public IEnumerable GetFilesOfType(ContentType type, bool searchAllContentPackages = false) - { - if (searchAllContentPackages) - { - return ContentPackage.GetFilesOfType(ContentPackage.AllPackages, type); - } - else - { - return ContentPackage.GetFilesOfType(Config.AllEnabledPackages, type); - } - } - public void OnInvitedToGame(Steamworks.Friend friend, string connectCommand) => OnInvitedToGame(connectCommand); public void OnInvitedToGame(string connectCommand) @@ -737,7 +624,7 @@ namespace Barotrauma if (SoundManager != null) { - if (WindowActive || !Config.MuteOnFocusLost) + if (WindowActive || !GameSettings.CurrentConfig.Audio.MuteOnFocusLost) { SoundManager.ListenerGain = SoundManager.CompressionDynamicRangeGain; } @@ -786,20 +673,23 @@ namespace Barotrauma CancelQuickStart = !CancelQuickStart; } - if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled || Config.TestScreenEnabled) && FirstLoad && !CancelQuickStart) + if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && + (GameSettings.CurrentConfig.AutomaticQuickStartEnabled || + GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled || + GameSettings.CurrentConfig.TestScreenEnabled) && FirstLoad && !CancelQuickStart) { loadingScreenOpen = false; FirstLoad = false; - if (Config.TestScreenEnabled) + if (GameSettings.CurrentConfig.TestScreenEnabled) { TestScreen.Select(); - } - else if (Config.AutomaticQuickStartEnabled) + } + else if (GameSettings.CurrentConfig.AutomaticQuickStartEnabled) { MainMenuScreen.QuickStart(); } - else if (Config.AutomaticCampaignLoadEnabled) + else if (GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled) { IEnumerable saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); @@ -822,12 +712,12 @@ namespace Barotrauma NetworkMember?.Update((float)Timing.Step); - if (!hasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) + if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { throw new LoadingException(loadingCoroutine.Exception); } } - else if (hasLoaded) + else if (HasLoaded) { if (ConnectLobby != 0) { @@ -854,7 +744,7 @@ namespace Barotrauma GameMain.MainMenuScreen.Select(); } UInt64 serverSteamId = SteamManager.SteamIDStringToUInt64(ConnectEndpoint); - Client = new GameClient(Config.PlayerName, + Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), serverSteamId != 0 ? null : ConnectEndpoint, serverSteamId, string.IsNullOrWhiteSpace(ConnectName) ? ConnectEndpoint : ConnectName); @@ -888,9 +778,9 @@ namespace Barotrauma { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } - else if (Tutorial.Initialized && Tutorial.ContentRunning) + else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { - (GameSession.GameMode as TutorialMode).Tutorial.CloseActiveContentGUI(); + tutorialMode.Tutorial.CloseActiveContentGUI(); } else if (GameSession.IsTabMenuOpen) { @@ -935,8 +825,11 @@ namespace Barotrauma #endif GUI.ClearUpdateList(); - Paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning || DebugConsole.Paused) && - (NetworkMember == null || !NetworkMember.GameStarted); + Paused = + (DebugConsole.IsOpen || DebugConsole.Paused || + GUI.PauseMenuOpen || GUI.SettingsMenuOpen || + (GameSession?.GameMode is TutorialMode tutoMode && tutoMode.Tutorial.ContentRunning)) && + (NetworkMember == null || !NetworkMember.GameStarted); if (GameSession?.GameMode != null && GameSession.GameMode.Paused) { Paused = true; @@ -944,7 +837,7 @@ namespace Barotrauma } #if !DEBUG - if (NetworkMember == null && !WindowActive && !Paused && true && Config.PauseOnFocusLost && + if (NetworkMember == null && !WindowActive && !Paused && true && GameSettings.CurrentConfig.PauseOnFocusLost && Screen.Selected != MainMenuScreen && Screen.Selected != ServerListScreen && Screen.Selected != NetLobbyScreen && Screen.Selected != SubEditorScreen && Screen.Selected != LevelEditorScreen) { @@ -969,9 +862,9 @@ namespace Barotrauma { Screen.Selected.Update(Timing.Step); } - else if (Tutorial.Initialized && Tutorial.ContentRunning) + else if (GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { - (GameSession.GameMode as TutorialMode).Update((float)Timing.Step); + tutorialMode.Update((float)Timing.Step); } else { @@ -988,6 +881,27 @@ namespace Barotrauma NetworkMember?.Update((float)Timing.Step); GUI.Update((float)Timing.Step); + +#if DEBUG + if (DebugDraw && GUI.MouseOn != null && PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.G)) + { + List hierarchy = new List(); + var currComponent = GUI.MouseOn; + while (currComponent != null) + { + hierarchy.Add(currComponent); + currComponent = currComponent.Parent; + } + DebugConsole.NewMessage("*********************"); + foreach (var component in hierarchy) + { + if (component is { MouseRect: var mouseRect, Rect: var rect }) + { + DebugConsole.NewMessage($"{component.GetType().Name} {component.Style?.Name ?? "[null]"} {rect.Bottom} {mouseRect.Bottom}", mouseRect!=rect ? Color.Lime : Color.Red); + } + } + } +#endif } CoroutineManager.Update((float)Timing.Step, Paused ? 0.0f : (float)Timing.Step); @@ -1035,7 +949,7 @@ namespace Barotrauma if (Timing.FrameLimit > 0) { double step = 1.0 / Timing.FrameLimit; - while (!Config.VSyncEnabled && sw.Elapsed.TotalSeconds + deltaTime < step) + while (!GameSettings.CurrentConfig.Graphics.VSync && sw.Elapsed.TotalSeconds + deltaTime < step) { Thread.Sleep(1); } @@ -1047,7 +961,7 @@ namespace Barotrauma { TitleScreen.Draw(spriteBatch, base.GraphicsDevice, (float)deltaTime); } - else if (hasLoaded) + else if (HasLoaded) { Screen.Selected.Draw(deltaTime, base.GraphicsDevice, spriteBatch); } @@ -1055,8 +969,33 @@ namespace Barotrauma if (DebugDraw && GUI.MouseOn != null) { spriteBatch.Begin(); - GUI.DrawRectangle(spriteBatch, GUI.MouseOn.MouseRect, Color.Lime); - GUI.DrawRectangle(spriteBatch, GUI.MouseOn.Rect, Color.Cyan); + if (PlayerInput.IsCtrlDown() && PlayerInput.KeyDown(Keys.G)) + { + List hierarchy = new List(); + var currComponent = GUI.MouseOn; + while (currComponent != null) + { + hierarchy.Add(currComponent); + currComponent = currComponent.Parent; + } + + Color[] colors = { Color.Lime, Color.Yellow, Color.Aqua, Color.Red }; + for (int index = 0; index < hierarchy.Count; index++) + { + var component = hierarchy[index]; + if (component is { MouseRect: var mouseRect, Rect: var rect }) + { + if (mouseRect.IsEmpty) { mouseRect = rect; } + mouseRect.Location += (index%2,(index%4)/2); + GUI.DrawRectangle(spriteBatch, mouseRect, colors[index%4]); + } + } + } + else + { + GUI.DrawRectangle(spriteBatch, GUI.MouseOn.MouseRect, Color.Lime); + GUI.DrawRectangle(spriteBatch, GUI.MouseOn.Rect, Color.Cyan); + } spriteBatch.End(); } @@ -1071,7 +1010,7 @@ namespace Barotrauma if (showVerificationPrompt) { string text = (Screen.Selected is CharacterEditor.CharacterEditorScreen || Screen.Selected is SubEditorScreen) ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification"; - var msgBox = new GUIMessageBox("", TextManager.Get(text), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + var msgBox = new GUIMessageBox("", TextManager.Get(text), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { UserData = "verificationprompt" }; @@ -1119,18 +1058,18 @@ namespace Barotrauma { double roundDuration = Timing.TotalTime - GameSession.RoundStartTime; GameAnalyticsManager.AddProgressionEvent(GameAnalyticsManager.ProgressionStatus.Fail, - GameSession.GameMode?.Preset.Identifier ?? "none", + GameSession.GameMode?.Preset.Identifier.Value ?? "none", roundDuration); - string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier ?? "none") + ":"; + string eventId = "QuitRound:" + (GameSession.GameMode?.Preset.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:CurrentIntensity", GameSession.EventManager.CurrentIntensity); foreach (var activeEvent in GameSession.EventManager.ActiveEvents) { GameAnalyticsManager.AddDesignEvent(eventId + "EventManager:ActiveEvents:" + activeEvent.ToString()); } GameSession.LogEndRoundStats(eventId); - if (Tutorial.Initialized) + if (GameSession.GameMode is TutorialMode tutorialMode) { - ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); + tutorialMode.Tutorial?.Stop(); } } GUIMessageBox.CloseAll(); @@ -1142,7 +1081,7 @@ namespace Barotrauma public void ShowCampaignDisclaimer(Action onContinue = null) { var msgBox = new GUIMessageBox(TextManager.Get("CampaignDisclaimerTitle"), TextManager.Get("CampaignDisclaimerText"), - new string[] { TextManager.Get("CampaignRoadMapTitle"), TextManager.Get("OK") }); + new LocalizedString[] { TextManager.Get("CampaignRoadMapTitle"), TextManager.Get("OK") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { @@ -1153,8 +1092,10 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked += msgBox.Close; msgBox.Buttons[1].OnClicked += (_, __) => { onContinue?.Invoke(); return true; }; - Config.CampaignDisclaimerShown = true; - Config.SaveNewPlayerConfig(); + var config = GameSettings.CurrentConfig; + config.CampaignDisclaimerShown = true; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); } public void ShowEditorDisclaimer() @@ -1162,16 +1103,16 @@ namespace Barotrauma var msgBox = new GUIMessageBox(TextManager.Get("EditorDisclaimerTitle"), TextManager.Get("EditorDisclaimerText")); var linkHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), msgBox.Content.RectTransform)) { Stretch = true, RelativeSpacing = 0.025f }; linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); - List> links = new List>() + List<(LocalizedString Caption, LocalizedString Url)> links = new List<(LocalizedString, LocalizedString)>() { - new Pair(TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), - new Pair(TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), + (TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), + (TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), }; foreach (var link in links) { - new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.First, style: "MainMenuGUIButton", textAlignment: Alignment.Left) + new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), linkHolder.RectTransform), link.Caption, style: "MainMenuGUIButton", textAlignment: Alignment.Left) { - UserData = link.Second, + UserData = link.Url, OnClicked = (btn, userdata) => { ShowOpenUrlInWebBrowserPrompt(userdata as string); @@ -1182,8 +1123,10 @@ namespace Barotrauma msgBox.InnerFrame.RectTransform.MinSize = new Point(0, msgBox.InnerFrame.Rect.Height + linkHolder.Rect.Height + msgBox.Content.AbsoluteSpacing * 2 + 10); - Config.EditorDisclaimerShown = true; - Config.SaveNewPlayerConfig(); + var config = GameSettings.CurrentConfig; + config.EditorDisclaimerShown = true; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); } public void ShowBugReporter() @@ -1257,7 +1200,8 @@ namespace Barotrauma } if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } base.OnExiting(sender, args); } @@ -1267,14 +1211,14 @@ namespace Barotrauma if (string.IsNullOrEmpty(url)) { return; } if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; } - string text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); - string extensionText = TextManager.Get(promptExtensionTag, returnNull: true, useEnglishAsFallBack: false); - if (!string.IsNullOrEmpty(extensionText)) + LocalizedString text = TextManager.GetWithVariable("openlinkinbrowserprompt", "[link]", url); + LocalizedString extensionText = TextManager.Get(promptExtensionTag); + if (!extensionText.IsNullOrEmpty()) { text += $"\n\n{extensionText}"; } - var msgBox = new GUIMessageBox("", text, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + var msgBox = new GUIMessageBox("", text, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 2d8f290ab..f25e1db11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -170,7 +170,7 @@ namespace Barotrauma var matchingItem = matchingItems.ElementAt(i); SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local)); - if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } + if (canAddToRemoveQueue) { Entity.Spawner.AddItemToRemoveQueue(matchingItem); } } } else @@ -185,7 +185,7 @@ namespace Barotrauma // Exchange money Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; - GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 48f599318..86d82528c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -17,7 +17,7 @@ namespace Barotrauma { private Point screenResolution; - public Order DraggedOrder; + public OrderPrefab DraggedOrderPrefab; public bool DragOrder; private bool dropOrder; private int framesToSkip = 2; @@ -54,7 +54,8 @@ namespace Barotrauma set { if (_isCrewMenuOpen == value) { return; } - _isCrewMenuOpen = GameMain.Config.CrewMenuOpen = value; + _isCrewMenuOpen = value; + #warning TODO: update GameSettings.CurrentConfig.CrewMenuOpen when round ends } } @@ -62,7 +63,7 @@ namespace Barotrauma public void AutoHideCrewList() => _isCrewMenuOpen = false; - public void ResetCrewList() => _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; + public void ResetCrewList() => _isCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; const float CommandNodeAnimDuration = 0.2f; @@ -192,7 +193,7 @@ namespace Barotrauma }; } - List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + var reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -200,7 +201,7 @@ namespace Barotrauma } ReportButtonFrame = new GUILayoutGroup(new RectTransform( - new Point((HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height - (int)((reports.Count - 1) * 5 * GUI.Scale)) / reports.Count, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height), guiFrame.RectTransform)) + new Point((HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height - (int)((reports.Length - 1) * 5 * GUI.Scale)) / reports.Length, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height), guiFrame.RectTransform)) { AbsoluteSpacing = (int)(5 * GUI.Scale), UserData = "reportbuttons", @@ -216,42 +217,42 @@ namespace Barotrauma screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = GUI.Scale; - _isCrewMenuOpen = GameMain.Config.CrewMenuOpen; - dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); + _isCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; } - public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, List reports, bool isHorizontal) + public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, IReadOnlyList reports, bool isHorizontal) { //report buttons - foreach (Order order in reports) + foreach (OrderPrefab orderPrefab in reports) { - if (!order.IsReport || order.SymbolSprite == null || order.Hidden) { continue; } - var btn = new GUIButton(new RectTransform(new Point(isHorizontal ? parent.Rect.Height : parent.Rect.Width), parent.RectTransform), style: null) + if (!orderPrefab.IsReport || orderPrefab.SymbolSprite == null || orderPrefab.Hidden) { continue; } + var btn = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: isHorizontal ? ScaleBasis.BothHeight : ScaleBasis.BothWidth), style: null) { OnClicked = (button, userData) => { - if (!CanIssueOrders || crewManager?.DraggedOrder != null) { return false; } + if (!CanIssueOrders || crewManager?.DraggedOrderPrefab != null) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } if (crewManager != null) { - crewManager.SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + Order order = new Order(orderPrefab, Identifier.Empty, CharacterInfo.HighestManualOrderPriority, Order.OrderType.Current, null, null, orderGiver: Character.Controlled); + crewManager.SetCharacterOrder(null, order); if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } } return true; }, - UserData = order, + UserData = orderPrefab, ClampMouseRectToParent = false }; - btn.ToolTip = $"‖color:{XMLExtensions.ColorToString(order.Prefab.Color)}‖{order.Name}‖color:end‖\n{TextManager.Get("draganddropreports")}"; + btn.ToolTip = RichString.Rich($"‖color:{XMLExtensions.ColorToString(orderPrefab.Color)}‖{orderPrefab.Name}‖color:end‖\n{TextManager.Get("draganddropreports")}"); if (crewManager != null) { btn.OnButtonDown = () => { crewManager.dragOrderTreshold = Math.Max(btn.Rect.Width, btn.Rect.Height) / 2f; - crewManager.DraggedOrder = order; + crewManager.DraggedOrderPrefab = orderPrefab; crewManager.dropOrder = false; crewManager.framesToSkip = 2; crewManager.dragPoint = btn.Rect.Center.ToVector2(); @@ -261,21 +262,21 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") { - Color = GUI.Style.Red * 0.8f, - HoverColor = GUI.Style.Red * 1.0f, - PressedColor = GUI.Style.Red * 0.6f, + Color = GUIStyle.Red * 0.8f, + HoverColor = GUIStyle.Red * 1.0f, + PressedColor = GUIStyle.Red * 0.6f, UserData = "highlighted", CanBeFocused = false, Visible = false }; - var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), order.Prefab.SymbolSprite, scaleToFit: true) + var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), orderPrefab.SymbolSprite, scaleToFit: true) { - Color = order.Color, - HoverColor = Color.Lerp(order.Color, Color.White, 0.5f), - ToolTip = btn.RawToolTip, + Color = orderPrefab.Color, + HoverColor = Color.Lerp(orderPrefab.Color, Color.White, 0.5f), + ToolTip = btn.ToolTip, SpriteEffects = SpriteEffects.FlipHorizontally, - UserData = order + UserData = orderPrefab }; } } @@ -402,7 +403,7 @@ namespace Barotrauma // Spacing - (7 * layoutGroup.RelativeSpacing); - var font = layoutGroup.Rect.Width < 150 ? GUI.SmallFont : GUI.Font; + var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font; var nameBlock = new GUITextBlock( new RectTransform( new Vector2(nameRelativeWidth, 1.0f), @@ -476,7 +477,7 @@ namespace Barotrauma }; new GUIImage( new RectTransform(Vector2.One, soundIconParent.RectTransform), - GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), + GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { CanBeFocused = false, @@ -521,14 +522,13 @@ namespace Barotrauma { if (!(characterComponent?.UserData is Character character)) { return; } if (character.Info?.Job?.Prefab == null) { return; } - string tooltip = TextManager.GetWithVariables("crewlistelementtooltip", - new string[] { "[name]", "[job]" }, - new string[] { character.Name, character.Info.Job.Name }); + + LocalizedString tooltip = TextManager.GetWithVariables("crewlistelementtooltip", + ("[name]", character.Name), + ("[job]", character.Info.Job.Name)); string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor); - tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; - var richTextData = RichTextData.GetRichTextData(tooltip, out string sanitizedTooltip); - characterComponent.ToolTip = sanitizedTooltip; - characterComponent.TooltipRichTextData = richTextData; + RichString richToolTip = RichString.Rich($"‖color:{color}‖"+tooltip+"‖color:end‖"); + characterComponent.ToolTip = richToolTip; } /// @@ -662,6 +662,10 @@ namespace Barotrauma /// /// Adds the message to the single player chatbox. /// + public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Character sender) + { + AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender); + } public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Character sender) { if (!IsSinglePlayer) @@ -767,16 +771,16 @@ namespace Barotrauma /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null, bool isNewOrder = true) + public void SetCharacterOrder(Character character, Order order, bool isNewOrder = true) { if (order != null && order.TargetAllCharacters) { - Hull hull = targetHull; + Hull hull = order.TargetHull; if (order.IsReport) { - if (orderGiver?.CurrentHull == null && hull == null) { return; } - hull ??= orderGiver.CurrentHull; - AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); + if (order.OrderGiver?.CurrentHull == null && hull == null) { return; } + hull ??= order.OrderGiver.CurrentHull; + AddOrder(order.WithTargetEntity(hull), order.FadeOutTime); } else if (order.IsIgnoreOrder) { @@ -784,7 +788,7 @@ namespace Barotrauma if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable) { ignorable.OrderedToBeIgnored = order.Identifier == "ignorethis"; - AddOrder(new Order(order.Prefab ?? order, order.TargetEntity, order.TargetItemComponent, orderGiver), null); + AddOrder(order.Clone(), null); } else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s) { @@ -793,7 +797,7 @@ namespace Barotrauma if (ws != null) { ws.OrderedToBeIgnored = order.Identifier == "ignorethis"; - AddOrder(new Order(order.Prefab ?? order, s, wallSectionIndex, orderGiver), null); + AddOrder(order.WithWallSection(s, wallSectionIndex), null); } } else @@ -817,11 +821,11 @@ namespace Barotrauma if (IsSinglePlayer) { - orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); + order.OrderGiver?.Speak(order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); } else { - OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver, isNewOrder: isNewOrder); + OrderChatMessage msg = new OrderChatMessage(order.WithTargetEntity(order.IsReport ? hull : order.TargetEntity), null, order.OrderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -830,15 +834,16 @@ namespace Barotrauma //can't issue an order if no characters are available if (character == null) { return; } + var orderGiver = order?.OrderGiver; if (IsSinglePlayer) { - character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, isNewOrder: isNewOrder); + character.SetOrder(order, speak: orderGiver != character); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); orderGiver?.Speak(message); } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item, character, orderGiver, isNewOrder: isNewOrder); + OrderChatMessage msg = new OrderChatMessage(order, character, orderGiver, isNewOrder: isNewOrder); GameMain.Client?.SendChatMessage(msg); } } @@ -847,7 +852,7 @@ namespace Barotrauma /// /// Displays the specified order in the crew UI next to the character. /// - public void AddCurrentOrderIcon(Character character, Order order, string option, int priority) + public void AddCurrentOrderIcon(Character character, Order order) { if (character == null) { return; } @@ -858,25 +863,25 @@ namespace Barotrauma var currentOrderIconList = GetCurrentOrderIconList(characterComponent); var currentOrderIcons = currentOrderIconList.Content.Children; var iconsToRemove = new List(); - var newPreviousOrders = new List(); + var newPreviousOrders = new List(); bool updatedExistingIcon = false; foreach (var icon in currentOrderIcons) { - var orderInfo = (OrderInfo)icon.UserData; - var matchingOrder = character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption); - if (!matchingOrder.HasValue) + var orderInfo = (Order)icon.UserData; + var matchingOrder = character.GetCurrentOrder(orderInfo); + if (matchingOrder is null) { iconsToRemove.Add(icon); newPreviousOrders.Add(orderInfo); } - else if (orderInfo.MatchesOrder(order, option)) + else if (orderInfo.MatchesOrder(order)) { - icon.UserData = new OrderInfo(order, option, priority); + icon.UserData = order.Clone(); if (icon is GUIImage image) { - image.Sprite = GetOrderIconSprite(order, option); - image.ToolTip = CreateOrderTooltip(order, option); + image.Sprite = GetOrderIconSprite(order); + image.ToolTip = CreateOrderTooltip(order); } updatedExistingIcon = true; } @@ -889,8 +894,8 @@ namespace Barotrauma var previousOrderIcons = previousOrderIconGroup.Children; foreach (var icon in previousOrderIcons) { - var orderInfo = (OrderInfo)icon.UserData; - if (orderInfo.MatchesOrder(order, option)) + var orderInfo = (Order)icon.UserData; + if (orderInfo.MatchesOrder(order)) { previousOrderIconGroup.RemoveChild(icon); break; @@ -922,15 +927,14 @@ namespace Barotrauma float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * currentOrderIconList.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); Point size = new Point((int)nodeWidth, currentOrderIconList.RectTransform.NonScaledSize.Y); - var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order, option), order.Color, tooltip: CreateOrderTooltip(order, option)); - nodeIcon.UserData = new OrderInfo(order, option, priority); + var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order), order.Color, tooltip: CreateOrderTooltip(order)); + nodeIcon.UserData = order.Clone(); nodeIcon.OnSecondaryClicked = (image, userData) => { if (!CanIssueOrders) { return false; } - var orderInfo = (OrderInfo)userData; - SetCharacterOrder(character, dismissedOrderPrefab, Order.GetDismissOrderOption(orderInfo), - character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption)?.ManualPriority ?? 0, - Character.Controlled); + var orderInfo = (Order)userData; + var order = orderInfo.GetDismissal().WithManualPriority(character.GetCurrentOrder(orderInfo)?.ManualPriority ?? 0).WithOrderGiver(Character.Controlled); + SetCharacterOrder(character, order); return true; }; @@ -942,7 +946,7 @@ namespace Barotrauma Visible = false }; - int hierarchyIndex = Math.Clamp(CharacterInfo.HighestManualOrderPriority - priority, 0, Math.Max(currentOrderIconList.Content.CountChildren - 1, 0)); + int hierarchyIndex = Math.Clamp(CharacterInfo.HighestManualOrderPriority - order.ManualPriority, 0, Math.Max(currentOrderIconList.Content.CountChildren - 1, 0)); if (hierarchyIndex != currentOrderIconList.Content.GetChildIndex(nodeIcon)) { nodeIcon.RectTransform.RepositionChildInHierarchy(hierarchyIndex); @@ -957,21 +961,21 @@ namespace Barotrauma // Make sure priority values are up-to-date foreach (var currentOrderInfo in character.CurrentOrders) { - var component = currentOrderIconList.Content.FindChild(c => c?.UserData is OrderInfo componentOrderInfo && + var component = currentOrderIconList.Content.FindChild(c => c?.UserData is Order componentOrderInfo && componentOrderInfo.MatchesOrder(currentOrderInfo)); if (component == null) { continue; } - var componentOrderInfo = (OrderInfo)component.UserData; + var componentOrderInfo = (Order)component.UserData; int newPriority = currentOrderInfo.ManualPriority; if (componentOrderInfo.ManualPriority != newPriority) { - component.UserData = new OrderInfo(componentOrderInfo, newPriority); + component.UserData = componentOrderInfo.WithManualPriority(newPriority); } } currentOrderIconList.Content.RectTransform.SortChildren((x, y) => { - var xOrder = (OrderInfo)x.GUIComponent.UserData; - var yOrder = (OrderInfo)y.GUIComponent.UserData; + var xOrder = (Order)x.GUIComponent.UserData; + var yOrder = (Order)y.GUIComponent.UserData; return yOrder.ManualPriority.CompareTo(xOrder.ManualPriority); }); @@ -987,14 +991,9 @@ namespace Barotrauma } } - public void AddCurrentOrderIcon(Character character, OrderInfo? orderInfo) + private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, Order orderInfo) { - AddCurrentOrderIcon(character, orderInfo?.Order, orderInfo?.OrderOption, orderInfo?.ManualPriority ?? 0); - } - - private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, OrderInfo orderInfo) - { - if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } + if (orderInfo == null || orderInfo.Identifier == dismissedOrderPrefab.Identifier) { return; } var currentOrderIconList = GetCurrentOrderIconList(characterComponent); int maxPreviousOrderIcons = CharacterInfo.MaxCurrentOrders - currentOrderIconList.Content.CountChildren; @@ -1009,16 +1008,16 @@ namespace Barotrauma float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * previousOrderIconGroup.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); Point size = new Point((int)nodeWidth, previousOrderIconGroup.Rect.Height); - var previousOrderInfo = new OrderInfo(orderInfo, OrderInfo.OrderType.Previous); + var previousOrderInfo = orderInfo.WithType(Order.OrderType.Previous); var prevOrderFrame = new GUIButton(new RectTransform(size, parent: previousOrderIconGroup.RectTransform), style: null) { UserData = previousOrderInfo, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } - var orderInfo = (OrderInfo)userData; - int priority = GetManualOrderPriority(character, orderInfo.Order); - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); + var orderInfo = (Order)userData; + int priority = GetManualOrderPriority(character, orderInfo); + SetCharacterOrder(character, orderInfo.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); return true; }, OnSecondaryClicked = (button, userData) => @@ -1038,7 +1037,7 @@ namespace Barotrauma CreateNodeIcon(Vector2.One, prevOrderIconFrame.RectTransform, GetOrderIconSprite(previousOrderInfo), - previousOrderInfo.Order.Color, + previousOrderInfo.Color, tooltip: CreateOrderTooltip(previousOrderInfo)); foreach (GUIComponent c in prevOrderIconFrame.Children) @@ -1071,7 +1070,7 @@ namespace Barotrauma { foreach (GUIComponent icon in oldPrevOrderIcons) { - if (icon.UserData is OrderInfo orderInfo) + if (icon.UserData is Order orderInfo) { AddPreviousOrderIcon(character, newCharacterComponent, orderInfo); } @@ -1116,33 +1115,27 @@ namespace Barotrauma { var orderComponent = orderList.Content.GetChildByUserData(userData); if (orderComponent == null) { return; } - var orderInfo = (OrderInfo)userData; + var orderInfo = (Order)userData; var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1); if (orderInfo.ManualPriority == priority) { return; } var character = (Character)orderList.UserData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled, isNewOrder: false); + SetCharacterOrder(character, orderInfo.WithManualPriority(priority), isNewOrder: false); } - private string CreateOrderTooltip(Order orderPrefab, string option, Entity targetEntity) + private LocalizedString CreateOrderTooltip(OrderPrefab orderPrefab, Identifier option, Entity targetEntity) { if (orderPrefab == null) { return ""; } - if (orderPrefab.DisplayGiverInTooltip && orderPrefab.OrderGiver != null) + if (option != Identifier.Empty) { - return TextManager.GetWithVariables("crewlistordericontooltip", - new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { orderPrefab.Name, orderPrefab.OrderGiver.DisplayName }); - } - else if (!string.IsNullOrEmpty(option)) - { - return TextManager.GetWithVariables("crewlistordericontooltip", - new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { orderPrefab.Name, orderPrefab.GetOptionName(option) }); + return TextManager.GetWithVariables("crewlistordericontooltip".ToIdentifier(), + ("[ordername]".ToIdentifier(), orderPrefab.Name), + ("[orderoption]".ToIdentifier(), orderPrefab.GetOptionName(option))); } else if (targetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) { - return TextManager.GetWithVariables("crewlistordericontooltip", - new string[2] { "[ordername]", "[orderoption]" }, - new string[2] { orderPrefab.Name, targetItem.Name }); + return TextManager.GetWithVariables("crewlistordericontooltip".ToIdentifier(), + ("[ordername]".ToIdentifier(), orderPrefab.Name), + ("[orderoption]".ToIdentifier(), targetItem.Name)); } else { @@ -1150,23 +1143,24 @@ namespace Barotrauma } } - private string CreateOrderTooltip(Order order, string option) + private LocalizedString CreateOrderTooltip(Order order) { - return CreateOrderTooltip(order?.Prefab ?? order, option, order?.TargetEntity); + if (order.DisplayGiverInTooltip && order.OrderGiver != null) + { + return TextManager.GetWithVariables("crewlistordericontooltip", + ("[ordername]", order.Name), + ("[orderoption]", order.OrderGiver.DisplayName)); + } + return CreateOrderTooltip(order.Prefab, order.Option, order?.TargetEntity); } - private string CreateOrderTooltip(OrderInfo orderInfo) - { - return CreateOrderTooltip(orderInfo.Order?.Prefab ?? orderInfo.Order, orderInfo.OrderOption, orderInfo.Order?.TargetEntity); - } - - private Sprite GetOrderIconSprite(Order order, string option) + private Sprite GetOrderIconSprite(Order order) { if (order == null) { return null; } Sprite sprite = null; - if (option != null && order.Prefab.OptionSprites.Any()) + if (order.Option != Identifier.Empty && order.Prefab.OptionSprites.Any()) { - order.Prefab.OptionSprites.TryGetValue(option, out sprite); + order.Prefab.OptionSprites.TryGetValue(order.Option, out sprite); } if (sprite == null && order.TargetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null) { @@ -1175,9 +1169,6 @@ namespace Barotrauma return sprite ?? order.SymbolSprite; } - private Sprite GetOrderIconSprite(OrderInfo orderInfo) => - GetOrderIconSprite(orderInfo.Order, orderInfo.OrderOption); - #endregion #region Updating and drawing the UI @@ -1208,7 +1199,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: 10); } } - + public void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } @@ -1297,13 +1288,15 @@ namespace Barotrauma return 0; } - private bool CreateOrder(Order order, Hull targetHull = null) + private bool CreateOrder(OrderPrefab orderPrefab, Hull targetHull = null) { var sub = Character.Controlled?.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } - SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled, targetHull); + var order = new Order(orderPrefab, targetHull, null, Character.Controlled) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetCharacterOrder(null, order); if (IsSinglePlayer) { @@ -1315,7 +1308,7 @@ namespace Barotrauma private void UpdateOrderDrag() { - if (DraggedOrder is { } order) + if (DraggedOrderPrefab is { } orderPrefab) { if (dropOrder) { @@ -1342,12 +1335,12 @@ namespace Barotrauma framesToSkip = 2; dropOrder = false; - DraggedOrder = null; + DraggedOrderPrefab = null; if (hull is null && GUI.MouseOn is { Visible: true, CanBeFocused: true }) { return; } - hull ??= Hull.hullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition))); - CreateOrder(order, hull); + hull ??= Hull.HullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition))); + CreateOrder(orderPrefab, hull); } } else @@ -1362,7 +1355,7 @@ namespace Barotrauma } else { - DraggedOrder = null; + DraggedOrderPrefab = null; } dragPoint = Vector2.Zero; DragOrder = false; @@ -1423,12 +1416,12 @@ namespace Barotrauma // When using Deselect to close the interface, make sure it's not a seconday mouse button click on a node // That should be reserved for opening manual assignment - bool isMouseOnOptionNode = optionNodes.Any(n => GUI.IsMouseOn(n.Item1)); + bool isMouseOnOptionNode = optionNodes.Any(n => GUI.IsMouseOn(n.Button)); bool isMouseOnShortcutNode = !isMouseOnOptionNode && shortcutNodes.Any(n => GUI.IsMouseOn(n)); bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode)); - bool isBoundToPrimaryMouse = GameMain.Config.KeyBind(InputType.Command).MouseButton is MouseButton mouseButton && + bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton is MouseButton mouseButton && (mouseButton == MouseButton.PrimaryMouse || mouseButton == (PlayerInput.MouseButtonsSwapped() ? MouseButton.RightMouse : MouseButton.LeftMouse)); bool canToggleInterface = !isBoundToPrimaryMouse || (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode)); @@ -1467,7 +1460,7 @@ namespace Barotrauma GUIComponent closestNode = null; float closestBearing = 0; - optionNodes.ForEach(n => CheckIfClosest(n.Item1)); + optionNodes.ForEach(n => CheckIfClosest(n.Button)); CheckIfClosest(returnNode); void CheckIfClosest(GUIComponent comp) @@ -1524,18 +1517,18 @@ namespace Barotrauma } var hotkeyHit = false; - foreach (Tuple node in optionNodes) + foreach (OptionNode node in optionNodes) { - if (node.Item2 != Keys.None && PlayerInput.KeyHit(node.Item2)) + if (node.Keys != Keys.None && PlayerInput.KeyHit(node.Keys)) { - var b = node.Item1 as GUIButton; + var b = node.Button as GUIButton; if (PlayerInput.IsShiftDown() && b?.OnSecondaryClicked != null) { - b.OnSecondaryClicked.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + b.OnSecondaryClicked.Invoke(node.Button as GUIButton, node.Button.UserData); } else { - b?.OnClicked?.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + b?.OnClicked?.Invoke(node.Button as GUIButton, node.Button.UserData); } ResetNodeSelection(); hotkeyHit = true; @@ -1630,8 +1623,7 @@ namespace Barotrauma { foreach (var orderIcon in currentOrderIconList.Content.Children) { - if (!(orderIcon.UserData is OrderInfo orderInfo)) { continue; } - if (!(orderInfo.Order is Order order)) { continue; } + if (!(orderIcon.UserData is Order order)) { continue; } if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled) { orderIcon.Color = AIObjective.ObjectiveIconColor; @@ -1652,7 +1644,10 @@ namespace Barotrauma if (objectiveManager.IsOrder(currentObjective)) { var orderInfo = objectiveManager.CurrentOrders.FirstOrDefault(o => o.Objective == currentObjective); - SetOrderHighlight(characterComponent, orderInfo.Order?.Identifier, orderInfo.OrderOption); + if (orderInfo != null) + { + SetOrderHighlight(characterComponent, orderInfo.Identifier, orderInfo.Option); + } } else { @@ -1706,7 +1701,7 @@ namespace Barotrauma UpdateReports(); } - private void SetOrderHighlight(GUIComponent characterComponent, string orderIdentifier, string orderOption) + private void SetOrderHighlight(GUIComponent characterComponent, Identifier orderIdentifier, Identifier orderOption) { if (characterComponent == null) { return; } RemoveObjectiveIcon(characterComponent); @@ -1722,14 +1717,14 @@ namespace Barotrauma glowComponent.Visible = false; continue; } - var orderInfo = (OrderInfo)orderIcon.UserData; + var orderInfo = (Order)orderIcon.UserData; foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption); glowComponent.Visible = foundMatch; } } } - public void SetOrderHighlight(Character character, string orderIdentifier, string orderOption) + public void SetOrderHighlight(Character character, Identifier orderIdentifier, Identifier orderOption) { if (crewList == null) { return; } var characterComponent = crewList.Content.GetChildByUserData(character); @@ -1749,7 +1744,7 @@ namespace Barotrauma } } - private void CreateObjectiveIcon(GUIComponent characterComponent, Sprite sprite, string tooltip) + private void CreateObjectiveIcon(GUIComponent characterComponent, Sprite sprite, LocalizedString tooltip) { if (characterComponent == null || !(characterComponent.UserData is Character character) || character.IsPlayer) { return; } DisableOrderHighlight(characterComponent); @@ -1777,7 +1772,7 @@ namespace Barotrauma } } - public void CreateObjectiveIcon(Character character, string identifier, string option, Entity targetEntity) + public void CreateObjectiveIcon(Character character, Identifier identifier, Identifier option, Entity targetEntity) { CreateObjectiveIcon(crewList?.Content.GetChildByUserData(character), AIObjective.GetSprite(identifier, option, targetEntity), @@ -1791,22 +1786,22 @@ namespace Barotrauma GetObjectiveIconTooltip(objective)); } - private string GetObjectiveIconTooltip(string identifier, string option, Entity targetEntity) + private LocalizedString GetObjectiveIconTooltip(Identifier identifier, Identifier option, Entity targetEntity) { - string variableValue; - identifier = identifier.RemoveWhitespace(); - if (Order.Prefabs.TryGetValue(identifier, out Order orderPrefab)) + LocalizedString variableValue; + if (OrderPrefab.Prefabs.ContainsKey(identifier)) { + var orderPrefab = OrderPrefab.Prefabs[identifier]; variableValue = CreateOrderTooltip(orderPrefab, option, targetEntity); } else { - variableValue = TextManager.Get($"objective.{identifier}", returnNull: true) ?? ""; + variableValue = TextManager.Get($"objective.{identifier}"); } - return string.IsNullOrEmpty(variableValue) ? variableValue : TextManager.GetWithVariable("crewlistobjectivetooltip", "[objective]", variableValue); + return variableValue.IsNullOrEmpty() ? variableValue : TextManager.GetWithVariable("crewlistobjectivetooltip", "[objective]", variableValue); } - private string GetObjectiveIconTooltip(AIObjective objective) + private LocalizedString GetObjectiveIconTooltip(AIObjective objective) { return objective == null ? "" : GetObjectiveIconTooltip(objective.Identifier, objective.Option, (objective as AIObjectiveOperateItem)?.OperateTarget); @@ -1850,7 +1845,17 @@ namespace Barotrauma private GUIFrame commandFrame, targetFrame; private GUIButton centerNode, returnNode, expandNode; private GUIFrame shortcutCenterNode; - private readonly List> optionNodes = new List>(); + private class OptionNode + { + public readonly GUIButton Button; + public readonly Keys Keys; + public OptionNode(GUIButton guiComponent, Keys keys) + { + Button = guiComponent; + Keys = keys; + } + } + private readonly List optionNodes = new List(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; private readonly List shortcutNodes = new List(); private readonly List extraOptionNodes = new List(); @@ -1875,13 +1880,13 @@ namespace Barotrauma private const float nodeColorMultiplier = 0.75f; private int nodeDistance = (int)(GUI.Scale * 250); private const float returnNodeDistanceModifier = 0.65f; - private Order dismissedOrderPrefab; + private OrderPrefab dismissedOrderPrefab => OrderPrefab.Dismissal; private Character characterContext; private Item itemContext; private Hull hullContext; private WallSection wallContext; private bool isContextual; - private readonly List contextualOrders = new List(); + private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; private const int maxShortcutNodeCount = 4; @@ -2033,7 +2038,6 @@ namespace Barotrauma SetCenterNode(startNode); availableCategories ??= GetAvailableCategories(); - dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); if (isContextual) { @@ -2095,7 +2099,7 @@ namespace Barotrauma availableCategories = new List(); foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory))) { - if (Order.PrefabList.Any(o => o.Category == category && !o.IsReport)) + if (OrderPrefab.Prefabs.Any(o => o.Category == category && !o.IsReport)) { availableCategories.Add(category); } @@ -2120,24 +2124,24 @@ namespace Barotrauma if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); // Don't draw connectors for assignment nodes - if (!(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) + if (!(optionNodes.FirstOrDefault()?.Button.UserData is Character)) { // Regular option nodes if (targetFrame == null || !targetFrame.Visible) { - optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); + optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Button, optionNodeMargin, spriteBatch)); } - // Minimap item nodes for single-option orders - else if(optionNodes.FirstOrDefault()?.Item1?.UserData is Tuple userData && string.IsNullOrEmpty(userData.Item2)) + // Minimap item nodes + else { foreach (var node in optionNodes) { float iconRadius = 0.5f * optionNodeMargin; - Vector2 itemPosition = node.Item1.Parent.Rect.Center.ToVector2(); - if (Vector2.Distance(node.Item1.Center, itemPosition) <= iconRadius) { continue; } - DrawNodeConnector(itemPosition, 0.0f, node.Item1, iconRadius, spriteBatch, widthMultiplier: 0.5f); + Vector2 itemPosition = node.Button.Parent.Rect.Center.ToVector2(); + if (Vector2.Distance(node.Button.Center, itemPosition) <= iconRadius) { continue; } + DrawNodeConnector(itemPosition, 0.0f, node.Button, iconRadius, spriteBatch, widthMultiplier: 0.5f); GUI.DrawFilledRectangle(spriteBatch, itemPosition - Vector2.One, new Vector2(3), - node.Item1.GetChildByUserData("colorsource")?.Color ?? Color.White); + node.Button.GetChildByUserData("colorsource")?.Color ?? Color.White); } } } @@ -2199,7 +2203,7 @@ namespace Barotrauma private bool NavigateForward(GUIButton node, object userData) { if (commandFrame == null) { return false; } - if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) + if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; @@ -2367,7 +2371,7 @@ namespace Barotrauma CreateOrderOptionNodes(nodeOrder, itemContext ?? nodeOrder.TargetEntity as Item ?? matchingItems?.FirstOrDefault()); } } - else if (userData is (Order minimapOrder, string option) && minimapOrder.HasOptions && string.IsNullOrEmpty(option)) + else if (userData is MinimapNodeData {Order: { } minimapOrder} && minimapOrder.Prefab.HasOptions) { CreateOrderOptionNodes(minimapOrder, minimapOrder.TargetEntity as Item); } @@ -2383,7 +2387,7 @@ namespace Barotrauma { if (commandFrame != null) { - optionNodes.ForEach(node => commandFrame.RemoveChild(node.Item1)); + optionNodes.ForEach(node => commandFrame.RemoveChild(node.Button)); shortcutNodes.ForEach(node => commandFrame.RemoveChild(node)); commandFrame.RemoveChild(expandNode); } @@ -2421,19 +2425,23 @@ namespace Barotrauma }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - if (Order.OrderCategoryIcons.TryGetValue(category, out Tuple sprite)) + var icon = OrderCategoryIcon.OrderCategoryIcons.FirstOrDefault(ic => ic.Category == category); + if (!(icon is null)) { - var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLowerInvariant()); - var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); - if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } - CreateNodeIcon(Vector2.One, node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); + var tooltip = TextManager.Get($"ordercategorytitle.{category}"); + var categoryDescription = TextManager.Get($"ordercategorydescription.{category}"); + if (!categoryDescription.IsNullOrWhiteSpace()) { tooltip += "\n" + categoryDescription; } + CreateNodeIcon(Vector2.One, node.RectTransform, icon.Sprite, icon.Color, tooltip: tooltip); } CreateHotkeyIcon(node.RectTransform, hotkey % 10); - optionNodes.Add(new Tuple(node, Keys.D0 + hotkey % 10)); + optionNodes.Add(new OptionNode(node, Keys.D0 + hotkey % 10)); } private void CreateShortcutNodes() { + bool HasAppropriateJobId(Character c, Identifier jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); + bool HasAppropriateJob(Character c, string jobId) => HasAppropriateJobId(c, jobId.ToIdentifier()); + var sub = GetTargetSubmarine(); if (sub == null) { return; } shortcutNodes.Clear(); @@ -2444,88 +2452,97 @@ namespace Barotrauma // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { - var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); - string option = order.Prefab.Options[0]; - if (IsNonDuplicateOrder(order, option)) + var orderPrefab = OrderPrefab.Prefabs["operatereactor"]; + var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor, Character.Controlled); + if (IsNonDuplicateOrder(order)) { - shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); + shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1)); } } } // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order - if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && Order.GetPrefab("steer") is Order steerOrder && IsNonDuplicateOrder(steerOrder) && + if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, steerOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering, Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order if (CanFitMoreNodes() && ShouldDelegateOrder("fightintruders") && - Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders) && - Order.GetPrefab("fightintruders") is Order fightOrder && IsNonDuplicateOrder(fightOrder)) + ActiveOrders.Any(o => o.Order.Identifier == "reportintruders") && + IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fightintruders"])) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fightOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["fightintruders"], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") && Order.GetPrefab("fixleaks") is Order fixLeaksOrder && IsNonDuplicateOrder(fixLeaksOrder) && - Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach)) + if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") && + IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fixleaks"]) && + ActiveOrders.Any(o => o.Order.Identifier == "reportbreach")) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, fixLeaksOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["fixleaks"], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } // --> Create shortcut nodes for the Repair orders - if (CanFitMoreNodes() && Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices)) + if (CanFitMoreNodes() && ActiveOrders.Any(o => o.Order.Identifier == "reportbrokendevices")) { + var reportBrokenDevices = OrderPrefab.Prefabs["reportbrokendevices"]; // TODO: Doesn't work for player issued reports, because they don't have a target. bool useSpecificRepairOrder = false; string tag = "repairelectrical"; if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && - ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) { - if (Order.GetPrefab(tag) is Order repairElectricalOrder && IsNonDuplicateOrder(repairElectricalOrder)) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairElectricalOrder, -1)); + var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } useSpecificRepairOrder = true; } tag = "repairmechanical"; if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && - ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices && o.First.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) + ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) { - if (Order.GetPrefab(tag) is Order repairMechanicalOrder && IsNonDuplicateOrder(repairMechanicalOrder)) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairMechanicalOrder, -1)); + var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } useSpecificRepairOrder = true; } tag = "repairsystems"; - if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && Order.GetPrefab(tag) is Order repairOrder && IsNonDuplicateOrder(repairOrder)) + if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && OrderPrefab.Prefabs[tag] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder)) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, repairOrder, -1)); + var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } } // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (CanFitMoreNodes() && Order.GetPrefab("extinguishfires") is Order extinguishOrder && IsNonDuplicateOrder(extinguishOrder) && - ActiveOrders.Any(o => o.First.Prefab == Order.GetPrefab("reportfire"))) + if (CanFitMoreNodes() && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["extinguishfires"]) && + ActiveOrders.Any(o => o.Order.Identifier == "reportfire")) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, extinguishOrder, -1)); + var order = new Order(OrderPrefab.Prefabs["extinguishfires"], null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { - foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) + foreach (Identifier orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) { - if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && IsNonDuplicateOrder(orderPrefab) && - shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || - (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && + if (OrderPrefab.Prefabs[orderIdentifier] is OrderPrefab orderPrefab && IsNonDuplicateOrderPrefab(orderPrefab) && + shortcutNodes.None(n => n.UserData is Order order && order.Identifier == orderIdentifier) && !orderPrefab.IsReport && orderPrefab.Category != null) { if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true, interactableFor: characterContext ?? Character.Controlled).Any()) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); + var order = new Order(orderPrefab, null, orderGiver: Character.Controlled); + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } if (!CanFitMoreNodes()) { break; } } @@ -2533,7 +2550,9 @@ namespace Barotrauma } if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed) { - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, dismissedOrderPrefab, -1)); + var order = new Order(OrderPrefab.Dismissal, null, orderGiver: Character.Controlled); + shortcutNodes.Add( + CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); } shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o)); if (shortcutNodes.Count < 1) { return; } @@ -2562,32 +2581,34 @@ namespace Barotrauma { return shortcutNodes.Count < maxShortcutNodeCount; } - static bool ShouldDelegateOrder(string orderIdentifier) + static bool ShouldDelegateOrder(string orderIdentifier) => ShouldDelegateOrderId(orderIdentifier.ToIdentifier()); + static bool ShouldDelegateOrderId(Identifier orderIdentifier) { return !(Character.Controlled is Character c) || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier)); } - bool IsNonDuplicateOrder(Order orderPrefab, string option = null) + bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option); + bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default) { - return characterContext == null || (string.IsNullOrEmpty(option) ? - characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier) : - characterContext.CurrentOrders.None(oi => oi.Order?.Identifier == orderPrefab?.Identifier && oi.OrderOption == option)); + return characterContext == null || (option.IsEmpty ? + characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) : + characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option)); } } private void CreateOrderNodes(OrderCategory orderCategory) { - var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)); + var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).ToArray(); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, - GetCircumferencePointCount(orders.Count), GetFirstNodeAngle(orders.Count)); - for (int i = 0; i < orders.Count; i++) + GetCircumferencePointCount(orderPrefabs.Length), GetFirstNodeAngle(orderPrefabs.Length)); + for (int i = 0; i < orderPrefabs.Length; i++) { - order = orders[i]; + order = new Order(orderPrefabs[i], null, orderGiver: Character.Controlled); disableNode = !CanCharacterBeHeard() || - (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && - order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); - optionNodes.Add(new Tuple( + (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && + order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); + optionNodes.Add(new OptionNode( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } @@ -2605,74 +2626,76 @@ namespace Barotrauma if (itemContext != null && itemContext.IsPlayerTeamInteractable) { ItemComponent targetComponent; - foreach (Order p in Order.PrefabList) + foreach (OrderPrefab p in OrderPrefab.Prefabs) { targetComponent = null; if (p.UseController && itemContext.Components.None(c => c is Controller)) { continue; } if (p.HasOptionSpecificTargetItems) { - foreach (string option in p.Options) + foreach (Identifier option in p.Options) { if (p.TargetItemsMatchItem(itemContext, option)) { - contextualOrders.Add(new OrderInfo(new Order(p, itemContext, targetComponent, Character.Controlled), option)); + contextualOrders.Add(new Order(p, itemContext, targetComponent, Character.Controlled).WithOption(option)); } } } else if (p.TargetItemsMatchItem(itemContext) || p.TryGetTargetItemComponent(itemContext, out targetComponent)) { - contextualOrders.Add(new OrderInfo(p.HasOptions ? p : new Order(p, itemContext, targetComponent, Character.Controlled), null)); + contextualOrders.Add(p.HasOptions ? + new Order(p, null, orderGiver: Character.Controlled) : + new Order(p, itemContext, targetComponent, Character.Controlled)); } } // If targeting a periscope connected to a turret, show the 'operateweapons' order orderIdentifier = "operateweapons"; - var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) + var operateWeaponsPrefab = OrderPrefab.Prefabs[orderIdentifier]; + if (contextualOrders.None(o => o.Identifier == orderIdentifier) && itemContext.Components.Any(c => c is Controller)) { var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)); if (turret != null) { - contextualOrders.Add(new OrderInfo(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled), null)); + contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } } // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order orderIdentifier = "repairsystems"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) + if (contextualOrders.None(order => order.Identifier == orderIdentifier) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("repairelectrical"), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null, Character.Controlled)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("repairmechanical"), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null, Character.Controlled)); } else { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); } } // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) orderIdentifier = "pumpwater"; - if (contextualOrders.FirstOrDefault(info => info.Order.Identifier.Equals(orderIdentifier)) is OrderInfo pumpOrderInfo && pumpOrderInfo.Order is Order pumpOrder && + if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals(orderIdentifier)) is Order pumpOrder && itemContext.Components.FirstOrDefault(c => c.GetType() == pumpOrder.ItemComponentType) is Pump pump && pump.IsAutoControlled) { - contextualOrders.Remove(pumpOrderInfo); + contextualOrders.Remove(pumpOrder); } orderIdentifier = "cleanupitems"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Identifier.Equals(orderIdentifier))) { if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); } } AddIgnoreOrder(itemContext); } else if (hullContext != null) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab("fixleaks"), hullContext, targetItem: null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null, Character.Controlled)); if (wallContext != null) { AddIgnoreOrder(wallContext); @@ -2681,14 +2704,14 @@ namespace Barotrauma void AddIgnoreOrder(IIgnorable target) { var orderIdentifier = "ignorethis"; - if (!target.OrderedToBeIgnored && contextualOrders.None(info => info.Order.Identifier == orderIdentifier)) + if (!target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); } else { orderIdentifier = "unignorethis"; - if (target.OrderedToBeIgnored && contextualOrders.None(info => info.Order.Identifier == orderIdentifier)) + if (target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier)) { AddOrder(); } @@ -2698,62 +2721,62 @@ namespace Barotrauma { if (target is WallSection ws) { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled)); } else { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), target as Entity, null, Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], target as Entity, null, Character.Controlled)); } } } orderIdentifier = "wait"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) { Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], new OrderTarget(position, hull), Character.Controlled)); } - if (contextualOrders.None(info => info.Order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) + if (contextualOrders.None(order => order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) { orderIdentifier = "follow"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) { - contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); } } // Show 'dismiss' order only when there are crew members with active orders orderIdentifier = "dismissed"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) + if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) { - contextualOrders.Add(new OrderInfo(Order.GetPrefab(orderIdentifier), null)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); } } - contextualOrders.RemoveAll(o => !IsOrderAvailable(o.Order)); + contextualOrders.RemoveAll(o => !IsOrderAvailable(o)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) { - var info = contextualOrders[i]; + var order = contextualOrders[i]; int hotkey = (i + 1) % 10; - var component = string.IsNullOrEmpty(info.OrderOption) ? - CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), info.Order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) : - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), info.Order, info.OrderOption, info.Order.Prefab.GetOptionName(info.OrderOption), hotkey); - optionNodes.Add(new Tuple(component, !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); + var component = order.Option.IsEmpty ? + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) : + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey); + optionNodes.Add(new OptionNode(component, !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place public static bool DoesItemHaveContextualOrders(Item item) { - if (Order.PrefabList.Any(o => o.TargetItemsMatchItem(item))) { return true; } - if (Order.PrefabList.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } + if (OrderPrefab.Prefabs.Any(o => o.TargetItemsMatchItem(item))) { return true; } + if (OrderPrefab.Prefabs.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled, checkInventory: false)) { return true; } if (AIObjectiveCleanupItems.IsValidContainer(item, Character.Controlled)) { return true; } - if (Order.GetPrefab("loaditems") is Order loadItemsPrefab && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } + if (OrderPrefab.Prefabs.TryGet("loaditems", out OrderPrefab loadItemsPrefab) && AIObjectiveLoadItems.IsValidTarget(item, Character.Controlled, targetContainerTags: loadItemsPrefab.GetTargetItems())) { return true; } if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { return true; } - return Order.GetPrefab("operateweapons") is Order operateWeaponsPrefab && item.Components.Any(c => c is Controller) && - (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || - item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); + return OrderPrefab.Prefabs.TryGet("operateweapons", out OrderPrefab operateWeaponsPrefab) && item.Components.Any(c => c is Controller) && + (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) || + item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item))); } /// Use a negative value (e.g. -1) if there should be no hotkey associated with the node @@ -2772,7 +2795,7 @@ namespace Barotrauma disableNode = !CanCharacterBeHeard(); } - bool mustSetOptionOrTarget = order.HasOptions; + bool mustSetOptionOrTarget = order.Prefab.HasOptions; Item orderTargetEntity = null; // If the order doesn't have options, but must set a target, @@ -2811,7 +2834,7 @@ namespace Barotrauma } var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null; int priority = GetManualOrderPriority(character, o); - SetCharacterOrder(character, o, null, priority, Character.Controlled); + SetCharacterOrder(character, o.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); } return true; @@ -2840,6 +2863,11 @@ namespace Barotrauma return node; } + private struct MinimapNodeData + { + public Order Order; + } + private void CreateMinimapNodes(Order order, Submarine submarine, List matchingItems) { // TODO: Further adjustments to frameSize calculations @@ -2899,7 +2927,10 @@ namespace Barotrauma anchor = Anchor.BottomLeft; } - var userData = new Tuple(item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), ""); + var userData = new MinimapNodeData + { + Order = item == null ? order : order.WithItemComponent(item, order.GetTargetItemComponent(item)) + }; var optionElement = new GUIButton( new RectTransform( new Point((int)(50 * GUI.Scale)), @@ -2908,38 +2939,42 @@ namespace Barotrauma style: null) { UserData = userData, - Font = GUI.SmallFont, - OnClicked = (button, userData) => + Font = GUIStyle.SmallFont, + OnClicked = (button, obj) => { if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - if (o.Item1.HasOptions) + var o = (MinimapNodeData)obj; + if (o.Order.Prefab.HasOptions) { - NavigateForward(button, userData); + NavigateForward(button, o); } - else if (o.Item1.MustManuallyAssign && characterContext == null) + else if (o.Order.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(button); } else { - var character = characterContext ?? GetCharacterForQuickAssignment(o.Item1); - int priority = GetManualOrderPriority(character, o.Item1); - SetCharacterOrder(character, o.Item1, o.Item2, priority, Character.Controlled); + var character = characterContext ?? GetCharacterForQuickAssignment(o.Order); + int priority = GetManualOrderPriority(character, o.Order); + SetCharacterOrder( + character, + o.Order + .WithManualPriority(priority) + .WithOrderGiver(Character.Controlled)); DisableCommandUI(); } return true; } }; - if (CanOpenManualAssignment(optionElement)) + if (CanOpenManualAssignmentMinimapOrder(optionElement)) { optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); } - var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && - o.Order.Identifier == userData.Item1.Identifier && - o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; + var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o != null && + o.Identifier == userData.Order.Identifier && + o.TargetEntity == userData.Order.TargetEntity)) ? 0.5f : 1f; CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name); - optionNodes.Add(new Tuple(optionElement, Keys.None)); + optionNodes.Add(new OptionNode(optionElement, Keys.None)); optionElements.Add(optionElement); } @@ -2967,37 +3002,37 @@ namespace Barotrauma targetItem = !order.UseController ? itemContext : itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item; } - var o = (targetItem == null || !order.IsPrefab) ? order : new Order(order, targetItem, order.GetTargetItemComponent(targetItem)); + var o = targetItem == null ? order : order.WithItemComponent(targetItem, order.GetTargetItemComponent(targetItem)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(order.Options.Length), GetFirstNodeAngle(order.Options.Length)); var offsetIndex = 0; for (int i = 0; i < order.Options.Length; i++) { - optionNodes.Add(new Tuple( - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), + optionNodes.Add(new OptionNode( + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o.WithOption(order.Options[i]), (i + 1) % 10), Keys.D0 + (i + 1) % 10)); } } - private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, string option, string optionName, int hotkey) + private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, int hotkey) { var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { - UserData = new Tuple(order, option), + UserData = order, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - if (o.Item1.MustManuallyAssign && characterContext == null) + var o = userData as Order; + if (o.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(button); } else { - var character = characterContext ?? GetCharacterForQuickAssignment(o.Item1); - int priority = GetManualOrderPriority(character, o.Item1); - SetCharacterOrder(character, o.Item1, o.Item2, priority, Character.Controlled); + var character = characterContext ?? GetCharacterForQuickAssignment(o); + int priority = GetManualOrderPriority(character, o); + SetCharacterOrder(character, o.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); } return true; @@ -3010,8 +3045,9 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); GUIImage icon = null; - if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) + if (order.Prefab.OptionSprites.TryGetValue(order.Option, out Sprite sprite)) { + var optionName = order.Prefab.GetOptionName(order.Option); var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + @@ -3039,13 +3075,11 @@ namespace Barotrauma return false; } - var order = (node.UserData is Order) ? - new Tuple(node.UserData as Order, null) : - node.UserData as Tuple; - var characters = GetCharactersForManualAssignment(order.Item1); + var order = node.UserData is MinimapNodeData minimapNodeData ? minimapNodeData.Order : node.UserData as Order; + var characters = GetCharactersForManualAssignment(order); if (characters.None()) { return false; } - if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) + if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; @@ -3065,7 +3099,7 @@ namespace Barotrauma } else { - if (string.IsNullOrEmpty(order.Item2)) + if (order.Option.IsEmpty) { SetCenterNode(node as GUIButton, resetAnchor: true); } @@ -3077,9 +3111,9 @@ namespace Barotrauma { UserData = node.UserData }; - if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) + if (order.Prefab.OptionSprites.TryGetValue(order.Option, out Sprite sprite)) { - CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Color, tooltip: order.GetOptionName(order.Option)); //TODO: revise tooltip } SetCenterNode(clickedOptionNode); node = null; @@ -3146,7 +3180,7 @@ namespace Barotrauma UserData = order, OnClicked = ExpandAssignmentNodes }; - CreateNodeIcon(expandNode.RectTransform, "CommandExpandNode", order.Item1.Color, tooltip: TextManager.Get("commandui.expand")); + CreateNodeIcon(expandNode.RectTransform, "CommandExpandNode", order.Color, tooltip: TextManager.Get("commandui.expand")); hotkey = optionNodes.Count + 1; CreateHotkeyIcon(expandNode.RectTransform, hotkey % 10); @@ -3186,12 +3220,12 @@ namespace Barotrauma firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions)))); for (int i = 0; i < extraOptionCharacters.Count && i < availableNodePositions; i++) { - CreateAssignmentNode(userData as Tuple, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); + CreateAssignmentNode(userData as Order, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); } return true; } - private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) + private void CreateAssignmentNode(Order order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) { // Button var node = new GUIButton( @@ -3203,8 +3237,8 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var character = userData as Character; - int priority = GetManualOrderPriority(character, order.Item1); - SetCharacterOrder(character, order.Item1, order.Item2, priority, Character.Controlled); + int priority = GetManualOrderPriority(character, order); + SetCharacterOrder(character, order.WithManualPriority(priority).WithOrderGiver(Character.Controlled)); DisableCommandUI(); return true; } @@ -3216,11 +3250,11 @@ namespace Barotrauma // Order icon var topOrderInfo = character.GetCurrentOrderWithTopPriority(); GUIImage orderIcon; - if (topOrderInfo.HasValue) + if (topOrderInfo != null) { - orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.Value.Order.SymbolSprite, scaleToFit: true); - var tooltip = topOrderInfo.Value.Order.Name; - if (!string.IsNullOrWhiteSpace(topOrderInfo.Value.OrderOption)) { tooltip += " (" + topOrderInfo.Value.Order.GetOptionName(topOrderInfo.Value.OrderOption) + ")"; }; + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.SymbolSprite, scaleToFit: true); + var tooltip = topOrderInfo.Name; + if (topOrderInfo.Option != Identifier.Empty) { tooltip += " (" + topOrderInfo.GetOptionName(topOrderInfo.Option) + ")"; }; orderIcon.ToolTip = tooltip; } else @@ -3235,7 +3269,7 @@ namespace Barotrauma // Name label var width = (int)(nameLabelScale * nodeSize.X); - var font = GUI.SmallFont; + var font = GUIStyle.SmallFont; var nameLabel = new GUITextBlock( new RectTransform(new Point(width, 0), parent: node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.BottomCenter) { @@ -3244,7 +3278,7 @@ namespace Barotrauma ToolBox.LimitString(character.Info?.DisplayName, font, width), textColor: jobColor * nodeColorMultiplier, font: font, textAlignment: Alignment.Center, style: null) { CanBeFocused = false, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, HoverTextColor = jobColor }; @@ -3277,7 +3311,7 @@ namespace Barotrauma if (hotkey >= 0) { if (canHear) { CreateHotkeyIcon(node.RectTransform, hotkey); } - optionNodes.Add(new Tuple(node, canHear ? Keys.D0 + hotkey : Keys.None)); + optionNodes.Add(new OptionNode(node, canHear ? Keys.D0 + hotkey : Keys.None)); } else { @@ -3285,7 +3319,7 @@ namespace Barotrauma } } - private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) + private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, LocalizedString tooltip = null) { // Icon return new GUIImage( @@ -3305,7 +3339,7 @@ namespace Barotrauma /// /// Create node icon with a fixed absolute size /// - private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) + private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, LocalizedString tooltip = null) { // Icon return new GUIImage( @@ -3322,7 +3356,7 @@ namespace Barotrauma }; } - private void CreateNodeIcon(RectTransform parent, string style, Color? color = null, string tooltip = null) + private void CreateNodeIcon(RectTransform parent, string style, Color? color = null, LocalizedString tooltip = null) { // Icon var icon = new GUIImage( @@ -3364,21 +3398,19 @@ namespace Barotrauma }; } - private void CreateBlockIcon(RectTransform parent, string tooltip = null) + private void CreateBlockIcon(RectTransform parent, LocalizedString tooltip = null) { var icon = new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { CanBeFocused = false, - Color = GUI.Style.Red * nodeColorMultiplier, - HoverColor = GUI.Style.Red + Color = GUIStyle.Red * nodeColorMultiplier, + HoverColor = GUIStyle.Red }; - if (!string.IsNullOrEmpty(tooltip)) + if (!tooltip.IsNullOrEmpty()) { - icon.ToolTip = tooltip; - string color = XMLExtensions.ColorToString(GUI.Style.Red); + string color = XMLExtensions.ColorToString(GUIStyle.Red); tooltip = $"‖color:{color}‖{tooltip}‖color:end‖"; - var richTextData = RichTextData.GetRichTextData(tooltip, out _); - icon.TooltipRichTextData = richTextData; + icon.ToolTip = RichString.Rich(tooltip); icon.CanBeFocused = true; } } @@ -3469,13 +3501,13 @@ namespace Barotrauma private void SetCharacterTooltip(GUIComponent component, Character character) { if (component == null) { return; } - var tooltip = character?.Info != null ? characterContext.Info.DisplayName : null; - if (string.IsNullOrWhiteSpace(tooltip)) { component.ToolTip = tooltip; return; } - if (character.Info?.Job != null && !string.IsNullOrWhiteSpace(characterContext.Info.Job.Name)) { tooltip += " (" + characterContext.Info.Job.Name + ")"; } + LocalizedString tooltip = character?.Info != null ? characterContext.Info.DisplayName : null; + if (tooltip.IsNullOrWhiteSpace()) { component.ToolTip = tooltip; return; } + if (character.Info?.Job != null && !characterContext.Info.Job.Name.IsNullOrWhiteSpace()) { tooltip += " (" + characterContext.Info.Job.Name + ")"; } component.ToolTip = tooltip; } - private string GetOrderNameBasedOnContextuality(Order order) + private LocalizedString GetOrderNameBasedOnContextuality(Order order) { if (order == null) { return ""; } if (isContextual) { return order.ContextualName; } @@ -3488,9 +3520,12 @@ namespace Barotrauma } private bool IsOrderAvailable(Order order) + => IsOrderAvailable(order.Prefab); + + private bool IsOrderAvailable(OrderPrefab order) { if (order == null) { return false; } - switch (order.Identifier.ToLowerInvariant()) + switch (order.Identifier.Value.ToLowerInvariant()) { case "assaultenemy": Character character = characterContext ?? Character.Controlled; @@ -3502,16 +3537,28 @@ namespace Barotrauma } #region Crew Member Assignment Logic - private bool CanOpenManualAssignment(GUIComponent node) + private bool CanOpenManualAssignmentMinimapOrder(GUIComponent node) { if (node == null || characterContext != null) { return false; } - if (node.UserData is (Order minimapOrder, string option)) + if (node.UserData is MinimapNodeData {Order: { } minimapOrder}) { - return !minimapOrder.TargetAllCharacters && (!minimapOrder.HasOptions || !string.IsNullOrEmpty(option)); + return !minimapOrder.TargetAllCharacters && (!minimapOrder.Prefab.HasOptions || !minimapOrder.Option.IsEmpty); } if (node.UserData is Order nodeOrder) { - return !nodeOrder.TargetAllCharacters && !nodeOrder.HasOptions && + return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && + (!nodeOrder.MustSetTarget || itemContext != null || + nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + } + return false; + } + + private bool CanOpenManualAssignment(GUIComponent node) + { + if (node == null || characterContext != null) { return false; } + if (node.UserData is Order nodeOrder) + { + return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && (!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); } @@ -3594,11 +3641,15 @@ namespace Barotrauma ReportButtonFrame.Visible = false; } } - + private void ToggleReportButton(string orderIdentifier, bool enabled) { - Order order = Order.GetPrefab(orderIdentifier); - var reportButton = ReportButtonFrame.GetChildByUserData(order); + ToggleReportButton(orderIdentifier.ToIdentifier(), enabled); + } + + private void ToggleReportButton(Identifier orderIdentifier, bool enabled) + { + var reportButton = ReportButtonFrame.FindChild(c => c.UserData is OrderPrefab orderPrefab && orderPrefab.Identifier == orderIdentifier); if (reportButton != null) { reportButton.GetChildByUserData("highlighted").Visible = enabled; @@ -3651,12 +3702,12 @@ namespace Barotrauma ushort orderGiverId = inc.ReadUInt16(); orderGiver = orderGiverId != Entity.NullEntityID ? Entity.FindEntityByID(orderGiverId) as Character : null; } - if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + if (orderMessageInfo.OrderIdentifier == Identifier.Empty) { - DebugConsole.ThrowError("Invalid active order - order index out of bounds."); + DebugConsole.ThrowError("Invalid active order - order identifier empty."); continue; } - Order orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; + OrderPrefab orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; Order order = orderMessageInfo.TargetType switch { Order.OrderTargetType.Entity => diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 6fa04747c..9f8368a3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -14,7 +14,7 @@ namespace Barotrauma protected bool crewDead; protected Color overlayColor; - protected string overlayText, overlayTextBottom; + protected LocalizedString overlayText, overlayTextBottom; protected Color overlayTextColor; protected Sprite overlaySprite; @@ -68,8 +68,8 @@ namespace Barotrauma foreach (Mission mission in Missions.ToList()) { new GUIMessageBox( - mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name, - mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon, parseRichText: true) + RichString.Rich(mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name), + RichString.Rich(mission.Description), Array.Empty(), type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) { IconColor = mission.Prefab.IconColor, UserData = "missionstartmessage" @@ -123,12 +123,12 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), overlayColor, isFilled: true); } - if (!string.IsNullOrEmpty(overlayText) && overlayTextColor.A > 0) + if (!overlayText.IsNullOrEmpty() && overlayTextColor.A > 0) { - var backgroundSprite = GUI.Style.GetComponentStyle("CommandBackground").GetDefaultSprite(); + var backgroundSprite = GUIStyle.GetComponentStyle("CommandBackground").GetDefaultSprite(); Vector2 centerPos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2; - string wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUI.Font); - Vector2 textSize = GUI.Font.MeasureString(wrappedText); + LocalizedString wrappedText = ToolBox.WrapText(overlayText, GameMain.GraphicsWidth / 3, GUIStyle.Font); + Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText); Vector2 textPos = centerPos - textSize / 2; backgroundSprite.Draw(spriteBatch, centerPos, @@ -140,11 +140,11 @@ namespace Barotrauma GUI.DrawString(spriteBatch, textPos + Vector2.One, wrappedText, Color.Black * (overlayTextColor.A / 255.0f)); GUI.DrawString(spriteBatch, textPos, wrappedText, overlayTextColor); - if (!string.IsNullOrEmpty(overlayTextBottom)) + if (!overlayTextBottom.IsNullOrEmpty()) { - Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUI.Font.MeasureString(overlayTextBottom) / 2; - GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom, Color.Black * (overlayTextColor.A / 255.0f)); - GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom, overlayTextColor); + Vector2 bottomTextPos = centerPos + new Vector2(0.0f, textSize.Y / 2 + 40 * GUI.Scale) - GUIStyle.Font.MeasureString(overlayTextBottom) / 2; + GUI.DrawString(spriteBatch, bottomTextPos + Vector2.One, overlayTextBottom.Value, Color.Black * (overlayTextColor.A / 255.0f)); + GUI.DrawString(spriteBatch, bottomTextPos, overlayTextBottom.Value, overlayTextColor); } } } @@ -159,7 +159,7 @@ namespace Barotrauma endRoundButton.Visible = false; var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); - string buttonText = ""; + LocalizedString buttonText = ""; switch (availableTransition) { case TransitionType.ProgressToNextLocation: @@ -188,7 +188,7 @@ namespace Barotrauma case TransitionType.None: default: if (Level.Loaded.Type == LevelData.LevelType.Outpost && - (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false))) + (Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) { buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; @@ -218,7 +218,7 @@ namespace Barotrauma endRoundButton.OnClicked(EndRoundButton, null); prevCampaignUIAutoOpenType = availableTransition; } - endRoundButton.Text = ToolBox.LimitString(buttonText, endRoundButton.Font, endRoundButton.Rect.Width - 5); + endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); if (endRoundButton.Text != buttonText) { endRoundButton.ToolTip = buttonText; @@ -244,7 +244,7 @@ namespace Barotrauma if (ReadyCheck.ReadyCheckCooldown > DateTime.Now) { float progress = (ReadyCheck.ReadyCheckCooldown - DateTime.Now).Seconds / 60.0f; - ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); + ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUIStyle.Red); } } } @@ -289,7 +289,7 @@ namespace Barotrauma case InteractionType.Examine: return; case InteractionType.Upgrade when !UpgradeManager.CanUpgradeSub(): - UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade"), IsSinglePlayer, npc); + UpgradeManager.CreateUpgradeErrorMessage(TextManager.Get("Dialog.CantUpgrade").Value, IsSinglePlayer, npc); return; case InteractionType.Crew when GameMain.NetworkMember != null: CampaignUI.CrewManagement.SendCrewState(false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs index 143f88789..8e0430760 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -52,39 +53,39 @@ namespace Barotrauma text = text.TrimEnd('\n'); - List richTextDatas = RichTextData.GetRichTextData(text, out text) ?? new List(); + ImmutableArray? richTextDatas = RichTextData.GetRichTextData(text, out text); - Vector2 size = GUI.SmallFont.MeasureString(text); + Vector2 size = GUIStyle.SmallFont.MeasureString(text); Vector2 infoPos = new Vector2(GameMain.GraphicsWidth - size.X - 16, pos.Y + 8); Rectangle infoRect = new Rectangle(infoPos.ToPoint(), size.ToPoint()); infoRect.Inflate(8, 8); GUI.DrawRectangle(spriteBatch, infoRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, infoRect, Color.White * 0.8f); - if (richTextDatas.Any()) + if (richTextDatas != null && richTextDatas.Value.Any()) { - GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextDatas, font: GUI.SmallFont); + GUI.DrawStringWithColors(spriteBatch, infoPos, text, Color.White, richTextDatas.Value, font: GUIStyle.SmallFont); } else { - GUI.DrawString(spriteBatch, infoPos, text, Color.White, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, infoPos, text, Color.White, font: GUIStyle.SmallFont); } float y = infoRect.Bottom + 16; if (Campaign.Factions != null) { const string factionHeader = "Reputations"; - Vector2 factionHeaderSize = GUI.SubHeadingFont.MeasureString(factionHeader); + Vector2 factionHeaderSize = GUIStyle.SubHeadingFont.MeasureString(factionHeader); Vector2 factionPos = new Vector2(GameMain.GraphicsWidth - (264 / 2) - factionHeaderSize.X / 2, y); - GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, factionPos, factionHeader, Color.White, font: GUIStyle.SubHeadingFont); y += factionHeaderSize.Y + 8; foreach (Faction faction in Campaign.Factions) { - string name = faction.Prefab.Name; - Vector2 nameSize = GUI.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUI.SmallFont); + LocalizedString name = faction.Prefab.Name; + Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); y += nameSize.Y + 5; Color color = ToolBox.GradientLerp(faction.Reputation.NormalizedValue, Color.Red, Color.Yellow, Color.LightGreen); @@ -98,8 +99,8 @@ namespace Barotrauma if (location?.Reputation != null) { string name = Campaign.Map?.CurrentLocation.Name; - Vector2 nameSize = GUI.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUI.SmallFont); + Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 264, y), name, Color.White, font: GUIStyle.SmallFont); y += nameSize.Y + 5; float normalizedReputation = MathUtils.InverseLerp(location.Reputation.MinReputation, location.Reputation.MaxReputation, location.Reputation.Value); @@ -107,8 +108,6 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, (int)(normalizedReputation * 255), 10), color, isFilled: true); GUI.DrawRectangle(spriteBatch, new Rectangle(GameMain.GraphicsWidth - 264, (int) y, 256, 10), Color.White); } - - richTextDatas.Clear(); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 167fab3e8..d628ec7fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -217,8 +217,7 @@ namespace Barotrauma overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); overlayTextColor = Color.Transparent; overlayText = TextManager.GetWithVariables("campaignstart", - new string[] { "xxxx", "yyyy" }, - new string[] { Map.CurrentLocation.Name, TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass) }); + ("xxxx", Map.CurrentLocation.Name), ("yyyy", TextManager.Get($"submarineclass.{Submarine.MainSub.Info.SubmarineClass}"))); float fadeInDuration = 1.0f; float textDuration = 10.0f; float timer = 0.0f; @@ -317,7 +316,7 @@ namespace Barotrauma private IEnumerable DoLevelTransition() { - SoundPlayer.OverrideMusicType = CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead"; + SoundPlayer.OverrideMusicType = (CrewManager.GetCharacters().Any(c => !c.IsDead) ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; Level prevLevel = Level.Loaded; @@ -584,7 +583,7 @@ namespace Barotrauma foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) { msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? string.Empty); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); } } @@ -610,11 +609,11 @@ namespace Barotrauma float? reputation = null; if (msg.ReadBoolean()) { reputation = msg.ReadSingle(); } - Dictionary factionReps = new Dictionary(); + Dictionary factionReps = new Dictionary(); byte factionsCount = msg.ReadByte(); for (int i = 0; i < factionsCount; i++) { - factionReps.Add(msg.ReadString(), msg.ReadSingle()); + factionReps.Add(msg.ReadIdentifier(), msg.ReadSingle()); } bool forceMapUI = msg.ReadBoolean(); @@ -625,12 +624,12 @@ namespace Barotrauma bool purchasedLostShuttles = msg.ReadBoolean(); byte missionCount = msg.ReadByte(); - List> availableMissions = new List>(); + var availableMissions = new List<(Identifier Identifier, byte ConnectionIndex)>(); for (int i = 0; i < missionCount; i++) { - string missionIdentifier = msg.ReadString(); + Identifier missionIdentifier = msg.ReadIdentifier(); byte connectionIndex = msg.ReadByte(); - availableMissions.Add(new Pair(missionIdentifier, connectionIndex)); + availableMissions.Add((missionIdentifier, connectionIndex)); } UInt16? storeBalance = null; @@ -643,7 +642,7 @@ namespace Barotrauma List buyCrateItems = new List(); for (int i = 0; i < buyCrateItemCount; i++) { - string itemPrefabIdentifier = msg.ReadString(); + Identifier itemPrefabIdentifier = msg.ReadIdentifier(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } @@ -661,7 +660,7 @@ namespace Barotrauma List purchasedItems = new List(); for (int i = 0; i < purchasedItemCount; i++) { - string itemPrefabIdentifier = msg.ReadString(); + Identifier itemPrefabIdentifier = msg.ReadIdentifier(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } @@ -670,7 +669,7 @@ namespace Barotrauma List soldItems = new List(); for (int i = 0; i < soldItemCount; i++) { - string itemPrefabIdentifier = msg.ReadString(); + Identifier itemPrefabIdentifier = msg.ReadIdentifier(); UInt16 id = msg.ReadUInt16(); bool removed = msg.ReadBoolean(); byte sellerId = msg.ReadByte(); @@ -682,9 +681,9 @@ namespace Barotrauma List pendingUpgrades = new List(); for (int i = 0; i < pendingUpgradeCount; i++) { - string upgradeIdentifier = msg.ReadString(); + Identifier upgradeIdentifier = msg.ReadIdentifier(); UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - string categoryIdentifier = msg.ReadString(); + Identifier categoryIdentifier = msg.ReadIdentifier(); UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); int upgradeLevel = msg.ReadByte(); if (prefab == null || category == null) { continue; } @@ -696,8 +695,8 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - string itemToInstallIdentifier = msg.ReadString(); - ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + 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)); } @@ -769,7 +768,7 @@ namespace Barotrauma foreach (var (identifier, rep) in factionReps) { - Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + Faction faction = campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == identifier); if (faction?.Reputation != null) { faction.Reputation.SetReputation(rep); @@ -788,24 +787,24 @@ namespace Barotrauma foreach (var availableMission in availableMissions) { - MissionPrefab missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier == availableMission.First); + MissionPrefab missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == availableMission.Identifier); if (missionPrefab == null) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.First}\" not found."); + DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.Identifier}\" not found."); continue; } - if (availableMission.Second == 255) + if (availableMission.ConnectionIndex == 255) { campaign.Map.CurrentLocation.UnlockMission(missionPrefab); } else { - if (availableMission.Second < 0 || availableMission.Second >= campaign.Map.CurrentLocation.Connections.Count) + if (availableMission.ConnectionIndex < 0 || availableMission.ConnectionIndex >= campaign.Map.CurrentLocation.Connections.Count) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.First}\" out of range (index: {availableMission.Second}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); + DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.Identifier}\" out of range (index: {availableMission.ConnectionIndex}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); continue; } - LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; + LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.ConnectionIndex]; campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } } @@ -849,7 +848,7 @@ namespace Barotrauma List availableHires = new List(); for (int i = 0; i < availableHireLength; i++) { - CharacterInfo hire = CharacterInfo.ClientRead("human", msg); + CharacterInfo hire = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); hire.Salary = msg.ReadInt32(); availableHires.Add(hire); } @@ -865,7 +864,7 @@ namespace Barotrauma List hiredCharacters = new List(); for (int i = 0; i < hiredLength; i++) { - CharacterInfo hired = CharacterInfo.ClientRead("human", msg); + CharacterInfo hired = CharacterInfo.ClientRead(CharacterPrefab.HumanSpeciesName, msg); hired.Salary = msg.ReadInt32(); hiredCharacters.Add(hired); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 1cc228e61..fb1181c12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -68,7 +68,7 @@ namespace Barotrauma for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); - CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); + CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } InitCampaignData(); @@ -82,7 +82,7 @@ namespace Barotrauma { IsFirstRound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -283,9 +283,9 @@ namespace Barotrauma overlaySprite = Map.CurrentLocation.Type.GetPortrait(Map.CurrentLocation.PortraitId); overlayTextColor = Color.Transparent; overlayText = TextManager.GetWithVariables(showCampaignResetText ? "campaignend4" : "campaignstart", - new string[] { "xxxx", "yyyy" }, - new string[] { Map.CurrentLocation.Name, TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass) }); - string pressAnyKeyText = TextManager.Get("pressanykey"); + ("xxxx", Map.CurrentLocation.Name), + ("yyyy", TextManager.Get("submarineclass." + Submarine.MainSub.Info.SubmarineClass))); + LocalizedString pressAnyKeyText = TextManager.Get("pressanykey"); float fadeInDuration = 2.0f; float textDuration = 10.0f; float timer = 0.0f; @@ -385,7 +385,7 @@ namespace Barotrauma { NextLevel = newLevel; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); - SoundPlayer.OverrideMusicType = success ? "endround" : "crewdead"; + SoundPlayer.OverrideMusicType = (success ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; GUI.SetSavingIndicatorState(success); crewDead = false; @@ -672,9 +672,9 @@ namespace Barotrauma var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); if (subsToLeaveBehind.Any()) { - string msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); + LocalizedString msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked += (btn, userdata) => { LoadNewLevel(); return true; } ; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[0].UserData = Submarine.Loaded.FindAll(s => !subsToLeaveBehind.Contains(s)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 2e4910a85..2cdcec576 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -29,7 +29,7 @@ namespace Barotrauma for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); - CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); + CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } } @@ -93,7 +93,7 @@ namespace Barotrauma private void GenerateOutpost(Submarine submarine) { - Submarine outpost = OutpostGenerator.Generate(OutpostParams ?? OutpostGenerationParams.Params.GetRandom(), OutpostType ?? LocationType.List.GetRandom()); + Submarine outpost = OutpostGenerator.Generate(OutpostParams ?? OutpostGenerationParams.OutpostParams.GetRandomUnsynced(), OutpostType ?? LocationType.Prefabs.GetRandomUnsynced()); outpost.SetPosition(Vector2.Zero); float closestDistance = 0.0f; @@ -131,7 +131,7 @@ namespace Barotrauma if (Character.Controlled != null) { - Character.Controlled.TeleportTo(outpost.GetWaypoints(false).GetRandom(point => point.SpawnType == SpawnType.Human).WorldPosition); + Character.Controlled.TeleportTo(outpost.GetWaypoints(false).GetRandomUnsynced(point => point.SpawnType == SpawnType.Human).WorldPosition); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs deleted file mode 100644 index 4431dbabb..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ /dev/null @@ -1,683 +0,0 @@ -using Barotrauma.Items.Components; -using FarseerPhysics; -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Tutorials -{ - class BasicTutorial : ScenarioTutorial - { - public BasicTutorial(XElement element) - : base(element) - { - } - - public override IEnumerable UpdateState() - { - Character Controlled = Character.Controlled; - if (Controlled == null) yield return CoroutineStatus.Success; - - foreach (Item item in Item.ItemList) - { - var wire = item.GetComponent(); - if (wire != null && wire.Connections.Any(c => c != null)) - { - wire.Locked = true; - } - } - - //remove all characters except the controlled one to prevent any unintended monster attacks - var existingCharacters = Character.CharacterList.FindAll(c => c != Controlled); - foreach (Character c in existingCharacters) - { - c.Remove(); - } - - yield return new WaitForSeconds(4.0f); - - infoBox = CreateInfoFrame("", "Use WASD to move and the mouse to look around"); - - yield return new WaitForSeconds(5.0f); - - //----------------------------------- - - infoBox = CreateInfoFrame("", "Open the door at your right side by highlighting the button next to it with your cursor and pressing E"); - - Door tutorialDoor = Item.ItemList.Find(i => i.HasTag("tutorialdoor")).GetComponent(); - - while (!tutorialDoor.IsOpen && Controlled.WorldPosition.X < tutorialDoor.Item.WorldPosition.X) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - yield return new WaitForSeconds(2.0f); - - //----------------------------------- - - infoBox = CreateInfoFrame("", "Hold W or S to walk up or down stairs. Use shift to run.", hasButton: true); - - while (infoBox != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - //----------------------------------- - - infoBox = CreateInfoFrame("", "At the moment the submarine has no power, which means that crucial systems such as the oxygen generator or the engine aren't running. Let's fix this: go to the upper left corner of the submarine, where you'll find a nuclear reactor."); - - Reactor reactor = Item.ItemList.Find(i => i.HasTag("tutorialreactor")).GetComponent(); - //reactor.MeltDownTemp = 20000.0f; - - while (Vector2.Distance(Controlled.Position, reactor.Item.Position) > 200.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The reactor requires fuel rods to generate power. You can grab one from the steel cabinet by walking next to it and pressing E."); - - while (Controlled.SelectedConstruction == null || Controlled.SelectedConstruction.Prefab.Identifier != "steelcabinet") - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Pick up one of the fuel rods either by double-clicking or dragging and dropping it into your inventory."); - - while (!HasItem("fuelrod")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Select the reactor by walking next to it and pressing E."); - - while (Controlled.SelectedConstruction != reactor.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "Load the fuel rod into the reactor by dropping it into any of the 5 slots."); - - while (reactor.AvailableFuel <= 0.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The reactor is now fueled up. Try turning it on by increasing the fission rate."); - - while (reactor.FissionRate <= 0.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "The reactor core has started generating heat, which in turn generates power for the submarine. The power generation is very low at the moment," - + " because the reactor is set to shut itself down when the temperature rises above 500 degrees Celsius. You can adjust the temperature limit by changing the \"Shutdown Temperature\" in the control panel.", hasButton: true); - - //TODO: reimplement - /*while (infoBox != null) - { - reactor.ShutDownTemp = Math.Min(reactor.ShutDownTemp, 5000.0f); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("The amount of power generated by the reactor should be kept close to the amount of power consumed by the devices in the submarine. " - + "If there isn't enough power, devices won't function properly (or at all), and if there's too much power, some devices may be damaged." - + " Try to raise the temperature of the reactor close to 3000 degrees by adjusting the fission and cooling rates.", true); - - while (Math.Abs(reactor.Temperature - 3000.0f) > 100.0f) - { - reactor.AutoTemp = false; - reactor.ShutDownTemp = Math.Min(reactor.ShutDownTemp, 5000.0f); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("Looks like we're up and running! Now you should turn on the \"Automatic temperature control\", which will make the reactor " - + "automatically adjust the temperature to a suitable level. Even though it's an easy way to keep the reactor up and running most of the time, " - + "you should keep in mind that it changes the temperature very slowly and carefully, which may cause issues if there are sudden changes in grid load."); - - while (!reactor.AutoTemp) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - }*/ - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "That's the basics of operating the reactor! Now that there's power available for the engines, it's time to get the submarine moving. " - + "Deselect the reactor by pressing E and head to the command room at the right edge of the vessel."); - - Steering steering = Item.ItemList.Find(i => i.HasTag("tutorialsteering")).GetComponent(); - Sonar sonar = steering.Item.GetComponent(); - - while (Vector2.Distance(Controlled.Position, steering.Item.Position) > 150.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - CoroutineManager.StartCoroutine(KeepReactorRunning(reactor)); - - infoBox = CreateInfoFrame("", "Select the navigation terminal by walking next to it and pressing E."); - - while (Controlled.SelectedConstruction != steering.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "There seems to be something wrong with the navigation terminal." + - " There's nothing on the monitor, so it's probably out of power. The reactor must still be" - + " running or the lights would've gone out, so it's most likely a problem with the wiring." - + " Deselect the terminal by pressing E to start checking the wiring."); - - while (Controlled.SelectedConstruction == steering.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(1.0f); - - infoBox = CreateInfoFrame("", "You need a screwdriver to check the wiring of the terminal." - + " Equip a screwdriver by pulling it to either of the slots with a hand symbol, and then use it on the terminal by left clicking."); - - while (Controlled.SelectedConstruction != steering.Item || - Controlled.HeldItems.FirstOrDefault(i => i.Prefab.Identifier == "screwdriver") == null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - - infoBox = CreateInfoFrame("", "Here you can see all the wires connected to the terminal. Apparently there's no wire" - + " going into the to the power connection - that's why the monitor isn't working." - + " You should find a piece of wire to connect it. Try searching some of the cabinets scattered around the sub."); - - while (!HasItem("wire")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Head back to the navigation terminal to fix the wiring."); - - PowerTransfer junctionBox = Item.ItemList.Find(i => i != null && i.HasTag("tutorialjunctionbox")).GetComponent(); - - while ((Controlled.SelectedConstruction != junctionBox.Item && - Controlled.SelectedConstruction != steering.Item) || - !Controlled.HeldItems.Any(i => i.Prefab.Identifier == "screwdriver")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - if (!Controlled.HeldItems.Any(i => i.GetComponent() != null)) - { - infoBox = CreateInfoFrame("", "Equip the wire by dragging it to one of the slots with a hand symbol."); - - while (!Controlled.HeldItems.Any(i => i.GetComponent() != null)) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - } - - infoBox = CreateInfoFrame("", "You can see the equipped wire at the middle of the connection panel. Drag it to the power connector."); - - var steeringConnection = steering.Item.Connections.Find(c => c.Name.Contains("power")); - - while (steeringConnection.Wires.FirstOrDefault(w => w != null) == null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - - } - - infoBox = CreateInfoFrame("", "Now you have to connect the other end of the wire to a power source. " - + "The junction box in the room just below the command room should do."); - - while (Controlled.SelectedConstruction != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - yield return new WaitForSeconds(2.0f); - - infoBox = CreateInfoFrame("", "You can now move the other end of the wire around, and attach it on the wall by left clicking or " - + "remove the previous attachment by right clicking. Or if you don't care for neatly laid out wiring, you can just " - + "run it straight to the junction box."); - - while (Controlled.SelectedConstruction == null || Controlled.SelectedConstruction.GetComponent() == null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Connect the wire to the junction box by pulling it to the power connection, the same way you did with the navigation terminal."); - - while (sonar.Voltage < 0.1f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Great! Now we should be able to get moving."); - - - while (Controlled.SelectedConstruction != steering.Item) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "You can take a look at the area around the sub by selecting the \"Active Sonar\" checkbox."); - - while (!sonar.IsActive) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(0.5f); - - infoBox = CreateInfoFrame("", "The blue rectangle in the middle is the submarine, and the flickering shapes outside it are the walls of an underwater cavern. " - + "Try moving the submarine by clicking somewhere on the monitor and dragging the pointer to the direction you want to go to."); - - while (steering.TargetVelocity == Vector2.Zero && steering.TargetVelocity.Length() < 50.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(4.0f); - - infoBox = CreateInfoFrame("", "The submarine moves up and down by pumping water in and out of the two ballast tanks at the bottom of the submarine. " - + "The engine at the back of the sub moves it forwards and backwards.", hasButton: true); - - while (infoBox != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Steer the submarine downwards, heading further into the cavern."); - - while (Submarine.MainSub.WorldPosition.Y > 32000.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - yield return new WaitForSeconds(1.0f); - - var moloch = Character.Create("moloch", steering.Item.WorldPosition + new Vector2(3000.0f, -500.0f), ""); - - moloch.PlaySound(CharacterSound.SoundType.Attack); - - yield return new WaitForSeconds(1.0f); - - infoBox = CreateInfoFrame("", "Uh-oh... Something enormous just appeared on the sonar."); - - List windows = new List(); - foreach (Structure s in Structure.WallList) - { - if (s.CastShadow || !s.HasBody) continue; - - if (s.Rect.Right > steering.Item.CurrentHull.Rect.Right) windows.Add(s); - } - - float slowdownTimer = 1.0f; - bool broken = false; - do - { - steering.TargetVelocity = Vector2.Zero; - - slowdownTimer = Math.Max(0.0f, slowdownTimer - CoroutineManager.DeltaTime * 0.3f); - Submarine.MainSub.Velocity *= slowdownTimer; - - moloch.AIController.SelectTarget(steering.Item.CurrentHull.AiTarget); - Vector2 steeringDir = windows[0].WorldPosition - moloch.WorldPosition; - if (steeringDir != Vector2.Zero) steeringDir = Vector2.Normalize(steeringDir); - - moloch.AIController.SteeringManager.SteeringManual(CoroutineManager.DeltaTime, steeringDir * 100.0f); - - foreach (Structure window in windows) - { - for (int i = 0; i < window.SectionCount; i++) - { - if (!window.SectionIsLeaking(i)) continue; - broken = true; - break; - } - if (broken) break; - } - if (broken) break; - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } while (!broken); - - //fix everything except the command windows - foreach (Structure w in Structure.WallList) - { - bool isWindow = windows.Contains(w); - - for (int i = 0; i < w.SectionCount; i++) - { - if (!w.SectionIsLeaking(i)) continue; - - if (isWindow) - { - //decrease window damage to slow down the leaking - w.AddDamage(i, -w.SectionDamage(i) * 0.48f); - } - else - { - w.AddDamage(i, -100000.0f); - } - } - } - - Submarine.MainSub.GodMode = true; - - var capacitor1 = Item.ItemList.Find(i => i.HasTag("capacitor1")).GetComponent(); - var capacitor2 = Item.ItemList.Find(i => i.HasTag("capacitor1")).GetComponent(); - CoroutineManager.StartCoroutine(KeepEnemyAway(moloch, new PowerContainer[] { capacitor1, capacitor2 })); - - infoBox = CreateInfoFrame("", "The hull has been breached! Close all the doors to the command room to stop the water from flooding the entire sub!"); - - Door commandDoor1 = Item.ItemList.Find(i => i.HasTag("commanddoor1")).GetComponent(); - Door commandDoor2 = Item.ItemList.Find(i => i.HasTag("commanddoor2")).GetComponent(); - - //wait until the player is out of the room and the doors are closed - while (Controlled.WorldPosition.X > commandDoor1.Item.WorldPosition.X || - (commandDoor1.IsOpen || commandDoor2.IsOpen)) - { - //prevent the hull from filling up completely and crushing the player - steering.Item.CurrentHull.WaterVolume = Math.Min(steering.Item.CurrentHull.WaterVolume, steering.Item.CurrentHull.Volume * 0.9f); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - - infoBox = CreateInfoFrame("", "You should quickly find yourself a diving mask or a diving suit. " + - "There are some in the room next to the airlock."); - - bool divingMaskSelected = false; - - while (!HasItem("divingmask") && !HasItem("divingsuit")) - { - if (!divingMaskSelected && - Controlled.FocusedItem != null && Controlled.FocusedItem.Prefab.Identifier == "divingsuit") - { - infoBox = CreateInfoFrame("", "There can only be one item in each inventory slot, so you need to take off " - + "the jumpsuit if you wish to wear a diving suit."); - - divingMaskSelected = true; - } - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - if (HasItem("divingmask")) - { - infoBox = CreateInfoFrame("", "The diving mask will let you breathe underwater, but it won't protect from the water pressure outside the sub. " + - "It should be fine for the situation at hand, but you still need to find an oxygen tank and drag it into the same slot as the mask." + - "You should grab one or two from one of the cabinets."); - } - else if (HasItem("divingsuit")) - { - infoBox = CreateInfoFrame("", "In addition to letting you breathe underwater, the suit will protect you from the water pressure outside the sub " + - "(unlike the diving mask). However, you still need to drag an oxygen tank into the same slot as the suit to supply oxygen. " + - "You should grab one or two from one of the cabinets."); - } - - while (!HasItem("oxygentank")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - yield return new WaitForSeconds(5.0f); - - infoBox = CreateInfoFrame("", "Now you should stop the creature attacking the submarine before it does any more damage. Head to the railgun room at the upper right corner of the sub."); - - var railGun = Item.ItemList.Find(i => i.GetComponent() != null); - - while (Vector2.Distance(Controlled.Position, railGun.Position) > 500) - { - yield return new WaitForSeconds(1.0f); - } - - infoBox = CreateInfoFrame("", "The railgun requires a large power surge to fire. The reactor can't provide a surge large enough, so we need to use the " - + " supercapacitors in the railgun room. The capacitors need to be charged first; select them and crank up the recharge rate."); - - while (capacitor1.RechargeSpeed < 0.5f && capacitor2.RechargeSpeed < 0.5f) - { - yield return new WaitForSeconds(1.0f); - } - - infoBox = CreateInfoFrame("", "The capacitors take some time to recharge, so now is a good " + - "time to head to the room below and load some shells for the railgun."); - - - var loader = Item.ItemList.Find(i => i.Prefab.Identifier == "railgunloader").GetComponent(); - - while (Math.Abs(Controlled.Position.Y - loader.Item.Position.Y) > 80) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Grab one of the shells. You can load it by selecting the railgun loader and dragging the shell to. " - + "one of the free slots. You need two hands to carry a shell, so make sure you don't have anything else in either hand."); - - while (loader.Item.ContainedItems.FirstOrDefault(i => i != null && i.Prefab.Identifier == "railgunshell") == null) - { - //TODO: reimplement - //moloch.Health = 50.0f; - - capacitor1.Charge += 5.0f; - capacitor2.Charge += 5.0f; - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "Now we're ready to shoot! Select the railgun controller."); - - while (Controlled.SelectedConstruction == null || Controlled.SelectedConstruction.Prefab.Identifier != "railguncontroller") - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - moloch.AnimController.SetPosition(ConvertUnits.ToSimUnits(Controlled.WorldPosition + Vector2.UnitY * 600.0f)); - - infoBox = CreateInfoFrame("", "Use the right mouse button to aim and wait for the creature to come closer. When you're ready to shoot, " - + "press the left mouse button."); - - while (!moloch.IsDead) - { - if (moloch.WorldPosition.Y > Controlled.WorldPosition.Y + 600.0f) - { - moloch.AIController.SteeringManager.SteeringManual(CoroutineManager.DeltaTime, Controlled.WorldPosition - moloch.WorldPosition); - } - - moloch.AIController.SelectTarget(Controlled.AiTarget); - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - Submarine.MainSub.GodMode = false; - - infoBox = CreateInfoFrame("", "The creature has died. Now you should fix the damages in the control room: " + - "Grab a welding tool from the closet in the railgun room."); - - while (!HasItem("weldingtool")) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The welding tool requires fuel to work. Grab a welding fuel tank and attach it to the tool " + - "by dragging it into the same slot."); - - do - { - var weldingTool = Controlled.Inventory.FindItemByIdentifier("weldingtool"); - if (weldingTool != null && - weldingTool.ContainedItems.FirstOrDefault(contained => contained != null && contained.Prefab.Identifier == "weldingfueltank") != null) break; - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } while (true); - - - infoBox = CreateInfoFrame("", "You can aim with the tool using the right mouse button and weld using the left button. " + - "Head to the command room to fix the leaks there."); - - do - { - broken = false; - foreach (Structure window in windows) - { - for (int i = 0; i < window.SectionCount; i++) - { - if (!window.SectionIsLeaking(i)) continue; - broken = true; - break; - } - if (broken) break; - } - - yield return new WaitForSeconds(1.0f); - } while (broken); - - infoBox = CreateInfoFrame("", "The hull is fixed now, but there's still quite a bit of water inside the sub. It should be pumped out " - + "using the bilge pump in the room at the bottom of the submarine."); - - Pump pump = Item.ItemList.Find(i => i.HasTag("tutorialpump")).GetComponent(); - - while (Vector2.Distance(Controlled.Position, pump.Item.Position) > 100.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The two pumps inside the ballast tanks " - + "are connected straight to the navigation terminal and can't be manually controlled unless you mess with their wiring, " + - "so you should only use the pump in the middle room to pump out the water. Select it, turn it on and adjust the pumping speed " + - "to start pumping water out.", hasButton: true); - - while (infoBox != null) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - - bool brokenMsgShown = false; - - Item brokenBox = null; - - while (pump.FlowPercentage > 0.0f || pump.CurrFlow <= 0.0f || !pump.IsActive) - { - if (!brokenMsgShown && pump.Voltage < pump.MinVoltage && Controlled.SelectedConstruction == pump.Item) - { - brokenMsgShown = true; - - infoBox = CreateInfoFrame("", "Looks like the pump isn't getting any power. The water must have short-circuited some of the junction " - + "boxes. You can check which boxes are broken by selecting them."); - - while (true) - { - if (Controlled.SelectedConstruction!=null && - Controlled.SelectedConstruction.GetComponent() != null && - Controlled.SelectedConstruction.Condition == 0.0f) - { - brokenBox = Controlled.SelectedConstruction; - - infoBox = CreateInfoFrame("", "Here's our problem: this junction box is broken. Luckily engineers are adept at fixing electrical devices - " - + "you just need to find a spare wire and click the \"Fix\"-button to repair the box."); - break; - } - - if (pump.Voltage > pump.MinVoltage) break; - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - } - - if (brokenBox != null && brokenBox.ConditionPercentage > 50.0f && pump.Voltage < pump.MinVoltage) - { - yield return new WaitForSeconds(1.0f); - - if (pump.Voltage < pump.MinVoltage) - { - infoBox = CreateInfoFrame("", "The pump is still not running. Check if there are more broken junction boxes between the pump and the reactor."); - } - brokenBox = null; - } - - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "The pump is up and running. Wait for the water to be drained out."); - - while (pump.Item.CurrentHull.WaterVolume > 1000.0f) - { - yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("", "That was all there is to this tutorial! Now you should be able to handle " + - "most of the basic tasks on board the submarine."); - - Completed = true; - - yield return new WaitForSeconds(4.0f); - - Controlled = null; - GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; - GameMain.LightManager.LosEnabled = false; - - var cinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, Alignment.CenterLeft, Alignment.CenterRight, panDuration: 5.0f); - - while (cinematic.Running) - { - yield return Controlled != null && Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; - } - - Submarine.Unload(); - GameMain.MainMenuScreen.Select(); - - yield return CoroutineStatus.Success; - } - - private bool HasItem(string itemIdentifier) - { - if (Character.Controlled == null) return false; - - return Character.Controlled.Inventory.FindItemByIdentifier(itemIdentifier) != null; - } - - protected IEnumerable KeepReactorRunning(Reactor reactor) - { - do - { - //TODO: reimplement - /*reactor.AutoTemp = true; - reactor.ShutDownTemp = 5000.0f;*/ - - yield return CoroutineStatus.Running; - } while (Item.ItemList.Contains(reactor.Item)); - - yield return CoroutineStatus.Success; - } - - - /// - /// keeps the enemy away from the sub until the capacitors are loaded - /// - private IEnumerable KeepEnemyAway(Character enemy, PowerContainer[] capacitors) - { - do - { - if (enemy == null || Character.Controlled == null) break; - - //TODO: reimplement - //enemy.Health = 50.0f; - - if (enemy.AIController is EnemyAIController enemyAI) - { - enemyAI.State = AIState.Idle; - } - - Vector2 targetPos = Character.Controlled.WorldPosition + new Vector2(0.0f, 3000.0f); - - Vector2 steering = targetPos - enemy.WorldPosition; - if (steering != Vector2.Zero) steering = Vector2.Normalize(steering); - - enemy.AIController.Steering = steering; - - yield return CoroutineStatus.Running; - } while (capacitors.FirstOrDefault(c => c.Charge > 0.4f) == null); - - yield return CoroutineStatus.Success; - } - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 5ad157c47..d6435a6f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -41,30 +41,79 @@ namespace Barotrauma.Tutorials // Variables private Character captain; - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Sprite captain_steerIcon; private Color captain_steerIconColor; - public CaptainTutorial(XElement element) : base(element) + public CaptainTutorial() : base("tutorial.captaintraining".ToIdentifier(), + new Segment( + "Captain.CommandMedic".ToIdentifier(), + "Captain.CommandMedicObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.CommandMedicText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_command.webm", TextTag = "Captain.CommandMedicText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Captain.CommandMechanic".ToIdentifier(), + "Captain.CommandMechanicObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Captain.CommandMechanicText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Captain.CommandSecurity".ToIdentifier(), + "Captain.CommandSecurityObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Captain.CommandSecurityText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Captain.CommandEngineer".ToIdentifier(), + "Captain.CommandEngineerObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Captain.CommandEngineerText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Captain.Undock".ToIdentifier(), + "Captain.UndockObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.UndockText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_undock.webm", TextTag = "Captain.UndockText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Captain.Navigate".ToIdentifier(), + "Captain.NavigateObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.NavigateText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_navigation.webm", TextTag = "Captain.NavigateText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Captain.Dock".ToIdentifier(), + "Captain.DockObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Captain.DockText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_docking.webm", TextTag = "Captain.DockText".ToIdentifier(), Width = 450, Height = 80 })) + { } + + protected override CharacterInfo GetCharacterInfo() { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["captain"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 20), + new Skill("weapons".ToIdentifier(), 20), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 70))); } - public override void Start() + protected override void Initialize() { - base.Start(); - captain = Character.Controlled; radioSpeakerName = TextManager.Get("Tutorial.Radio.Watchman"); GameMain.GameSession.CrewManager.AllowCharacterSwitch = false; - var revolver = FindOrGiveItem(captain, "revolver"); + var revolver = FindOrGiveItem(captain, "revolver".ToIdentifier()); revolver.Unequip(captain); captain.Inventory.RemoveItem(revolver); var captainscap = - captain.Inventory.FindItemByIdentifier("captainscap1") ?? - captain.Inventory.FindItemByIdentifier("captainscap2") ?? - captain.Inventory.FindItemByIdentifier("captainscap3"); + captain.Inventory.FindItemByIdentifier("captainscap1".ToIdentifier()) ?? + captain.Inventory.FindItemByIdentifier("captainscap2".ToIdentifier()) ?? + captain.Inventory.FindItemByIdentifier("captainscap3".ToIdentifier()); if (captainscap != null) { @@ -73,16 +122,16 @@ namespace Barotrauma.Tutorials } var captainsuniform = - captain.Inventory.FindItemByIdentifier("captainsuniform1") ?? - captain.Inventory.FindItemByIdentifier("captainsuniform2") ?? - captain.Inventory.FindItemByIdentifier("captainsuniform3"); + 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); } - var steerOrder = Order.GetPrefab("steer"); + var steerOrder = OrderPrefab.Prefabs["steer"]; captain_steerIcon = steerOrder.SymbolSprite; captain_steerIconColor = steerOrder.Color; @@ -99,7 +148,7 @@ namespace Barotrauma.Tutorials captain_medicSpawnPos = Item.ItemList.Find(i => i.HasTag("captain_medicspawnpos")).WorldPosition; tutorial_submarineDoor = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoor")).GetComponent(); tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); - var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("medicaldoctor")); + var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("medicaldoctor")); captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); captain_medic.TeamID = CharacterTeamType.Team1; captain_medic.GiveJobItems(null); @@ -122,17 +171,17 @@ namespace Barotrauma.Tutorials SetDoorAccess(tutorial_lockedDoor_1, null, false); SetDoorAccess(tutorial_lockedDoor_2, null, false); - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("mechanic")); + var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("mechanic")); captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "mechanic"); captain_mechanic.TeamID = CharacterTeamType.Team1; captain_mechanic.GiveJobItems(); - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); + var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("securityofficer")); captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "securityofficer"); captain_security.TeamID = CharacterTeamType.Team1; captain_security.GiveJobItems(); - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); + var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")); captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "engineer"); captain_engineer.TeamID = CharacterTeamType.Team1; captain_engineer.GiveJobItems(); @@ -163,7 +212,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2f, false); GameMain.GameSession.CrewManager.AutoShowCrewList(); GameMain.GameSession.CrewManager.AddCharacter(captain_medic); - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); do { yield return null; @@ -172,13 +221,13 @@ namespace Barotrauma.Tutorials } while (!HasOrder(captain_medic, "follow")); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); // Submarine do { yield return null; } while (!captain_enteredSubmarineSensor.MotionDetected); yield return new WaitForSeconds(3f, false); captain_mechanic.AIController.Enabled = captain_security.AIController.Enabled = captain_engineer.AIController.Enabled = true; - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_mechanic); do { @@ -187,9 +236,9 @@ namespace Barotrauma.Tutorials // GameMain.GameSession.CrewManager.HighlightOrderButton(captain_mechanic, "repairsystems", highlightColor, new Vector2(5, 5)); //HighlightOrderOption("jobspecific"); } while (!HasOrder(captain_mechanic, "repairsystems") && !HasOrder(captain_mechanic, "repairmechanical") && !HasOrder(captain_mechanic, "repairelectrical")); - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_security); do { @@ -199,9 +248,9 @@ namespace Barotrauma.Tutorials HighlightOrderOption("fireatwill"); } while (!HasOrder(captain_security, "operateweapons")); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); yield return new WaitForSeconds(4f, false); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); GameMain.GameSession.CrewManager.AddCharacter(captain_engineer); do { @@ -211,7 +260,7 @@ namespace Barotrauma.Tutorials HighlightOrderOption("powerup"); } while (!HasOrder(captain_engineer, "operatereactor", "powerup")); - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); tutorial_submarineReactor.CanBeSelected = true; do { yield return null; } while (!tutorial_submarineReactor.IsActive); // Wait until reactor on TriggerTutorialSegment(4); @@ -226,7 +275,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1.0f, false); } while (Submarine.MainSub.DockedTo.Any()); captain_navConsole.UseAutoDocking = false; - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(5); // Navigate to destination do @@ -241,7 +290,7 @@ namespace Barotrauma.Tutorials yield return null; } while (captain_sonar.CurrentMode != Sonar.Mode.Active); do { yield return null; } while (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 4000f); - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); captain_navConsole.UseAutoDocking = true; yield return new WaitForSeconds(4f, false); TriggerTutorialSegment(6); // Docking @@ -250,7 +299,7 @@ namespace Barotrauma.Tutorials //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); } while (!Submarine.MainSub.AtEndExit || !Submarine.MainSub.DockedTo.Any()); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs deleted file mode 100644 index a05c6a835..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ContextualTutorial.cs +++ /dev/null @@ -1,520 +0,0 @@ -/*using System.Collections.Generic; -using System.Xml.Linq; -using System; -using Microsoft.Xna.Framework; -using Barotrauma.Items.Components; -using System.Linq; - -namespace Barotrauma.Tutorials -{ - class ContextualTutorial : Tutorial - { - public ContextualTutorial(XElement element) : base(element) - { - //Name = "ContextualTutorial"; - } - - public static bool Selected = false; - - private Steering navConsole; - private Reactor reactor; - private Sonar sonar; - private Vector2 subStartingPosition; - private List crew; - private Character mechanic; - private Character engineer; - private Character injuredMember = null; - - private List> characterTimeOnSonar; - private float requiredTimeOnSonar = 5f; - - private float tutorialTimer; - - private bool disableTutorialOnDeficiencyFound = true; - - private float floodTutorialTimer = 0.0f; - private const float floodTutorialDelay = 2.0f; - private float medicalTutorialTimer = 0.0f; - private const float medicalTutorialDelay = 2.0f; - - public override void Initialize() - { - base.Initialize(); - - for (int i = 0; i < segments.Count; i++) - { - segments[i].IsTriggered = false; - } - - characterTimeOnSonar = new List>(); - } - - public void LoadPartiallyComplete(XElement element) - { - int[] completedSegments = element.GetAttributeIntArray("completedsegments", null); - - if (completedSegments == null || completedSegments.Length == 0) - { - return; - } - - if (completedSegments.Length == segments.Count) // Completed all segments - { - Stop(); - return; - } - - for (int i = 0; i < completedSegments.Length; i++) - { - segments[completedSegments[i]].IsTriggered = true; - } - } - - public void SavePartiallyComplete(XElement element) - { - XElement tutorialElement = new XElement("contextualtutorial"); - tutorialElement.Add(new XAttribute("completedsegments", GetCompletedSegments())); - element.Add(tutorialElement); - } - - private string GetCompletedSegments() - { - string completedSegments = string.Empty; - - for (int i = 0; i < segments.Count; i++) - { - if (segments[i].IsTriggered) - { - completedSegments += i + ","; - } - } - - if (completedSegments.Length > 0) - { - completedSegments = completedSegments.TrimEnd(','); - } - - return completedSegments; - } - - public override void Start() - { - if (!Initialized) return; - - base.Start(); - injuredMember = null; - activeContentSegment = null; - tutorialTimer = floodTutorialTimer = medicalTutorialTimer = 0.0f; - subStartingPosition = Vector2.Zero; - characterTimeOnSonar.Clear(); - - subStartingPosition = Submarine.MainSub.WorldPosition; - navConsole = Item.ItemList.Find(i => i.HasTag("command"))?.GetComponent(); - sonar = navConsole?.Item.GetComponent(); - reactor = Item.ItemList.Find(i => i.HasTag("reactor"))?.GetComponent(); - -#if DEBUG - if (reactor == null || navConsole == null || sonar == null) - { - infoBox = CreateInfoFrame("Error", "Submarine not compatible with the tutorial:" - + "\nReactor - " + (reactor != null ? "OK" : "Tag 'reactor' not found") - + "\nNavigation Console - " + (navConsole != null ? "OK" : "Tag 'command' not found") - + "\nSonar - " + (sonar != null ? "OK" : "Not found under Navigation Console"), hasButton: true); - CoroutineManager.StartCoroutine(WaitForErrorClosed()); - return; - } -#endif - if (disableTutorialOnDeficiencyFound) - { - if (reactor == null || navConsole == null || sonar == null) - { - Stop(); - return; - } - } - else - { - if (navConsole == null) segments[2].IsTriggered = true; // Disable navigation console usage tutorial - if (reactor == null) segments[5].IsTriggered = true; // Disable reactor usage tutorial - if (sonar == null) segments[6].IsTriggered = true; // Disable enemy on sonar tutorial - } - - crew = GameMain.GameSession.CrewManager.GetCharacters().ToList(); - mechanic = CrewMemberWithJob("mechanic"); - engineer = CrewMemberWithJob("engineer"); - - Completed = true; // Trigger completed at start to prevent the contextual tutorial from automatically activating on starting new campaigns after this one - started = true; - } - -#if DEBUG - private IEnumerable WaitForErrorClosed() - { - while (infoBox != null) yield return null; - Stop(); - } -#endif - - public override void Stop() - { - base.Stop(); - characterTimeOnSonar = null; - } - - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - if (!started || ContentRunning) return; - - deltaTime *= 0.5f; - - for (int i = 0; i < segments.Count; i++) - { - if (segments[i].IsTriggered || HasObjective(segments[i])) continue; - if (CheckContextualTutorials(i, deltaTime)) // Found a relevant tutorial, halt finding new ones - { - break; - } - } - } - - private bool CheckContextualTutorials(int index, float deltaTime) - { - switch (index) - { - case 0: // Welcome: Game Start [Text] - if (tutorialTimer < 1.0f) - { - tutorialTimer += deltaTime; - return false; - } - break; - case 1: // Command Reactor: 2 seconds after 'Welcome' dismissed and only if no command given to start reactor [Video] - if (!segments[0].IsTriggered) return false; - if (tutorialTimer < 3.0f) - { - tutorialTimer += deltaTime; - - if (HasOrder("operatereactor")) - { - segments[index].IsTriggered = true; - tutorialTimer = 2.5f; - } - return false; - } - break; - case 2: // Nav Console: 2 seconds after 'Command Reactor' dismissed or if nav console is activated [Video] - if (!IsReactorPoweredUp()) return false; // Do not advance tutorial based on this segment if reactor has not been powered up - if (Character.Controlled?.SelectedConstruction != navConsole.Item) - { - if (tutorialTimer < 4.5f) - { - tutorialTimer += deltaTime; - return false; - } - } - else - { - tutorialTimer = 4.5f; - } - - TriggerTutorialSegment(index, GameMain.GameSession.EndLocation.Name); - return true; - case 3: // Objective: Travel ~150 meters and while sub is not flooding [Text] - if (Vector2.Distance(subStartingPosition, Submarine.MainSub.WorldPosition) < 8000f || IsFlooding()) - { - return false; - } - else // Called earlier than others due to requiring specific args - { - TriggerTutorialSegment(index, GameMain.GameSession.EndLocation.Name); - return true; - } - case 4: // Flood: Hull is breached and sub is taking on water [Video] - if (!IsFlooding()) - { - return false; - } - else if (floodTutorialTimer < floodTutorialDelay) - { - floodTutorialTimer += deltaTime; - return false; - } - break; - case 5: // Reactor: Player uses reactor for the first time [Video] - if (Character.Controlled?.SelectedConstruction != reactor.Item) - { - return false; - } - break; - case 6: // Enemy on Sonar: Player witnesses creature signal on sonar for 5 seconds [Video] - if (!HasEnemyOnSonarForDuration(deltaTime)) - { - return false; - } - break; - case 7: // Degrading1: Any equipment degrades to 50% health or less and player has not assigned any crew to perform maintenance [Text] - if ((mechanic == null || mechanic.IsDead) && (engineer == null || engineer.IsDead)) // Both engineer and mechanic are dead or do not exist -> do not display - { - return false; - } - - bool degradedEquipmentFound = false; - - foreach (Item item in Item.ItemList) - { - if (!item.Repairables.Any() || item.Condition > 50.0f) continue; - degradedEquipmentFound = true; - break; - } - - if (degradedEquipmentFound) - { - if (HasOrder("repairsystems", "jobspecific")) - { - segments[index].IsTriggered = true; - return false; - } - } - else - { - return false; - } - break; - case 8: // Medical: Crewmember is injured but not killed [Video] - - if (injuredMember == null) - { - for (int i = 0; i < crew.Count; i++) - { - Character member = crew[i]; - if (member.Vitality < member.MaxVitality && !member.IsDead) - { - injuredMember = member; - break; - } - } - - return false; - } - else if (medicalTutorialTimer < medicalTutorialDelay) - { - medicalTutorialTimer += deltaTime; - return false; - } - else - { - TriggerTutorialSegment(index, new string[] { injuredMember.Info.DisplayName, - (injuredMember.Info.Gender == Gender.Male) ? TextManager.Get("PronounPossessiveMale").ToLower() : TextManager.Get("PronounPossessiveFemale").ToLower() }); - return true; - } - case 9: // Approach1: Destination is within ~100m [Video] - if (Vector2.Distance(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) > 8000f) - { - return false; - } - else - { - TriggerTutorialSegment(index, GameMain.GameSession.EndLocation.Name); - return true; - } - case 10: // Approach2: Sub is docked [Text] - if (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Count == 0) - { - return false; - } - break; - } - - TriggerTutorialSegment(index); - return true; - } - - protected override void CheckActiveObjectives(TutorialSegment objective, float deltaTime) - { - switch(objective.Id) - { - case "ReactorCommand": // Reactor commanded - if (!IsReactorPoweredUp()) - { - if (!HasOrder("operatereactor")) return; - } - break; - case "NavConsole": // traveled 50 meters - if (Vector2.Distance(subStartingPosition, Submarine.MainSub.WorldPosition) < 4000f) - { - return; - } - break; - case "Flood": // Hull breaches repaired - if (IsFlooding()) return; - break; - case "Medical": - if (injuredMember != null && !injuredMember.IsDead) - { - if (injuredMember.CharacterHealth.DroppedItem == null) return; - } - break; - case "EnemyOnSonar": // Enemy dispatched - if (HasEnemyOnSonarForDuration(deltaTime)) - { - return; - } - break; - case "Degrading": // Fixed - if (mechanic != null && !mechanic.IsDead) - { - HumanAIController humanAI = mechanic.AIController as HumanAIController; - if (mechanic.CurrentOrder?.AITag != "repairsystems" || humanAI.CurrentOrderOption != "jobspecific") - { - return; - } - } - - if (engineer != null && !engineer.IsDead) - { - HumanAIController humanAI = engineer.AIController as HumanAIController; - if (engineer.CurrentOrder?.AITag != "repairsystems" || humanAI.CurrentOrderOption != "jobspecific") - { - return; - } - } - - break; - case "Approach1": // Wait until docked - if (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Count == 0) - { - return; - } - break; - } - - RemoveCompletedObjective(objective); - } - - private bool IsReactorPoweredUp() - { - float load = 0.0f; - List connections = reactor.Item.Connections; - if (connections != null && connections.Count > 0) - { - foreach (Connection connection in connections) - { - if (!connection.IsPower) continue; - foreach (Connection recipient in connection.Recipients) - { - if (!(recipient.Item is Item it)) continue; - - PowerTransfer pt = it.GetComponent(); - if (pt == null) continue; - - load = Math.Max(load, pt.PowerLoad); - } - } - } - - return Math.Abs(load + reactor.CurrPowerConsumption) < 10; - } - - private Character CrewMemberWithJob(string job) - { - job = job.ToLowerInvariant(); - for (int i = 0; i < crew.Count; i++) - { - if (crew[i].Info.Job.Prefab.Identifier.ToLowerInvariant() == job) return crew[i]; - } - - return null; - } - - private bool HasOrder(string aiTag, string option = null) - { - for (int i = 0; i < crew.Count; i++) - { - if (crew[i].CurrentOrder?.AITag == aiTag) - { - if (option == null) - { - return true; - } - else - { - HumanAIController humanAI = crew[i].AIController as HumanAIController; - return humanAI.CurrentOrderOption == option; - } - } - } - - return false; - } - - private bool IsFlooding() - { - foreach (Gap gap in Gap.GapList) - { - if (gap.ConnectedWall == null || gap.IsRoomToRoom) continue; - if (gap.ConnectedDoor != null || gap.Open <= 0.0f) continue; - if (gap.Submarine == null) continue; - if (gap.Submarine.IsOutpost) continue; - if (gap.Submarine != Submarine.MainSub) continue; - if (gap.FlowTargetHull == null || gap.FlowTargetHull.WaterPercentage <= 0.0f) continue; - return true; - } - - return false; - } - - private bool HasEnemyOnSonarForDuration(float deltaTime) - { - foreach (Character c in Character.CharacterList) - { - if (c.AnimController.CurrentHull != null || !c.Enabled || !(c.AIController is EnemyAIController)) continue; - if (sonar.DetectSubmarineWalls && c.AnimController.CurrentHull == null && sonar.Item.CurrentHull != null) continue; - if (Vector2.DistanceSquared(c.WorldPosition, sonar.Item.WorldPosition) > sonar.Range * sonar.Range) - { - for (int i = 0; i < characterTimeOnSonar.Count; i++) - { - if (characterTimeOnSonar[i].First == c) - { - characterTimeOnSonar.RemoveAt(i); - break; - } - } - - continue; - } - - Pair pair = characterTimeOnSonar.Find(ct => ct.First == c); - if (pair != null) - { - pair.Second += deltaTime; - } - else - { - characterTimeOnSonar.Add(new Pair(c, deltaTime)); - } - } - - return characterTimeOnSonar.Find(ct => ct.Second >= requiredTimeOnSonar && !ct.First.IsDead) != null; - } - - protected override void TriggerTutorialSegment(int index, params object[] args) - { - base.TriggerTutorialSegment(index, args); - - for (int i = 0; i < segments.Count; i++) - { - if (!segments[i].IsTriggered) return; - } - - CoroutineManager.StartCoroutine(WaitToStop()); // Completed - } - - private IEnumerable WaitToStop() - { - while (ContentRunning) yield return null; - Stop(); - } - } -}*/ diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 641b28ad8..8b46096c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Tutorials private float shakeTimer = 1f; private float shakeAmount = 20f; - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character doctor; private ItemContainer doctor_suppliesCabinet; @@ -40,14 +40,66 @@ namespace Barotrauma.Tutorials private Sprite doctor_firstAidIcon; private Color doctor_firstAidIconColor; - public DoctorTutorial(XElement element) : base(element) - { - } - public override void Start() - { - base.Start(); - var firstAidOrder = Order.GetPrefab("requestfirstaid"); + public DoctorTutorial() : base("tutorial.medicaldoctortraining".ToIdentifier(), + new Segment( + "Doctor.Supplies".ToIdentifier(), + "Doctor.SuppliesObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Doctor.SuppliesText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Doctor.OpenMedicalInterface".ToIdentifier(), + "Doctor.OpenMedicalInterfaceObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.OpenMedicalInterfaceText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_medinterface1.webm", TextTag = "Doctor.OpenMedicalInterfaceText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.FirstAidSelf".ToIdentifier(), + "Doctor.FirstAidSelfObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.FirstAidSelfText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_medinterface1.webm", TextTag = "Doctor.FirstAidSelfText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.Medbay".ToIdentifier(), + "Doctor.MedbayObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.MedbayText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_command.webm", TextTag = "Doctor.MedbayText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.TreatBurns".ToIdentifier(), + "Doctor.TreatBurnsObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.TreatBurnsText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_medinterface2.webm", TextTag = "Doctor.TreatBurnsText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.CPR".ToIdentifier(), + "Doctor.CPRObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Doctor.CPRText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_cpr.webm", TextTag = "Doctor.CPRText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Doctor.Submarine".ToIdentifier(), + "Doctor.SubmarineObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Doctor.SubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) + { } + + protected override CharacterInfo GetCharacterInfo() + { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 70), + new Skill("weapons".ToIdentifier(), 20), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 20))); + } + + protected override void Initialize() + { + var firstAidOrder = OrderPrefab.Prefabs["requestfirstaid"]; doctor_firstAidIcon = firstAidOrder.SymbolSprite; doctor_firstAidIconColor = firstAidOrder.Color; @@ -55,19 +107,19 @@ namespace Barotrauma.Tutorials radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); doctor = Character.Controlled; - var bandages = FindOrGiveItem(doctor, "antibleeding1"); + var bandages = FindOrGiveItem(doctor, "antibleeding1".ToIdentifier()); bandages.Unequip(doctor); doctor.Inventory.RemoveItem(bandages); - var syringegun = FindOrGiveItem(doctor, "syringegun"); + var syringegun = FindOrGiveItem(doctor, "syringegun".ToIdentifier()); syringegun.Unequip(doctor); doctor.Inventory.RemoveItem(syringegun); - var antibiotics = FindOrGiveItem(doctor, "antibiotics"); + var antibiotics = FindOrGiveItem(doctor, "antibiotics".ToIdentifier()); antibiotics.Unequip(doctor); doctor.Inventory.RemoveItem(antibiotics); - var morphine = FindOrGiveItem(doctor, "antidama1"); + var morphine = FindOrGiveItem(doctor, "antidama1".ToIdentifier()); morphine.Unequip(doctor); doctor.Inventory.RemoveItem(morphine); @@ -78,7 +130,7 @@ namespace Barotrauma.Tutorials var patientHull2 = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "airlock").CurrentHull; medBay = WayPoint.WayPointList.Find(wp => wp.IdCardDesc == "medbay").CurrentHull; - var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("assistant")); + var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("assistant")); patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); patient1.TeamID = CharacterTeamType.Team1; patient1.GiveJobItems(null); @@ -86,26 +138,26 @@ namespace Barotrauma.Tutorials patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 15.0f) }, stun: 0, playSound: false); patient1.AIController.Enabled = false; - assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("assistant")); + assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("assistant")); patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); patient2.TeamID = CharacterTeamType.Team1; patient2.GiveJobItems(null); patient2.CanSpeak = false; patient2.AIController.Enabled = false; - var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); + var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")); var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient1.TeamID = CharacterTeamType.Team1; subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient1); - var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("securityofficer")); + var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("securityofficer")); var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient2.TeamID = CharacterTeamType.Team1; subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient2); - var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")); + var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: JobPrefab.Get("engineer")); var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job?.Prefab, Submarine.MainSub).WorldPosition, "3"); subPatient3.TeamID = CharacterTeamType.Team1; subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); @@ -196,7 +248,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2.0f); }*/ - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Deselect), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Medical supplies objective + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Medical supplies objective do { @@ -215,24 +267,24 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (doctor.Inventory.FindItemByIdentifier("antidama1") == null); // Wait until looted + } while (doctor.Inventory.FindItemByIdentifier("antidama1".ToIdentifier()) == null); // Wait until looted yield return new WaitForSeconds(1.0f, false); SetHighlight(doctor_suppliesCabinet.Item, false); - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); yield return new WaitForSeconds(1.0f, false); // 2nd tutorial segment, treat self ------------------------------------------------------------------------- - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Health)); // Open health interface + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // Open health interface while (CharacterHealth.OpenHealthWindow == null) { doctor.CharacterHealth.HealthBarPulsateTimer = 1.0f; yield return null; } yield return null; - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); yield return new WaitForSeconds(1.0f, false); TriggerTutorialSegment(2); //Treat self while (doctor.CharacterHealth.GetAfflictionStrength("damage") > 0.01f) @@ -243,13 +295,13 @@ namespace Barotrauma.Tutorials } else { - HighlightInventorySlot(doctor.Inventory, "antidama1", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(doctor.Inventory, "antidama1".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; } - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); SetDoorAccess(doctor_firstDoor, doctor_firstDoorLight, true); while (CharacterHealth.OpenHealthWindow != null) @@ -260,10 +312,10 @@ namespace Barotrauma.Tutorials // treat patient -------------------------------------------------------------------------------------------- //patient 1 requests first aid - var newOrder = new Order(Order.GetPrefab("requestfirstaid"), patient1.CurrentHull, null, orderGiver: patient1); + var newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], patient1.CurrentHull, null, orderGiver: patient1); doctor.AddActiveObjectiveEntity(patient1, doctor_firstAidIcon, doctor_firstAidIconColor); //GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient1.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order, null); + GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient1.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName?.Value, givingOrderToSelf: false), ChatMessageType.Order, null); while (doctor.CurrentHull != patient1.CurrentHull) { @@ -281,9 +333,9 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(3.0f, false); patient1.AIController.Enabled = true; doctor.RemoveActiveObjectiveEntity(patient1); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); // Get the patient to medbay + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)); // Get the patient to medbay - while (patient1.GetCurrentOrderWithTopPriority()?.Order?.Identifier != "follow") + while (patient1.GetCurrentOrderWithTopPriority()?.Identifier != "follow") { // TODO: Rework order highlighting for new command UI // GameMain.GameSession.CrewManager.HighlightOrderButton(patient1, "follow", highlightColor, new Vector2(5, 5)); @@ -296,14 +348,14 @@ namespace Barotrauma.Tutorials { yield return new WaitForSeconds(1.0f, false); } - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); SetHighlight(doctor_medBayCabinet.Item, true); SetDoorAccess(doctor_thirdDoor, doctor_thirdDoorLight, true); patient1.CharacterHealth.UseHealthWindow = true; yield return new WaitForSeconds(2.0f, false); - TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Health)); // treat burns + TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // treat burns do { @@ -322,7 +374,7 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (doctor.Inventory.FindItemByIdentifier("antibleeding1") == null); // Wait until looted + } while (doctor.Inventory.FindItemByIdentifier("antibleeding1".ToIdentifier()) == null); // Wait until looted SetHighlight(doctor_medBayCabinet.Item, false); SetHighlight(patient1, true); @@ -334,12 +386,12 @@ namespace Barotrauma.Tutorials } else { - HighlightInventorySlot(doctor.Inventory, "antibleeding1", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(doctor.Inventory, "antibleeding1".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; } - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); SetHighlight(patient1, false); yield return new WaitForSeconds(1.0f, false); @@ -350,10 +402,10 @@ namespace Barotrauma.Tutorials //patient calls for help //patient2.CanSpeak = true; yield return new WaitForSeconds(2.0f, false); - newOrder = new Order(Order.GetPrefab("requestfirstaid"), patient2.CurrentHull, null, orderGiver: patient2); + newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], patient2.CurrentHull, null, orderGiver: patient2); doctor.AddActiveObjectiveEntity(patient2, doctor_firstAidIcon, doctor_firstAidIconColor); //GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime); - GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient2.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order, null); + GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(patient2.Name, newOrder.GetChatMessage("", patient1.CurrentHull?.DisplayName?.Value, givingOrderToSelf: false), ChatMessageType.Order, null); patient2.AIController.Enabled = true; patient2.Oxygen = -50; CoroutineManager.StartCoroutine(KeepPatientAlive(patient2), "KeepPatient2Alive"); @@ -365,7 +417,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!tutorial_upperFinalDoor.IsOpen); yield return new WaitForSeconds(2.0f, false); - TriggerTutorialSegment(5, GameMain.Config.KeyBindText(InputType.Health)); // perform CPR + TriggerTutorialSegment(5, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // perform CPR SetHighlight(patient2, true); while (patient2.IsUnconscious) { @@ -380,7 +432,7 @@ namespace Barotrauma.Tutorials } yield return null; } - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); SetHighlight(patient2, false); doctor.RemoveActiveObjectiveEntity(patient2); CoroutineManager.StopCoroutines("KeepPatient2Alive"); @@ -399,7 +451,7 @@ namespace Barotrauma.Tutorials GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Doctor.Radio.EnteredSub"), ChatMessageType.Radio, null); yield return new WaitForSeconds(3.0f, false); - TriggerTutorialSegment(6, GameMain.Config.KeyBindText(InputType.Health)); // give treatment to anyone in need + TriggerTutorialSegment(6, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)); // give treatment to anyone in need foreach (var patient in subPatients) { @@ -421,8 +473,8 @@ namespace Barotrauma.Tutorials if (!patientCalledHelp[i] && Timing.TotalTime > subEnterTime + 60 * (i + 1)) { doctor.AddActiveObjectiveEntity(subPatients[i], doctor_firstAidIcon, doctor_firstAidIconColor); - newOrder = new Order(Order.GetPrefab("requestfirstaid"), subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); - string message = newOrder.GetChatMessage("", subPatients[i].CurrentHull?.DisplayName, givingOrderToSelf: false); + newOrder = new Order(OrderPrefab.Prefabs["requestfirstaid"], subPatients[i].CurrentHull, null, orderGiver: subPatients[i]); + string message = newOrder.GetChatMessage("", subPatients[i].CurrentHull?.DisplayName?.Value, givingOrderToSelf: false); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(subPatients[i].Name, message, ChatMessageType.Order, null); patientCalledHelp[i] = true; } @@ -435,7 +487,7 @@ namespace Barotrauma.Tutorials } yield return new WaitForSeconds(1.0f, false); } - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); foreach (var patient in subPatients) { SetHighlight(patient, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs deleted file mode 100644 index d2591a2b1..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EditorTutorial.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Tutorials -{ - class EditorTutorial : Tutorial - { - public EditorTutorial(XElement element) - : base (element) - { - } - - public override IEnumerable UpdateState() - { - /*infoBox = CreateInfoFrame("Use the mouse wheel to zoom in and out, and WASD to move the camera around.", true); - - while (infoBox != null) - { - yield return CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("Press \"Structure\" at the left side of the screen to start placing some walls."); - - while (GameMain.SubEditorScreen.SelectedTab != (int)MapEntityCategory.Structure) - { - yield return CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("Select \"topwall\" from the list.", true); - - while (MapEntityPrefab.Selected == null || MapEntityPrefab.Selected.Name != "topwall") - { - yield return CoroutineStatus.Running; - } - - infoBox = CreateInfoFrame("You can now create a horizontal wall by clicking and dragging. When you're done, right click to stop creating walls.");*/ - - - - - yield return CoroutineStatus.Success; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index a380ef67e..da8a40fad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Tutorials private Reactor engineer_submarineReactor; // Variables - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character engineer; private int[] reactorLoads = new int[5] { 1500, 3000, 2000, 5000, 3500 }; private float reactorLoadChangeTime = 2f; @@ -75,27 +75,74 @@ namespace Barotrauma.Tutorials private Color engineer_reactorIconColor; private bool wiringActive = false; - public EngineerTutorial(XElement element) : base(element) - { + public EngineerTutorial() : base("tutorial.engineertraining".ToIdentifier(), + new Segment( + "Mechanic.Equipment".ToIdentifier(), + "Mechanic.EquipmentObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Engineer.Reactor".ToIdentifier(), + "Engineer.ReactorObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_reactor.webm", TextTag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80 }), + new Segment( + "Engineer.OperateReactor".ToIdentifier(), + "Engineer.OperateReactorObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Engineer.OperateReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_reactor.webm", TextTag = "Engineer.ReactorText".ToIdentifier(), Width = 700, Height = 80 }), + new Segment( + "Engineer.RepairJunctionBox".ToIdentifier(), + "Engineer.RepairJunctionBoxObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Engineer.RepairJunctionBoxText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Engineer.WireJunctionBoxes".ToIdentifier(), + "Engineer.WireJunctionBoxesObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Engineer.WireJunctionBoxesText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_wiring.webm", TextTag = "Engineer.WireJunctionBoxesText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Engineer.RepairElectricalRoom".ToIdentifier(), + "Engineer.RepairElectricalRoomObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Engineer.RepairElectricalRoomText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Engineer.PowerUpReactor".ToIdentifier(), + "Engineer.PowerUpReactorObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Engineer.PowerUpReactorText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center })) + { } + protected override CharacterInfo GetCharacterInfo() + { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 0), + new Skill("weapons".ToIdentifier(), 0), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 60), + new Skill("helm".ToIdentifier(), 0))); } - public override void Start() + protected override void Initialize() { - base.Start(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); engineer = Character.Controlled; - var toolbelt = FindOrGiveItem(engineer, "toolbelt"); + var toolbelt = FindOrGiveItem(engineer, "toolbelt".ToIdentifier()); toolbelt.Unequip(engineer); engineer.Inventory.RemoveItem(toolbelt); - var repairOrder = Order.GetPrefab("repairsystems"); + var repairOrder = OrderPrefab.Prefabs["repairsystems"]; engineer_repairIcon = repairOrder.SymbolSprite; engineer_repairIconColor = repairOrder.Color; - var reactorOrder = Order.GetPrefab("operatereactor"); + var reactorOrder = OrderPrefab.Prefabs["operatereactor"]; engineer_reactorIcon = reactorOrder.SymbolSprite; engineer_reactorIconColor = reactorOrder.Color; @@ -235,7 +282,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, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Deselect), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Retrieve equipment + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Retrieve equipment bool firstSlotRemoved = false; bool secondSlotRemoved = false; bool thirdSlotRemoved = false; @@ -276,7 +323,7 @@ namespace Barotrauma.Tutorials yield return null; } while (!engineer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); SetHighlight(engineer_equipmentCabinet.Item, false); SetHighlight(engineer_reactor.Item, true); SetDoorAccess(engineer_firstDoor, engineer_firstDoorLight, true); @@ -302,7 +349,7 @@ namespace Barotrauma.Tutorials if (IsSelectedItem(engineer_reactor.Item) && engineer_reactor.Item.OwnInventory.visualSlots != null) { engineer_reactor.AutoTemp = false; - HighlightInventorySlot(engineer.Inventory, "fuelrod", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(engineer.Inventory, "fuelrod".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); for (int i = 0; i < engineer_reactor.Item.OwnInventory.visualSlots.Length; i++) { @@ -311,7 +358,7 @@ namespace Barotrauma.Tutorials } yield return null; } while (engineer_reactor.AvailableFuel == 0); - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); TriggerTutorialSegment(2); CoroutineManager.StartCoroutine(ReactorOperatedProperly()); do @@ -354,7 +401,7 @@ namespace Barotrauma.Tutorials } while (wait > 0.0f); engineer.SelectedConstruction = null; engineer_reactor.CanBeSelected = false; - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); SetHighlight(engineer_reactor.Item, false); SetHighlight(engineer_brokenJunctionBox, true); SetDoorAccess(engineer_secondDoor, engineer_secondDoorLight, true); @@ -363,12 +410,12 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_secondDoor.IsOpen); yield return new WaitForSeconds(1f, false); Repairable repairableJunctionBoxComponent = engineer_brokenJunctionBox.GetComponent(); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Select)); // Repair the junction box + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Repair the junction box do { - if (!engineer.HasEquippedItem("screwdriver")) + if (!engineer.HasEquippedItem("screwdriver".ToIdentifier())) { - HighlightInventorySlot(engineer.Inventory, "screwdriver", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(engineer.Inventory, "screwdriver".ToIdentifier(), highlightColor, .5f, .5f, 0f); } else if (IsSelectedItem(engineer_brokenJunctionBox) && repairableJunctionBoxComponent.CurrentFixer == null) { @@ -380,7 +427,7 @@ namespace Barotrauma.Tutorials yield return null; } while (repairableJunctionBoxComponent.IsBelowRepairThreshold); // Wait until repaired SetHighlight(engineer_brokenJunctionBox, false); - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); SetDoorAccess(engineer_thirdDoor, engineer_thirdDoorLight, true); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { @@ -391,14 +438,14 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!engineer_thirdDoor.IsOpen); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.FaultyWiring"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Use), GameMain.Config.KeyBindText(InputType.Deselect)); // Connect the junction boxes + TriggerTutorialSegment(4, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)); // Connect the junction boxes do { CheckGhostWires(); HandleJunctionBoxWiringHighlights(); yield return null; } while (engineer_workingPump.Voltage < engineer_workingPump.MinVoltage); // Wait until connected all the way to the pump CheckGhostWires(); for (int i = 0; i < engineer_disconnectedJunctionBoxes.Length; i++) { SetHighlight(engineer_disconnectedJunctionBoxes[i].Item, false); } - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); do { yield return null; } while (engineer_workingPump.Item.CurrentHull.WaterPercentage > waterVolumeBeforeOpening); // Wait until drained wiringActive = false; SetDoorAccess(engineer_fourthDoor, engineer_fourthDoorLight, true); @@ -424,7 +471,7 @@ namespace Barotrauma.Tutorials // Remove highlights when each individual machine is repaired do { CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); yield return null; } while (repairableJunctionBoxComponent1.IsBelowRepairThreshold || repairableJunctionBoxComponent2.IsBelowRepairThreshold || repairableJunctionBoxComponent3.IsBelowRepairThreshold); CheckJunctionBoxHighlights(repairableJunctionBoxComponent1, repairableJunctionBoxComponent2, repairableJunctionBoxComponent3); - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(6); // Powerup reactor @@ -433,7 +480,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!IsReactorPoweredUp(engineer_submarineReactor)); // Wait until ~matches load engineer.RemoveActiveObjectiveEntity(engineer_submarineReactor.Item); SetHighlight(engineer_submarineReactor.Item, false); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Engineer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); @@ -516,9 +563,9 @@ namespace Barotrauma.Tutorials { Item selected = engineer.SelectedConstruction; - if (!engineer.HasEquippedItem("screwdriver")) + if (!engineer.HasEquippedItem("screwdriver".ToIdentifier())) { - HighlightInventorySlot(engineer.Inventory, "screwdriver", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(engineer.Inventory, "screwdriver".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } int selectedIndex = -1; @@ -537,9 +584,9 @@ namespace Barotrauma.Tutorials wiringActive = selectedIndex != -1; - if (!engineer.HasEquippedItem("wire")) + if (!engineer.HasEquippedItem("wire".ToIdentifier())) { - HighlightInventorySlotWithTag(engineer.Inventory, "wire", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlotWithTag(engineer.Inventory, "wire".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 11227ac0e..b26b2d32e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -69,33 +69,106 @@ namespace Barotrauma.Tutorials // Variables private const float waterVolumeBeforeOpening = 15f; - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character mechanic; private Sprite mechanic_repairIcon; private Color mechanic_repairIconColor; private Sprite mechanic_weldIcon; - public MechanicTutorial(XElement element) : base(element) - { + public MechanicTutorial() : base("tutorial.mechanictraining".ToIdentifier(), + new Segment( + "Mechanic.OpenDoor".ToIdentifier(), + "Mechanic.OpenDoorObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.OpenDoorText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.Equipment".ToIdentifier(), + "Mechanic.EquipmentObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_inventory.webm", TextTag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Welding".ToIdentifier(), + "Mechanic.WeldingObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.WeldingText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_equip.webm", TextTag = "Mechanic.WeldingText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Drain".ToIdentifier(), + "Mechanic.DrainObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.DrainText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.Deconstruct".ToIdentifier(), + "Mechanic.DeconstructObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.DeconstructText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_deconstruct.webm", TextTag = "Mechanic.DeconstructText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Fabricate".ToIdentifier(), + "Mechanic.FabricateObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Mechanic.FabricateText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_fabricate.webm", TextTag = "Mechanic.FabricateText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Mechanic.Extinguisher".ToIdentifier(), + "Mechanic.ExtinguisherObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.ExtinguisherText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.DropExtinguisher".ToIdentifier(), + "Mechanic.DropExtinguisherObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.DropExtinguisherText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.Diving".ToIdentifier(), + "Mechanic.DivingObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.DivingText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.RepairPump".ToIdentifier(), + "Mechanic.RepairPumpObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.RepairPumpText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Mechanic.RepairSubmarine".ToIdentifier(), + "Mechanic.RepairSubmarineObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.RepairSubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "tutorial.laddertitle".ToIdentifier(), + "tutorial.laddertitle".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "tutorial.ladderdescription".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) + { } + protected override CharacterInfo GetCharacterInfo() + { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 0), + new Skill("weapons".ToIdentifier(), 0), + new Skill("mechanical".ToIdentifier(), 50), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 0))); } - public override void Start() + protected override void Initialize() { - base.Start(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); mechanic = Character.Controlled; - var toolbelt = FindOrGiveItem(mechanic, "toolbelt"); + var toolbelt = FindOrGiveItem(mechanic, "toolbelt".ToIdentifier()); toolbelt.Unequip(mechanic); mechanic.Inventory.RemoveItem(toolbelt); - var crowbar = FindOrGiveItem(mechanic, "crowbar"); + var crowbar = FindOrGiveItem(mechanic, "crowbar".ToIdentifier()); crowbar.Unequip(mechanic); mechanic.Inventory.RemoveItem(crowbar); - var repairOrder = Order.GetPrefab("repairsystems"); + var repairOrder = OrderPrefab.Prefabs["repairsystems"]; mechanic_repairIcon = repairOrder.SymbolSprite; mechanic_repairIconColor = repairOrder.Color; mechanic_weldIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(1, 256, 127, 127), new Vector2(0.5f, 0.5f)); @@ -239,24 +312,25 @@ namespace Barotrauma.Tutorials } yield return new WaitForSeconds(2.5f, false); - mechanic_fabricator.RemoveFabricationRecipes(new List() { "extinguisher", "wrench", "weldingtool", "weldingfuel", "divingmask", "railgunshell", "nuclearshell", "uex", "harpoongun" }); + mechanic_fabricator.RemoveFabricationRecipes(allowedIdentifiers: + new[] { "extinguisher", "wrench", "weldingtool", "weldingfuel", "divingmask", "railgunshell", "nuclearshell", "uex", "harpoongun" }.ToIdentifiers()); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.WakeUp"), ChatMessageType.Radio, null); yield return new WaitForSeconds(2.5f, false); - TriggerTutorialSegment(0, GameMain.Config.KeyBindText(InputType.Up), GameMain.Config.KeyBindText(InputType.Left), GameMain.Config.KeyBindText(InputType.Down), GameMain.Config.KeyBindText(InputType.Right), GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Select)); // Open door objective + TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Up), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Left), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Down), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Right), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Open door objective yield return new WaitForSeconds(0.0f, false); SetDoorAccess(mechanic_firstDoor, mechanic_firstDoorLight, true); SetHighlight(mechanic_firstDoor.Item, true); do { yield return null; } while (!mechanic_firstDoor.IsOpen); SetHighlight(mechanic_firstDoor.Item, false); yield return new WaitForSeconds(1.5f, false); - RemoveCompletedObjective(segments[0]); + RemoveCompletedObjective(0); // Room 2 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, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Deselect), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Equipment & inventory objective + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Equipment & inventory objective SetHighlight(mechanic_equipmentCabinet.Item, true); bool firstSlotRemoved = false; bool secondSlotRemoved = false; @@ -290,35 +364,35 @@ namespace Barotrauma.Tutorials } yield return null; - } while (mechanic.Inventory.FindItemByIdentifier("divingmask") == null || mechanic.Inventory.FindItemByIdentifier("weldingtool") == null || mechanic.Inventory.FindItemByIdentifier("wrench") == null); // Wait until looted + } while (mechanic.Inventory.FindItemByIdentifier("divingmask".ToIdentifier()) == null || mechanic.Inventory.FindItemByIdentifier("weldingtool".ToIdentifier()) == null || mechanic.Inventory.FindItemByIdentifier("wrench".ToIdentifier()) == null); // Wait until looted SetHighlight(mechanic_equipmentCabinet.Item, false); yield return new WaitForSeconds(1.5f, false); - RemoveCompletedObjective(segments[1]); + RemoveCompletedObjective(1); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Breach"), ChatMessageType.Radio, null); // Room 3 do { yield return null; } while (!mechanic_weldingObjectiveSensor.MotionDetected); - TriggerTutorialSegment(2, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.ToggleInventory)); // Welding objective + TriggerTutorialSegment(2, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ToggleInventory)); // Welding objective do { - if (!mechanic.HasEquippedItem("divingmask")) + if (!mechanic.HasEquippedItem("divingmask".ToIdentifier())) { - HighlightInventorySlot(mechanic.Inventory, "divingmask", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "divingmask".ToIdentifier(), highlightColor, .5f, .5f, 0f); } - if (!mechanic.HasEquippedItem("weldingtool")) + if (!mechanic.HasEquippedItem("weldingtool".ToIdentifier())) { - HighlightInventorySlot(mechanic.Inventory, "weldingtool", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "weldingtool".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; - } while (!mechanic.HasEquippedItem("divingmask") || !mechanic.HasEquippedItem("weldingtool")); // Wait until equipped + } while (!mechanic.HasEquippedItem("divingmask".ToIdentifier()) || !mechanic.HasEquippedItem("weldingtool".ToIdentifier())); // Wait until equipped SetDoorAccess(mechanic_secondDoor, mechanic_secondDoorLight, true); mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_1, mechanic_weldIcon, mechanic_repairIconColor); do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_1)); // Highlight until repaired mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_1); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); yield return new WaitForSeconds(1f, false); - TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Select)); // Pump objective + TriggerTutorialSegment(3, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Select)); // Pump objective SetHighlight(mechanic_workingPump.Item, true); do { @@ -333,9 +407,9 @@ 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 - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); SetDoorAccess(mechanic_thirdDoor, mechanic_thirdDoorLight, true); - //TriggerTutorialSegment(11, GameMain.Config.KeyBind(InputType.Select), GameMain.Config.KeyBind(InputType.Up), GameMain.Config.KeyBind(InputType.Down), GameMain.Config.KeyBind(InputType.Select)); // Ladder objective + //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); //RemoveCompletedObjective(segments[11]); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.News"), ChatMessageType.Radio, null); @@ -362,24 +436,24 @@ namespace Barotrauma.Tutorials if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } } - if (mechanic.Inventory.FindItemByIdentifier("oxygentank") == null && mechanic.Inventory.FindItemByIdentifier("aluminium") == null) + if (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) == null && mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null) { for (int i = 0; i < mechanic_craftingCabinet.Capacity; i++) { Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); - if (item != null && item.prefab.Identifier == "oxygentank") + if (item != null && item.Prefab.Identifier == "oxygentank") { HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); } } } - if (mechanic.Inventory.FindItemByIdentifier("sodium") == null) + if (mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) == null) { for (int i = 0; i < mechanic_craftingCabinet.Inventory.Capacity; i++) { Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); - if (item != null && item.prefab.Identifier == "sodium") + if (item != null && item.Prefab.Identifier == "sodium") { HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); } @@ -387,12 +461,12 @@ namespace Barotrauma.Tutorials } } - if (!gotOxygenTank && (mechanic.Inventory.FindItemByIdentifier("oxygentank") != null || - mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null)) + if (!gotOxygenTank && (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null || + mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null)) { gotOxygenTank = true; } - if (!gotSodium && mechanic.Inventory.FindItemByIdentifier("sodium") != null) + if (!gotSodium && mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null) { gotSodium = true; } @@ -406,9 +480,9 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(mechanic_deconstructor.Item)) { - if (mechanic_deconstructor.OutputContainer.Inventory.FindItemByIdentifier("aluminium") != null) + if (mechanic_deconstructor.OutputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null) { - HighlightInventorySlot(mechanic_deconstructor.OutputContainer.Inventory, "aluminium", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic_deconstructor.OutputContainer.Inventory, "aluminium".ToIdentifier(), highlightColor, .5f, .5f, 0f); for (int i = 0; i < mechanic.Inventory.Capacity; i++) { @@ -417,16 +491,16 @@ namespace Barotrauma.Tutorials } else { - if (mechanic.Inventory.FindItemByIdentifier("oxygentank") != null && mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") == null) + if (mechanic.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null && mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) == null) { - HighlightInventorySlot(mechanic.Inventory, "oxygentank", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "oxygentank".ToIdentifier(), highlightColor, .5f, .5f, 0f); for (int i = 0; i < mechanic_deconstructor.InputContainer.Inventory.Capacity; i++) { HighlightInventorySlot(mechanic_deconstructor.InputContainer.Inventory, i, highlightColor, .5f, .5f, 0f); } } - if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null && !mechanic_deconstructor.IsActive) + if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank".ToIdentifier()) != null && !mechanic_deconstructor.IsActive) { if (mechanic_deconstructor.ActivateButton.FlashTimer <= 0) { @@ -437,11 +511,11 @@ namespace Barotrauma.Tutorials } yield return null; } while ( - mechanic.Inventory.FindItemByIdentifier("aluminium") == null && - mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium") == null); // Wait until aluminium obtained + mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null && + mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) == null); // Wait until aluminium obtained - SetHighlight(mechanic_deconstructor.Item, false); - RemoveCompletedObjective(segments[4]); + SetHighlight(mechanic_deconstructor.Item, false); + RemoveCompletedObjective(4); yield return new WaitForSeconds(1f, false); TriggerTutorialSegment(5); // Fabricate SetHighlight(mechanic_fabricator.Item, true); @@ -455,26 +529,26 @@ namespace Barotrauma.Tutorials } else { - if (mechanic_fabricator.OutputContainer.Inventory.FindItemByIdentifier("extinguisher") != null) + if (mechanic_fabricator.OutputContainer.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) != null) { - HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher".ToIdentifier(), highlightColor, .5f, .5f, 0f); /*for (int i = 0; i < mechanic.Inventory.Capacity; i++) { if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); }*/ } - else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium") != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium") != null && !mechanic_fabricator.IsActive) + else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null && !mechanic_fabricator.IsActive) { if (mechanic_fabricator.ActivateButton.FlashTimer <= 0) { mechanic_fabricator.ActivateButton.Flash(highlightColor, 1.5f, false); } } - else if (mechanic.Inventory.FindItemByIdentifier("aluminium") != null || mechanic.Inventory.FindItemByIdentifier("sodium") != null) + else if (mechanic.Inventory.FindItemByIdentifier("aluminium".ToIdentifier()) != null || mechanic.Inventory.FindItemByIdentifier("sodium".ToIdentifier()) != null) { - HighlightInventorySlot(mechanic.Inventory, "aluminium", highlightColor, .5f, .5f, 0f); - HighlightInventorySlot(mechanic.Inventory, "sodium", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "aluminium".ToIdentifier(), highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "sodium".ToIdentifier(), highlightColor, .5f, .5f, 0f); if (mechanic_fabricator.InputContainer.Inventory.GetItemAt(0) == null) { @@ -489,27 +563,27 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (mechanic.Inventory.FindItemByIdentifier("extinguisher") == null); // Wait until extinguisher is created - RemoveCompletedObjective(segments[5]); + } while (mechanic.Inventory.FindItemByIdentifier("extinguisher".ToIdentifier()) == null); // Wait until extinguisher is created + RemoveCompletedObjective(5); SetHighlight(mechanic_fabricator.Item, false); SetDoorAccess(mechanic_fourthDoor, mechanic_fourthDoorLight, true); // Room 5 do { yield return null; } while (!mechanic_fireSensor.MotionDetected); - TriggerTutorialSegment(6, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot)); // Using the extinguisher + TriggerTutorialSegment(6, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Using the extinguisher do { yield return null; } while (!mechanic_fire.Removed); // Wait until extinguished yield return new WaitForSeconds(3f, false); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); - if (mechanic.HasEquippedItem("extinguisher")) // do not trigger if dropped already + if (mechanic.HasEquippedItem("extinguisher".ToIdentifier())) // do not trigger if dropped already { TriggerTutorialSegment(7); do { - HighlightInventorySlot(mechanic.Inventory, "extinguisher", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "extinguisher".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); yield return null; - } while (mechanic.HasEquippedItem("extinguisher")); - RemoveCompletedObjective(segments[7]); + } while (mechanic.HasEquippedItem("extinguisher".ToIdentifier())); + RemoveCompletedObjective(7); } SetDoorAccess(mechanic_fifthDoor, mechanic_fifthDoorLight, true); @@ -531,9 +605,9 @@ namespace Barotrauma.Tutorials } } yield return null; - } while (!mechanic.HasEquippedItem("divingsuit", slotType: InvSlotType.OuterClothes)); + } while (!mechanic.HasEquippedItem("divingsuit".ToIdentifier(), slotType: InvSlotType.OuterClothes)); SetHighlight(mechanic_divingSuitContainer.Item, false); - RemoveCompletedObjective(segments[8]); + RemoveCompletedObjective(8); SetDoorAccess(tutorial_mechanicFinalDoor, tutorial_mechanicFinalDoorLight, true); // Room 7 @@ -542,7 +616,7 @@ namespace Barotrauma.Tutorials mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_2); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(9, GameMain.Config.KeyBindText(InputType.Use)); // Repairing machinery (pump) + TriggerTutorialSegment(9, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)); // Repairing machinery (pump) SetHighlight(mechanic_brokenPump.Item, true); mechanic_brokenPump.CanBeSelected = true; Repairable repairablePumpComponent = mechanic_brokenPump.Item.GetComponent(); @@ -552,9 +626,9 @@ namespace Barotrauma.Tutorials yield return null; if (repairablePumpComponent.IsBelowRepairThreshold) { - if (!mechanic.HasEquippedItem("wrench")) + if (!mechanic.HasEquippedItem("wrench".ToIdentifier())) { - HighlightInventorySlot(mechanic.Inventory, "wrench", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(mechanic.Inventory, "wrench".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } else if (IsSelectedItem(mechanic_brokenPump.Item) && repairablePumpComponent.CurrentFixer == null) { @@ -575,7 +649,7 @@ namespace Barotrauma.Tutorials } } } while (repairablePumpComponent.IsBelowRepairThreshold || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); - RemoveCompletedObjective(segments[9]); + RemoveCompletedObjective(9); SetHighlight(mechanic_brokenPump.Item, false); do { yield return null; } while (mechanic_brokenhull_2.WaterPercentage > waterVolumeBeforeOpening); SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, true); @@ -599,7 +673,7 @@ namespace Barotrauma.Tutorials // Remove highlights when each individual machine is repaired do { CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); yield return null; } while (repairablePumpComponent1.IsBelowRepairThreshold || repairablePumpComponent2.IsBelowRepairThreshold || repairableEngineComponent.IsBelowRepairThreshold); CheckHighlights(repairablePumpComponent1, repairablePumpComponent2, repairableEngineComponent); - RemoveCompletedObjective(segments[10]); + RemoveCompletedObjective(10); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Mechanic.Radio.Complete"), ChatMessageType.Radio, null); // END TUTORIAL diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 594e847cb..68d91df57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -72,62 +72,114 @@ namespace Barotrauma.Tutorials private PowerContainer officer_subSuperCapacitor_2; // Variables - private string radioSpeakerName; + private LocalizedString radioSpeakerName; private Character officer; private float superCapacitorRechargeRate = 10; private Sprite officer_gunIcon; private Color officer_gunIconColor; - public OfficerTutorial(XElement element) : base(element) + public OfficerTutorial() : base("tutorial.securityofficertraining".ToIdentifier(), + new Segment( + "Mechanic.Equipment".ToIdentifier(), + "Mechanic.EquipmentObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Mechanic.EquipmentText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.MeleeWeapon".ToIdentifier(), + "Officer.MeleeWeaponObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.MeleeWeaponText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.Crawler".ToIdentifier(), + "Officer.CrawlerObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.CrawlerText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.SomethingBig".ToIdentifier(), + "Officer.SomethingBigObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Officer.SomethingBigText".ToIdentifier(), Width = 700, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_loaders.webm", TextTag = "Officer.SomethingBigText".ToIdentifier(), Width = 700, Height = 80 }), + new Segment( + "Officer.Hammerhead".ToIdentifier(), + "Officer.HammerheadObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.HammerheadText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.RangedWeapon".ToIdentifier(), + "Officer.RangedWeaponObjective".ToIdentifier(), + TutorialContentType.ManualVideo, + textContent: new Segment.Text { Tag = "Officer.RangedWeaponText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }, + videoContent: new Segment.Video { File = "tutorial_ranged.webm", TextTag = "Officer.RangedWeaponText".ToIdentifier(), Width = 450, Height = 80 }), + new Segment( + "Officer.Mudraptor".ToIdentifier(), + "Officer.MudraptorObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.MudraptorText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center }), + new Segment( + "Officer.ArmSubmarine".ToIdentifier(), + "Officer.ArmSubmarineObjective".ToIdentifier(), + TutorialContentType.TextOnly, + textContent: new Segment.Text { Tag = "Officer.ArmSubmarineText".ToIdentifier(), Width = 450, Height = 80, Anchor = Anchor.Center })) + { } + + protected override CharacterInfo GetCharacterInfo() { + return new CharacterInfo( + CharacterPrefab.HumanSpeciesName, + jobOrJobPrefab: new Job( + JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + new Skill("medical".ToIdentifier(), 20), + new Skill("weapons".ToIdentifier(), 70), + new Skill("mechanical".ToIdentifier(), 20), + new Skill("electrical".ToIdentifier(), 20), + new Skill("helm".ToIdentifier(), 20))); } - public override void Start() + protected override void Initialize() { - base.Start(); - radioSpeakerName = TextManager.Get("Tutorial.Radio.Speaker"); officer = Character.Controlled; - var handcuffs = FindOrGiveItem(officer, "handcuffs"); + var handcuffs = FindOrGiveItem(officer, "handcuffs".ToIdentifier()); handcuffs.Unequip(officer); officer.Inventory.RemoveItem(handcuffs); - var stunbaton = FindOrGiveItem(officer, "stunbaton"); + var stunbaton = FindOrGiveItem(officer, "stunbaton".ToIdentifier()); stunbaton.Unequip(officer); officer.Inventory.RemoveItem(stunbaton); - var smg = FindOrGiveItem(officer, "smg"); + var smg = FindOrGiveItem(officer, "smg".ToIdentifier()); smg.Unequip(officer); officer.Inventory.RemoveItem(smg); - var divingknife = FindOrGiveItem(officer, "divingknife"); + var divingknife = FindOrGiveItem(officer, "divingknife".ToIdentifier()); divingknife.Unequip(officer); officer.Inventory.RemoveItem(divingknife); - var steroids = FindOrGiveItem(officer, "steroids"); + var steroids = FindOrGiveItem(officer, "steroids".ToIdentifier()); steroids.Unequip(officer); officer.Inventory.RemoveItem(steroids); var ballistichelmet = - officer.Inventory.FindItemByIdentifier("ballistichelmet1") ?? - officer.Inventory.FindItemByIdentifier("ballistichelmet2") ?? - FindOrGiveItem(officer, "ballistichelmet3"); + 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"); + var bodyarmor = FindOrGiveItem(officer, "bodyarmor".ToIdentifier()); bodyarmor.Unequip(officer); officer.Inventory.RemoveItem(bodyarmor); - var gunOrder = Order.GetPrefab("operateweapons"); + var gunOrder = OrderPrefab.Prefabs["operateweapons"]; officer_gunIcon = gunOrder.SymbolSprite; officer_gunIconColor = gunOrder.Color; - var bandage = FindOrGiveItem(officer, "antibleeding1"); + var bandage = FindOrGiveItem(officer, "antibleeding1".ToIdentifier()); bandage.Unequip(officer); officer.Inventory.RemoveItem(bandage); - FindOrGiveItem(officer, "antibleeding1"); + FindOrGiveItem(officer, "antibleeding1".ToIdentifier()); // Other tutorial items tutorial_mechanicFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoorlight")).GetComponent(); @@ -222,7 +274,7 @@ namespace Barotrauma.Tutorials do { yield return null; } while (!officer_equipmentObjectiveSensor.MotionDetected); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Equipment"), ChatMessageType.Radio, null); yield return new WaitForSeconds(3f, false); - //TriggerTutorialSegment(0, GameMain.Config.KeyBind(InputType.Select), GameMain.Config.KeyBind(InputType.Deselect)); // Retrieve equipment + //TriggerTutorialSegment(0, GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select], GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect]); // Retrieve equipment SetHighlight(officer_equipmentCabinet.Item, true); bool firstSlotRemoved = false; bool secondSlotRemoved = false; @@ -260,24 +312,24 @@ namespace Barotrauma.Tutorials //RemoveCompletedObjective(segments[0]); SetHighlight(officer_equipmentCabinet.Item, false); do { yield return null; } while (IsSelectedItem(officer_equipmentCabinet.Item)); - TriggerTutorialSegment(1, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot)); // Equip melee weapon & armor + TriggerTutorialSegment(1, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Equip melee weapon & armor do { - if (!officer.HasEquippedItem("stunbaton")) + if (!officer.HasEquippedItem("stunbaton".ToIdentifier())) { - HighlightInventorySlot(officer.Inventory, "stunbaton", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "stunbaton".ToIdentifier(), highlightColor, .5f, .5f, 0f); } - if (!officer.HasEquippedItem("bodyarmor")) + if (!officer.HasEquippedItem("bodyarmor".ToIdentifier())) { - HighlightInventorySlot(officer.Inventory, "bodyarmor", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "bodyarmor".ToIdentifier(), highlightColor, .5f, .5f, 0f); } - if (!officer.HasEquippedItem("ballistichelmet1")) + if (!officer.HasEquippedItem("ballistichelmet1".ToIdentifier())) { - HighlightInventorySlot(officer.Inventory, "ballistichelmet1", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "ballistichelmet1".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return new WaitForSeconds(1f, false); - } while (!officer.HasEquippedItem("stunbaton") || !officer.HasEquippedItem("bodyarmor") || !officer.HasEquippedItem("ballistichelmet1")); - RemoveCompletedObjective(segments[1]); + } while (!officer.HasEquippedItem("stunbaton".ToIdentifier()) || !officer.HasEquippedItem("bodyarmor".ToIdentifier()) || !officer.HasEquippedItem("ballistichelmet1".ToIdentifier())); + RemoveCompletedObjective(1); SetDoorAccess(officer_firstDoor, officer_firstDoorLight, true); // Room 3 @@ -285,7 +337,7 @@ namespace Barotrauma.Tutorials TriggerTutorialSegment(2); officer_crawler = SpawnMonster("crawler", officer_crawlerSpawnPos); do { yield return null; } while (!officer_crawler.IsDead); - RemoveCompletedObjective(segments[2]); + RemoveCompletedObjective(2); Heal(officer); yield return new WaitForSeconds(1f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.CrawlerDead"), ChatMessageType.Radio, null); @@ -305,7 +357,7 @@ namespace Barotrauma.Tutorials SetHighlight(officer_ammoShelf_2.Item, officer_coilgunLoader.Item.ExternalHighlight ); if (IsSelectedItem(officer_coilgunLoader.Item)) { - HighlightInventorySlot(officer.Inventory, "coilgunammobox", highlightColor, .5f, .5f, 0f); + HighlightInventorySlot(officer.Inventory, "coilgunammobox".ToIdentifier(), highlightColor, .5f, .5f, 0f); } yield return null; } while (officer_coilgunLoader.Inventory.GetItemAt(0) == null || officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate || officer_coilgunLoader.Inventory.GetItemAt(0).Condition == 0); @@ -313,9 +365,9 @@ namespace Barotrauma.Tutorials SetHighlight(officer_superCapacitor.Item, false); SetHighlight(officer_ammoShelf_1.Item, false); SetHighlight(officer_ammoShelf_2.Item, false); - RemoveCompletedObjective(segments[3]); + RemoveCompletedObjective(3); yield return new WaitForSeconds(2f, false); - TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect)); // Kill hammerhead + 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); officer_hammerhead.Params.AI.AvoidAbyss = false; officer_hammerhead.Params.AI.StayInAbyss = false; @@ -348,7 +400,7 @@ namespace Barotrauma.Tutorials while(!officer_hammerhead.IsDead); Heal(officer); SetHighlight(officer_coilgunPeriscope, false); - RemoveCompletedObjective(segments[4]); + RemoveCompletedObjective(4); 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); @@ -357,16 +409,16 @@ namespace Barotrauma.Tutorials //do { yield return null; } while (!officer_rangedWeaponSensor.MotionDetected); do { yield return null; } while (!officer_thirdDoor.IsOpen); yield return new WaitForSeconds(3f, false); - TriggerTutorialSegment(5, GameMain.Config.KeyBindText(InputType.Aim), GameMain.Config.KeyBindText(InputType.Shoot)); // Ranged weapons + TriggerTutorialSegment(5, GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)); // Ranged weapons SetHighlight(officer_rangedWeaponHolder.Item, true); do { yield return null; } while (!officer_rangedWeaponHolder.Inventory.IsEmpty()); // Wait until looted SetHighlight(officer_rangedWeaponHolder.Item, false); do { - HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(officer.Inventory, "shotgun".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); yield return null; - } while (!officer.HasEquippedItem("shotgun")); // Wait until equipped - ItemContainer shotGunChamber = officer.Inventory.FindItemByIdentifier("shotgun").GetComponent(); + } while (!officer.HasEquippedItem("shotgun".ToIdentifier())); // Wait until equipped + ItemContainer shotGunChamber = officer.Inventory.FindItemByIdentifier("shotgun".ToIdentifier()).GetComponent(); SetHighlight(officer_rangedWeaponCabinet.Item, true); do { @@ -392,13 +444,13 @@ namespace Barotrauma.Tutorials } } - if (officer.Inventory.FindItemByIdentifier("shotgunshell") != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("shotgunshell") != null)) + if (officer.Inventory.FindItemByIdentifier("shotgunshell".ToIdentifier()) != null || (IsSelectedItem(officer_rangedWeaponCabinet.Item) && officer_rangedWeaponCabinet.Inventory.FindItemByIdentifier("shotgunshell".ToIdentifier()) != null)) { - HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); + HighlightInventorySlot(officer.Inventory, "shotgun".ToIdentifier(), highlightColor, 0.5f, 0.5f, 0f); } yield return null; } while (!shotGunChamber.Inventory.IsFull(takeStacksIntoAccount: true)); // Wait until all six harpoons loaded - RemoveCompletedObjective(segments[5]); + RemoveCompletedObjective(5); SetHighlight(officer_rangedWeaponCabinet.Item, false); SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, true); @@ -408,7 +460,7 @@ namespace Barotrauma.Tutorials officer_mudraptor = SpawnMonster("mudraptor", officer_mudraptorSpawnPos); do { yield return null; } while (!officer_mudraptor.IsDead); Heal(officer); - RemoveCompletedObjective(segments[6]); + RemoveCompletedObjective(6); SetDoorAccess(tutorial_securityFinalDoor, tutorial_securityFinalDoorLight, true); // Submarine @@ -459,7 +511,7 @@ namespace Barotrauma.Tutorials officer.RemoveActiveObjectiveEntity(officer_subSuperCapacitor_2.Item); officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_1); officer.RemoveActiveObjectiveEntity(officer_subAmmoBox_2); - RemoveCompletedObjective(segments[7]); + RemoveCompletedObjective(7); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Complete"), ChatMessageType.Radio, null); yield return new WaitForSeconds(4f, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 4cb49e6fb..60369b132 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -8,18 +8,21 @@ using System.Xml.Linq; namespace Barotrauma.Tutorials { - class ScenarioTutorial : Tutorial + abstract class ScenarioTutorial : Tutorial { private CoroutineHandle tutorialCoroutine; private Character character; - private string spawnSub; - private SpawnType spawnPointType; - private string submarinePath; - private string startOutpostPath; - private string endOutpostPath; - private string levelSeed; - private string levelParams; + + private const string submarinePath = "Content/Tutorials/Dugong_Tutorial.sub"; + private const string startOutpostPath = "Content/Tutorials/TutorialOutpost.sub"; + //private const string endOutpostPath = ""; + + private const string levelSeed = "nLoZLLtza"; + private const string levelParams = "ColdCavernsTutorial"; + + //private const string spawnSub = "startoutpost"; + private const SpawnType spawnPointType = SpawnType.Human; private SubmarineInfo startOutpost = null; private SubmarineInfo endOutpost = null; @@ -31,34 +34,18 @@ namespace Barotrauma.Tutorials protected Color highlightColor = Color.OrangeRed; protected Color uiHighlightColor = new Color(150, 50, 0); protected Color buttonHighlightColor = new Color(255, 100, 0); - protected Color inaccessibleColor = GUI.Style.Red; - protected Color accessibleColor = GUI.Style.Green; + protected Color inaccessibleColor = GUIStyle.Red; + protected Color accessibleColor = GUIStyle.Green; - public ScenarioTutorial(XElement element) : base(element) - { - submarinePath = element.GetAttributeString("submarinepath", ""); - startOutpostPath = element.GetAttributeString("startoutpostpath", ""); - endOutpostPath = element.GetAttributeString("endoutpostpath", ""); + protected ScenarioTutorial(Identifier identifier, params Segment[] segments) : base(identifier, segments) { } - levelSeed = element.GetAttributeString("levelseed", "tuto"); - levelParams = element.GetAttributeString("levelparams", ""); - - spawnSub = element.GetAttributeString("spawnsub", ""); - Enum.TryParse(element.GetAttributeString("spawnpointtype", "Human"), true, out spawnPointType); - } - - public override void Initialize() - { - base.Initialize(); - currentTutorialCompleted = false; - GameMain.Instance.ShowLoading(Loading()); - } - - private IEnumerable Loading() + protected abstract void Initialize(); + + protected override IEnumerable Loading() { SubmarineInfo subInfo = new SubmarineInfo(submarinePath); - LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier.Equals(levelParams, StringComparison.OrdinalIgnoreCase)); + LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier == levelParams); yield return CoroutineStatus.Running; @@ -68,18 +55,18 @@ namespace Barotrauma.Tutorials if (generationParams != null) { Biome biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? - LevelGenerationParams.GetBiomes().First(); + Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ?? + Biome.Prefabs.First(); if (!string.IsNullOrEmpty(startOutpostPath)) { startOutpost = new SubmarineInfo(startOutpostPath); } - if (!string.IsNullOrEmpty(endOutpostPath)) + /*if (!string.IsNullOrEmpty(endOutpostPath)) { endOutpost = new SubmarineInfo(endOutpostPath); - } + }*/ LevelData tutorialLevel = new LevelData(levelSeed, 0, 0, generationParams, biome); GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost, endOutpost: endOutpost); @@ -93,12 +80,6 @@ namespace Barotrauma.Tutorials GameMain.GameSession.EventManager.Enabled = false; GameMain.GameScreen.Select(); - yield return CoroutineStatus.Success; - } - - public override void Start() - { - base.Start(); Submarine.MainSub.GodMode = true; foreach (Structure wall in Structure.WallList) @@ -109,16 +90,15 @@ namespace Barotrauma.Tutorials } } - CharacterInfo charInfo = configElement.Element("Character") == null ? - new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: JobPrefab.Get("engineer")) : - new CharacterInfo(configElement.Element("Character")); + CharacterInfo charInfo = GetCharacterInfo(); WayPoint wayPoint = GetSpawnPoint(charInfo); if (wayPoint == null) { DebugConsole.ThrowError("A waypoint with the spawntype \"" + spawnPointType + "\" is required for the tutorial event"); - return; + yield return CoroutineStatus.Failure; + yield break; } character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); @@ -126,11 +106,12 @@ namespace Barotrauma.Tutorials Character.Controlled = character; character.GiveJobItems(null); - var idCard = character.Inventory.FindItemByIdentifier("idcard"); + var idCard = character.Inventory.FindItemByIdentifier("idcard".ToIdentifier()); if (idCard == null) { DebugConsole.ThrowError("Item prefab \"ID Card\" not found!"); - return; + yield return CoroutineStatus.Failure; + yield break; } idCard.AddTag("com"); idCard.AddTag("eng"); @@ -145,8 +126,14 @@ namespace Barotrauma.Tutorials } tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); + + Initialize(); + + yield return CoroutineStatus.Success; } + protected abstract CharacterInfo GetCharacterInfo(); + public override void AddToGUIUpdateList() { if (!currentTutorialCompleted) @@ -157,7 +144,7 @@ namespace Barotrauma.Tutorials private WayPoint GetSpawnPoint(CharacterInfo charInfo) { - Submarine spawnSub = null; + /*Submarine spawnSub = null; if (this.spawnSub != string.Empty) { @@ -175,15 +162,15 @@ namespace Barotrauma.Tutorials spawnSub = Submarine.MainSub; break; } - } - + }*/ + Submarine spawnSub = Level.Loaded.StartOutpost; return WayPoint.GetRandom(spawnPointType, charInfo.Job?.Prefab, spawnSub); } protected bool HasOrder(Character character, string identifier, string option = null) { var currentOrderInfo = character.GetCurrentOrderWithTopPriority(); - if (currentOrderInfo?.Order?.Identifier == identifier) + if (currentOrderInfo?.Identifier == identifier) { if (option == null) { @@ -191,7 +178,7 @@ namespace Barotrauma.Tutorials } else { - return currentOrderInfo?.OrderOption == option; + return currentOrderInfo?.Option == option; } } @@ -267,7 +254,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(3.0f); - var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); messageBox.Buttons[0].OnClicked += Restart; messageBox.Buttons[0].OnClicked += messageBox.Close; @@ -303,7 +290,7 @@ namespace Barotrauma.Tutorials character.SetStun(0.0f, true); } - protected Item FindOrGiveItem(Character character, string identifier) + protected Item FindOrGiveItem(Character character, Identifier identifier) { var item = character.Inventory.FindItemByIdentifier(identifier); if (item != null && !item.Removed) { return item; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 87f3c404e..42795e0bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -7,189 +7,128 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma.Tutorials { + enum TutorialContentType { None = 0, Video = 1, ManualVideo = 2, TextOnly = 3 }; + + /// + /// If you're seeing this and are currently working on improving the tutorials, consider + /// deleting this class and all that derive from it, and starting from scratch. + /// abstract class Tutorial { - #region Tutorial variables - public static bool Initialized = false; - public static bool ContentRunning = false; - public static List Tutorials; + #region Constants + public const string PlayableContentPath = "Content/Tutorials/TutorialVideos/"; + #endregion + + #region Tutorial variables + public static ImmutableHashSet Types; + static Tutorial() + { + Types = ReflectionUtils.GetDerivedNonAbstract() + .ToImmutableHashSet(); + } + + public readonly Identifier Identifier; + + public LocalizedString DisplayName { get; } + + public bool ContentRunning { get; protected set; } - protected bool started = false; protected GUIComponent infoBox; private Action infoBoxClosedCallback; - protected XElement configElement; protected VideoPlayer videoPlayer; - protected enum TutorialContentTypes { None = 0, Video = 1, ManualVideo = 2, TextOnly = 3 }; - protected string playableContentPath; protected Point screenResolution; protected WindowMode windowMode; protected float prevUIScale; private GUIFrame holderFrame, objectiveFrame; - private List activeObjectives = new List(); - private string objectiveTranslated; + private readonly List activeObjectives; + private readonly LocalizedString objectiveTranslated; - protected TutorialSegment activeContentSegment; - protected List segments; + protected readonly ImmutableArray segments; + protected Index activeContentSegmentIndex; + protected Segment activeContentSegment => segments[activeContentSegmentIndex]; - protected class TutorialSegment + protected class Segment { - public string Id; - public string Objective; - public TutorialContentTypes ContentType; - public XElement TextContent; - public XElement VideoContent; + public struct Text + { + public Identifier Tag; + public int Width; + public int Height; + public Anchor Anchor; + } + + public struct Video + { + public string File; + public Identifier TextTag; + public int Width; + public int Height; + } + public bool IsTriggered; public GUIButton ReplayButton; public GUITextBlock LinkedTitle, LinkedText; public object[] Args; + public LocalizedString Objective; - public TutorialSegment(XElement config) + public readonly Identifier Id; + public readonly Text? TextContent; + public readonly Video? VideoContent; + public readonly TutorialContentType ContentType; + + public Segment(Identifier id, Identifier objectiveTextTag, TutorialContentType contentType, Text? textContent = null, Video? videoContent = null) { - Id = config.GetAttributeString("id", "Missing ID"); - Objective = TextManager.Get(config.GetAttributeString("objective", string.Empty), true); - Enum.TryParse(config.GetAttributeString("contenttype", "None"), true, out ContentType); - IsTriggered = config.GetAttributeBool("istriggered", false); + Id = id; + Objective = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); + ContentType = contentType; + TextContent = textContent; + VideoContent = videoContent; - switch (ContentType) - { - case TutorialContentTypes.None: - break; - case TutorialContentTypes.Video: - case TutorialContentTypes.ManualVideo: - VideoContent = config.Element("Video"); - TextContent = config.Element("Text"); - break; - case TutorialContentTypes.TextOnly: - TextContent = config.Element("Text"); - break; - } + IsTriggered = false; } } - public string Identifier - { - get; - protected set; - } - - public string DisplayName - { - get; - protected set; - } - private bool completed; public bool Completed { get { return completed; } protected set { - if (completed == value) return; + if (completed == value) { return; } completed = value; - GameMain.Config.SaveNewPlayerConfig(); + if (value) + { + CompletedTutorials.Instance.Add(Identifier); + } + GameSettings.SaveCurrentConfig(); } } #endregion #region Tutorial Controls - public static void Init() + protected Tutorial(Identifier identifier, params Segment[] segments) { - Tutorials = new List(); - foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Tutorials)) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) - { - Tutorial newTutorial = Load(element); - if (newTutorial != null) Tutorials.Add(newTutorial); - } - } - } - - private static Tutorial Load(XElement element) - { - Type t; - string type = element.Name.ToString().ToLowerInvariant(); - try - { - // Get the type of a specified class. - t = Type.GetType("Barotrauma.Tutorials." + type + "", false, true); - if (t == null) - { - DebugConsole.ThrowError("Could not find tutorial type \"" + type + "\""); - return null; - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Could not find tutorial type \"" + type + "\"", e); - return null; - } - - ConstructorInfo constructor; - try - { - if (!t.IsSubclassOf(typeof(Tutorial))) return null; - constructor = t.GetConstructor(new Type[] { typeof(XElement) }); - if (constructor == null) - { - DebugConsole.ThrowError("Could not find the constructor of tutorial type \"" + type + "\""); - return null; - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Could not find the constructor of tutorial type \"" + type + "\"", e); - return null; - } - Tutorial tutorial = null; - try - { - object component = constructor.Invoke(new object[] { element }); - tutorial = (Tutorial)component; - } - catch (TargetInvocationException e) - { - DebugConsole.ThrowError("Error while loading tutorial of the type " + t + ".", e.InnerException); - } - - return tutorial; - } - - public Tutorial(XElement element) - { - configElement = element; - Identifier = element.GetAttributeString("identifier", "unknown"); + Identifier = identifier; + this.segments = segments.ToImmutableArray(); DisplayName = TextManager.Get(Identifier); - completed = GameMain.Config.CompletedTutorialNames.Contains(Identifier); - playableContentPath = element.GetAttributeString("playablecontentpath", ""); - - segments = new List(); - - foreach (var segment in element.Elements("Segment")) - { - segments.Add(new TutorialSegment(segment)); - } - } - - public virtual void Initialize() - { - if (Initialized) return; - Initialized = true; - videoPlayer = new VideoPlayer(); - } - - public virtual void Start() - { - activeObjectives.Clear(); + activeObjectives = new List(); objectiveTranslated = TextManager.Get("Tutorial.Objective"); + } + + protected abstract IEnumerable Loading(); + + public void Start() + { + videoPlayer = new VideoPlayer(); + GameMain.Instance.ShowLoading(Loading()); + + activeObjectives.Clear(); CreateObjectiveFrame(); // Setup doors: Clear all requirements, unless the door is setup as locked. @@ -208,7 +147,7 @@ namespace Barotrauma.Tutorials public virtual void AddToGUIUpdateList() { - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameMain.Config.WindowMode != windowMode) + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode) { CreateObjectiveFrame(); } @@ -255,74 +194,69 @@ namespace Barotrauma.Tutorials protected bool Restart(GUIButton button, object obj) { GUI.PreventPauseMenuToggle = false; - TutorialMode.StartTutorial(this); return true; } - protected virtual void TriggerTutorialSegment(int index, params object[] args) + protected virtual void TriggerTutorialSegment(Index index, params object[] args) { Inventory.DraggingItems.Clear(); ContentRunning = true; - activeContentSegment = segments[index]; + activeContentSegmentIndex = index; segments[index].Args = args; - string tutorialText = TextManager.GetFormatted(activeContentSegment.TextContent.GetAttributeString("tag", ""), true, args); + LocalizedString tutorialText = TextManager.GetFormatted(segments[index].TextContent.Value.Tag, args); tutorialText = TextManager.ParseInputTypes(tutorialText); - string objectiveText = string.Empty; + LocalizedString objectiveText = string.Empty; - if (!string.IsNullOrEmpty(activeContentSegment.Objective)) + if (!segments[index].Objective.IsNullOrEmpty()) { if (args.Length == 0) { - objectiveText = activeContentSegment.Objective; + objectiveText = segments[index].Objective; } else { - objectiveText = string.Format(activeContentSegment.Objective, args); + objectiveText = TextManager.GetFormatted(segments[index].Objective, args); } objectiveText = TextManager.ParseInputTypes(objectiveText); - activeContentSegment.Objective = objectiveText; + segments[index].Objective = objectiveText; } else { - activeContentSegment.IsTriggered = true; // Complete at this stage only if no related objective + segments[index].IsTriggered = true; // Complete at this stage only if no related objective } - switch (activeContentSegment.ContentType) + switch (segments[index].ContentType) { - case TutorialContentTypes.None: + case TutorialContentType.None: break; - case TutorialContentTypes.Video: + case TutorialContentType.Video: infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.GetAttributeInt("width", 300), - activeContentSegment.TextContent.GetAttributeInt("height", 80), - activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, () => LoadVideo(activeContentSegment)); + activeContentSegment.TextContent.Value.Width, + activeContentSegment.TextContent.Value.Height, + activeContentSegment.TextContent.Value.Anchor, true, () => LoadVideo(activeContentSegment)); break; - case TutorialContentTypes.ManualVideo: + case TutorialContentType.ManualVideo: infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.GetAttributeInt("width", 300), - activeContentSegment.TextContent.GetAttributeInt("height", 80), - activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, StopCurrentContentSegment, () => LoadVideo(activeContentSegment)); + activeContentSegment.TextContent.Value.Width, + activeContentSegment.TextContent.Value.Height, + activeContentSegment.TextContent.Value.Anchor, true, StopCurrentContentSegment, () => LoadVideo(activeContentSegment)); break; - case TutorialContentTypes.TextOnly: + case TutorialContentType.TextOnly: infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, - activeContentSegment.TextContent.GetAttributeInt("width", 300), - activeContentSegment.TextContent.GetAttributeInt("height", 80), - activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, StopCurrentContentSegment); + activeContentSegment.TextContent.Value.Width, + activeContentSegment.TextContent.Value.Height, + activeContentSegment.TextContent.Value.Anchor, true, StopCurrentContentSegment); break; } } public virtual void Stop() { - started = ContentRunning = Initialized = false; + ContentRunning = false; infoBox = null; - if (videoPlayer != null) - { - videoPlayer.Remove(); - videoPlayer = null; - } + videoPlayer.Remove(); } #endregion @@ -334,50 +268,51 @@ namespace Barotrauma.Tutorials for (int i = 0; i < activeObjectives.Count; i++) { - CreateObjectiveGUI(activeObjectives[i], i, activeObjectives[i].ContentType); + CreateObjectiveGUI(activeObjectives[i], i, segments[activeObjectives[i]].ContentType); } screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - windowMode = GameMain.Config.WindowMode; + windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode; prevUIScale = GUI.Scale; } protected void StopCurrentContentSegment() { - if (!string.IsNullOrEmpty(activeContentSegment.Objective)) + if (!activeContentSegment.Objective.IsNullOrEmpty()) { - AddNewObjective(activeContentSegment, activeContentSegment.ContentType); + AddNewObjective(activeContentSegmentIndex, activeContentSegment.ContentType); } - activeContentSegment = null; ContentRunning = false; + activeContentSegmentIndex = Index.End; } - protected virtual void CheckActiveObjectives(TutorialSegment objective, float deltaTime) + protected virtual void CheckActiveObjectives(Index objective, float deltaTime) { } - protected bool HasObjective(TutorialSegment segment) + protected bool HasObjective(Index segment) { return activeObjectives.Contains(segment); } - protected void AddNewObjective(TutorialSegment segment, TutorialContentTypes type) + protected void AddNewObjective(Index segment, TutorialContentType type) { activeObjectives.Add(segment); CreateObjectiveGUI(segment, activeObjectives.Count - 1, type); } - private void CreateObjectiveGUI(TutorialSegment segment, int index, TutorialContentTypes type) + private void CreateObjectiveGUI(Index segmentIndex, int index, TutorialContentType type) { - string objectiveText = TextManager.ParseInputTypes(segment.Objective); - Point replayButtonSize = new Point((int)(GUI.LargeFont.MeasureString(objectiveText).X), (int)(GUI.LargeFont.MeasureString(objectiveText).Y * 1.45f)); + var segment = segments[segmentIndex]; + LocalizedString objectiveText = TextManager.ParseInputTypes(segment.Objective); + Point replayButtonSize = new Point((int)(GUIStyle.LargeFont.MeasureString(objectiveText).X), (int)(GUIStyle.LargeFont.MeasureString(objectiveText).Y * 1.45f)); segment.ReplayButton = new GUIButton(new RectTransform(replayButtonSize, objectiveFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft) { AbsoluteOffset = new Point(0, (replayButtonSize.Y + (int)(20f * GUI.Scale)) * index) }, style: null); segment.ReplayButton.OnClicked += (GUIButton btn, object userdata) => { - if (type == TutorialContentTypes.Video) + if (type == TutorialContentType.Video) { ReplaySegmentVideo(segment); } @@ -388,23 +323,23 @@ namespace Barotrauma.Tutorials return true; }; - string objectiveTitleText = TextManager.ParseInputTypes(objectiveTranslated); - int yOffset = (int)((GUI.SubHeadingFont.MeasureString(objectiveTitleText).Y + 5)); - segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point((int)GUI.SubHeadingFont.MeasureString(objectiveTitleText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.BottomLeft) /*{ AbsoluteOffset = new Point((int)(-10 * GUI.Scale), 0) }*/, - objectiveTitleText, textColor: Color.White, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + LocalizedString objectiveTitleText = TextManager.ParseInputTypes(objectiveTranslated); + int yOffset = (int)((GUIStyle.SubHeadingFont.MeasureString(objectiveTitleText).Y + 5)); + segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point((int)GUIStyle.SubHeadingFont.MeasureString(objectiveTitleText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.BottomLeft) /*{ AbsoluteOffset = new Point((int)(-10 * GUI.Scale), 0) }*/, + objectiveTitleText, textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - segment.LinkedText = new GUITextBlock(new RectTransform(new Point((int)GUI.LargeFont.MeasureString(objectiveText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.TopLeft) /*{ AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }*/, - objectiveText, textColor: new Color(4, 180, 108), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); + segment.LinkedText = new GUITextBlock(new RectTransform(new Point((int)GUIStyle.LargeFont.MeasureString(objectiveText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterLeft, Pivot.TopLeft) /*{ AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }*/, + objectiveText, textColor: new Color(4, 180, 108), font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft); segment.LinkedTitle.Color = segment.LinkedTitle.HoverColor = segment.LinkedTitle.PressedColor = segment.LinkedTitle.SelectedColor = Color.Transparent; segment.LinkedText.Color = segment.LinkedText.HoverColor = segment.LinkedText.PressedColor = segment.LinkedText.SelectedColor = Color.Transparent; segment.ReplayButton.Color = segment.ReplayButton.HoverColor = segment.ReplayButton.PressedColor = segment.ReplayButton.SelectedColor = Color.Transparent; } - private void ReplaySegmentVideo(TutorialSegment segment) + private void ReplaySegmentVideo(Segment segment) { if (ContentRunning) return; Inventory.DraggingItems.Clear(); @@ -413,30 +348,31 @@ namespace Barotrauma.Tutorials //videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, callback: () => ContentRunning = false); } - private void ShowSegmentText(TutorialSegment segment) + private void ShowSegmentText(Segment segment) { if (ContentRunning) return; Inventory.DraggingItems.Clear(); ContentRunning = true; - string tutorialText = TextManager.GetFormatted(segment.TextContent.GetAttributeString("tag", ""), true, segment.Args); + LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Value.Tag, segment.Args); Action videoAction = null; - if (segment.ContentType != TutorialContentTypes.TextOnly) + if (segment.ContentType != TutorialContentType.TextOnly) { videoAction = () => LoadVideo(segment); } infoBox = CreateInfoFrame(TextManager.Get(segment.Id), tutorialText, - segment.TextContent.GetAttributeInt("width", 300), - segment.TextContent.GetAttributeInt("height", 80), - segment.TextContent.GetAttributeString("anchor", "Center"), true, () => ContentRunning = false, videoAction); + segment.TextContent.Value.Width, + segment.TextContent.Value.Height, + segment.TextContent.Value.Anchor, true, () => ContentRunning = false, videoAction); } - protected void RemoveCompletedObjective(TutorialSegment segment) + protected void RemoveCompletedObjective(Index segmentIndex) { - if (!HasObjective(segment)) return; + if (!HasObjective(segmentIndex)) return; + var segment = segments[segmentIndex]; segment.IsTriggered = true; segment.ReplayButton.OnClicked = null; @@ -467,18 +403,20 @@ namespace Barotrauma.Tutorials GUIImage stroke = new GUIImage(rectTB, "Stroke"); stroke.Color = stroke.SelectedColor = stroke.HoverColor = stroke.PressedColor = color; - CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segment)); + CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segmentIndex)); } - private IEnumerable WaitForObjectiveEnd(TutorialSegment objective) + private IEnumerable WaitForObjectiveEnd(Index objectiveIndex) { + var objective = segments[objectiveIndex]; yield return new WaitForSeconds(2.0f); objectiveFrame.RemoveChild(objective.ReplayButton); - activeObjectives.Remove(objective); + activeObjectives.Remove(objectiveIndex); for (int i = 0; i < activeObjectives.Count; i++) { - activeObjectives[i].ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjectives[i].ReplayButton.Rect.Height + 20) * i); + var activeObjective = segments[activeObjectives[i]]; + activeObjective.ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjective.ReplayButton.Rect.Height + 20) * i); } } @@ -492,32 +430,25 @@ namespace Barotrauma.Tutorials return true; } - protected GUIComponent CreateInfoFrame(string title, string text, int width = 300, int height = 80, string anchorStr = "", bool hasButton = false, Action callback = null, Action showVideo = null) + protected GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action callback = null, Action showVideo = null) { if (hasButton) height += 60; - Anchor anchor = Anchor.TopRight; - - if (anchorStr != string.Empty) - { - Enum.TryParse(anchorStr, out anchor); - } - width = (int)(width * GUI.Scale); height = (int)(height * GUI.Scale); - string wrappedText = ToolBox.WrapText(text, width, GUI.Font); - height += (int)GUI.Font.MeasureString(wrappedText).Y; + LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font); + height += (int)GUIStyle.Font.MeasureString(wrappedText).Y; if (title.Length > 0) { - height += (int)GUI.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); + height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); } var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); - infoBlock.Flash(GUI.Style.Green); + infoBlock.Flash(GUIStyle.Green); var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center)) { @@ -528,20 +459,12 @@ namespace Barotrauma.Tutorials if (title.Length > 0) { var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), - title, font: GUI.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); + title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); titleBlock.RectTransform.IsFixedSize = true; } - List richTextData = RichTextData.GetRichTextData(" " + text, out text); - GUITextBlock textBlock; - if (richTextData == null) - { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); - } - else - { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), richTextData, text, wrap: true); - } + text = RichString.Rich(text); + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); textBlock.RectTransform.IsFixedSize = true; infoBoxClosedCallback = callback; @@ -589,22 +512,26 @@ namespace Barotrauma.Tutorials #endregion #region Video - protected void LoadVideo(TutorialSegment segment) + protected void LoadVideo(Segment segment) { if (videoPlayer == null) videoPlayer = new VideoPlayer(); - if (segment.ContentType != TutorialContentTypes.ManualVideo) + if (segment.ContentType != TutorialContentType.ManualVideo) { - videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, segment.Objective, StopCurrentContentSegment); + videoPlayer.LoadContent( + PlayableContentPath, + new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), + new VideoPlayer.TextSettings(segment.VideoContent.Value.TextTag, segment.VideoContent.Value.Width), + segment.Id, true, segment.Objective, StopCurrentContentSegment); } else { - videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), null, segment.Id, true, string.Empty, null); + videoPlayer.LoadContent(PlayableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), null, segment.Id, true, string.Empty, null); } } #endregion #region Highlights - protected void HighlightInventorySlot(Inventory inventory, string identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + protected void HighlightInventorySlot(Inventory inventory, Identifier identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { if (inventory.visualSlots == null) { return; } for (int i = 0; i < inventory.Capacity; i++) @@ -616,7 +543,7 @@ namespace Barotrauma.Tutorials } } - protected void HighlightInventorySlotWithTag(Inventory inventory, string tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + protected void HighlightInventorySlotWithTag(Inventory inventory, Identifier tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { if (inventory.visualSlots == null) { return; } for (int i = 0; i < inventory.Capacity; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs index d9ec85f14..2b904f82f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -5,11 +5,6 @@ namespace Barotrauma class TutorialMode : GameMode { public Tutorial Tutorial; - - public static void StartTutorial(Tutorial tutorial) - { - tutorial.Initialize(); - } public TutorialMode(GameModePreset preset) : base(preset) @@ -20,7 +15,6 @@ namespace Barotrauma { base.Start(); GameMain.GameSession.CrewManager = new CrewManager(true); - Tutorial.Start(); foreach (Item item in Item.ItemList) { //don't consider the items to belong in the outpost to prevent the stealing icon from showing diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 113f7e5c3..14daa8a4a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -64,12 +64,12 @@ namespace Barotrauma GameMain.Instance.ResolutionChanged -= CreateTopLeftButtons; }; int buttonHeight = GUI.IntScale(40); - Vector2 buttonSpriteSize = GUI.Style.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; + Vector2 buttonSpriteSize = GUIStyle.GetComponentStyle("CrewListToggleButton").GetDefaultSprite().size; int buttonWidth = (int)((buttonHeight / buttonSpriteSize.Y) * buttonSpriteSize.X); Point buttonSize = new Point(buttonWidth, buttonHeight); crewListButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "CrewListToggleButton") { - ToolTip = TextManager.GetWithVariable("hudbutton.crewlist", "[key]", GameMain.Config.KeyBindText(InputType.CrewOrders)), + ToolTip = TextManager.GetWithVariable("hudbutton.crewlist", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.CrewOrders)), OnClicked = (GUIButton btn, object userdata) => { if (CrewManager == null) { return false; } @@ -79,7 +79,7 @@ namespace Barotrauma }; commandButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "CommandButton") { - ToolTip = TextManager.GetWithVariable("hudbutton.commandinterface", "[key]", GameMain.Config.KeyBindText(InputType.Command)), + ToolTip = TextManager.GetWithVariable("hudbutton.commandinterface", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)), OnClicked = (button, userData) => { if (CrewManager == null) { return false; } @@ -89,7 +89,7 @@ namespace Barotrauma }; tabMenuButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "TabMenuButton") { - ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameMain.Config.KeyBindText(InputType.InfoTab)), + ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.InfoTab)), OnClicked = (button, userData) => ToggleTabMenu() }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs index 40cff56c3..a7ee9cc4c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/HintManager.cs @@ -13,14 +13,14 @@ namespace Barotrauma { private const string HintManagerFile = "hintmanager.xml"; - public static bool Enabled => GameMain.Config != null && !GameMain.Config.DisableInGameHints; - private static HashSet HintIdentifiers { get; set; } - private static Dictionary> HintTags { get; } = new Dictionary>(); - private static Dictionary HintOrders { get; } = new Dictionary(); + public static bool Enabled => !GameSettings.CurrentConfig.DisableInGameHints; + private static HashSet HintIdentifiers { get; set; } + private static Dictionary> HintTags { get; } = new Dictionary>(); + private static Dictionary HintOrders { get; } = new Dictionary(); /// /// Hints that have already been shown this round and shouldn't be shown shown again until the next round /// - private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); + private static HashSet HintsIgnoredThisRound { get; } = new HashSet(); private static GUIMessageBox ActiveHintMessageBox { get; set; } private static Action OnUpdate { get; set; } private static double TimeStoppedInteracting { get; set; } @@ -43,10 +43,10 @@ namespace Barotrauma var doc = XMLExtensions.TryLoadXml(HintManagerFile); if (doc?.Root != null) { - HintIdentifiers = new HashSet(); + HintIdentifiers = new HashSet(); foreach (var element in doc.Root.Elements()) { - GetHintsRecursive(element, element.Name.ToString()); + GetHintsRecursive(element, element.NameAsIdentifier()); } } else @@ -59,18 +59,18 @@ namespace Barotrauma DebugConsole.ThrowError($"File \"{HintManagerFile}\" is missing - cannot initialize the HintManager!"); } - static void GetHintsRecursive(XElement element, string identifier) + static void GetHintsRecursive(XElement element, Identifier identifier) { if (!element.HasElements) { HintIdentifiers.Add(identifier); - if (element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true) is string[] tags) + if (element.GetAttributeIdentifierArray("tags", null) is Identifier[] tags) { HintTags.TryAdd(identifier, tags.ToHashSet()); } - if (element.GetAttributeString("order", null) is string orderIdentifier && !string.IsNullOrEmpty(orderIdentifier)) + if (element.GetAttributeIdentifier("order", Identifier.Empty) is Identifier orderIdentifier && orderIdentifier != Identifier.Empty) { - string orderOption = element.GetAttributeString("orderoption", ""); + Identifier orderOption = element.GetAttributeIdentifier("orderoption", Identifier.Empty); HintOrders.Add(identifier, (orderIdentifier, orderOption)); } return; @@ -82,14 +82,14 @@ namespace Barotrauma } foreach (var childElement in element.Elements()) { - GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}"); + GetHintsRecursive(childElement, $"{identifier}.{childElement.Name}".ToIdentifier()); } } } public static void Update() { - if (HintIdentifiers == null || GameMain.Config.DisableInGameHints) { return; } + if (HintIdentifiers == null || GameSettings.CurrentConfig.DisableInGameHints) { return; } if (GameMain.GameSession == null || !GameMain.GameSession.IsRunning) { return; } if (ActiveHintMessageBox != null) @@ -137,7 +137,7 @@ namespace Barotrauma // onstartedinteracting.brokenitem if (item.Repairables.Any(r => r.IsBelowRepairThreshold)) { - if (DisplayHint($"{hintIdentifierBase}.brokenitem")) { return; } + if (DisplayHint($"{hintIdentifierBase}.brokenitem".ToIdentifier())) { return; } } // Don't display other item-related hints if the repair interface is displayed @@ -147,22 +147,25 @@ namespace Barotrauma if (item.Submarine?.Info?.Type == SubmarineType.Outpost && item.ContainedItems.Any(i => !i.AllowStealing)) { - if (DisplayHint($"{hintIdentifierBase}.lootingisstealing")) { return; } + if (DisplayHint($"{hintIdentifierBase}.lootingisstealing".ToIdentifier())) { return; } } // onstartedinteracting.turretperiscope if (item.HasTag("periscope") && item.GetConnectedComponents().FirstOrDefault(t => t.Item.HasTag("turret")) is Turret) { - if (DisplayHint($"{hintIdentifierBase}.turretperiscope", - variableTags: new string[] { "[shootkey]", "[deselectkey]", }, - variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect) })) + if (DisplayHint($"{hintIdentifierBase}.turretperiscope".ToIdentifier(), + variables: new[] + { + ("[shootkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Shoot)), + ("[deselectkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Deselect)) + })) { return; } } // onstartedinteracting.item... hintIdentifierBase += ".item"; - foreach (string hintIdentifier in HintIdentifiers) + foreach (Identifier hintIdentifier in HintIdentifiers) { if (!hintIdentifier.StartsWith(hintIdentifierBase)) { continue; } if (!HintTags.TryGetValue(hintIdentifier, out var hintTags)) { continue; } @@ -180,7 +183,7 @@ namespace Barotrauma Character.Controlled.SelectedConstruction.OwnInventory?.AllItems is IEnumerable containedItems && containedItems.Count(i => i.HasTag("reactorfuel")) > 1) { - if (DisplayHint("onisinteracting.reactorwithextrarods")) { return; } + if (DisplayHint("onisinteracting.reactorwithextrarods".ToIdentifier())) { return; } } } @@ -233,11 +236,11 @@ namespace Barotrauma } if (!GameMain.GameSession.GameMode.IsSinglePlayer && - GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { - DisplayHint("onroundstarted.voipdisabled", onUpdate: () => + DisplayHint("onroundstarted.voipdisabled".ToIdentifier(), onUpdate: () => { - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) { return; } + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { return; } ActiveHintMessageBox.Close(); }); } @@ -271,7 +274,7 @@ namespace Barotrauma if (spottedCharacter == null || spottedCharacter.Removed || spottedCharacter.IsDead) { return; } if (Character.Controlled.SelectedConstruction != sonar) { return; } if (HumanAIController.IsFriendly(Character.Controlled, spottedCharacter)) { return; } - DisplayHint("onsonarspottedenemy"); + DisplayHint("onsonarspottedenemy".ToIdentifier()); } public static void OnAfflictionDisplayed(Character character, List displayedAfflictions) @@ -285,9 +288,8 @@ namespace Barotrauma if (affliction.Prefab == AfflictionPrefab.OxygenLow) { continue; } if (affliction.Prefab == AfflictionPrefab.RadiationSickness && (GameMain.GameSession.Map?.Radiation?.IsEntityRadiated(character) ?? false)) { continue; } if (affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } - DisplayHint("onafflictiondisplayed", - variableTags: new string[1] { "[key]" }, - variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Health) }, + DisplayHint("onafflictiondisplayed".ToIdentifier(), + variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)) }, icon: affliction.Prefab.Icon, iconColor: CharacterHealth.GetAfflictionIconColor(affliction), onUpdate: () => @@ -308,12 +310,11 @@ namespace Barotrauma if (TimeStoppedInteracting + 1 > Timing.TotalTime) { return; } if (GUI.MouseOn != null) { return; } if (Character.Controlled.Inventory?.visualSlots != null && Character.Controlled.Inventory.visualSlots.Any(s => s.InteractRect.Contains(PlayerInput.MousePosition))) { return; } - string hintIdentifier = "onshootwithoutaiming"; + Identifier hintIdentifier = "onshootwithoutaiming".ToIdentifier(); if (!HintTags.TryGetValue(hintIdentifier, out var tags)) { return; } if (!item.HasTag(tags)) { return; } DisplayHint(hintIdentifier, - variableTags: new string[1] { "[key]" }, - variableValues: new string[1] { GameMain.Config.KeyBindText(InputType.Aim) }, + variables: new[] { ("[key]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Aim)) }, onUpdate: () => { if (character.SelectedConstruction == null && GUI.MouseOn == null && PlayerInput.KeyDown(InputType.Aim)) @@ -328,21 +329,21 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (door == null || door.Stuck < 20.0f) { return; } - DisplayHint("onweldingdoor"); + DisplayHint("onweldingdoor".ToIdentifier()); } public static void OnTryOpenStuckDoor(Character character) { if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } - DisplayHint("ontryopenstuckdoor"); + DisplayHint("ontryopenstuckdoor".ToIdentifier()); } public static void OnShowCampaignInterface(CampaignMode.InteractionType interactionType) { if (!CanDisplayHints()) { return; } if (interactionType == CampaignMode.InteractionType.None) { return; } - string hintIdentifier = $"onshowcampaigninterface.{interactionType.ToString().ToLowerInvariant()}"; + Identifier hintIdentifier = $"onshowcampaigninterface.{interactionType}".ToIdentifier(); DisplayHint(hintIdentifier, onUpdate: () => { @@ -359,7 +360,7 @@ namespace Barotrauma { IgnoreReminder("commandinterface"); if (!CanDisplayHints()) { return; } - DisplayHint("onshowcommandinterface", onUpdate: () => + DisplayHint("onshowcommandinterface".ToIdentifier(), onUpdate: () => { if (CrewManager.IsCommandInterfaceOpen) { return; } ActiveHintMessageBox.Close(); @@ -370,7 +371,7 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (CharacterHealth.OpenHealthWindow == null) { return; } - DisplayHint("onshowhealthinterface", onUpdate: () => + DisplayHint("onshowhealthinterface".ToIdentifier(), onUpdate: () => { if (CharacterHealth.OpenHealthWindow != null) { return; } ActiveHintMessageBox.Close(); @@ -387,7 +388,7 @@ namespace Barotrauma if (!CanDisplayHints()) { return; } if (character != Character.Controlled) { return; } if (item == null || item.AllowStealing || !item.StolenDuringRound) { return; } - DisplayHint("onstoleitem", onUpdate: () => + DisplayHint("onstoleitem".ToIdentifier(), onUpdate: () => { if (item == null || item.Removed || item.GetRootInventoryOwner() != character) { @@ -400,7 +401,7 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (character != Character.Controlled || !character.LockHands) { return; } - DisplayHint("onhandcuffed", onUpdate: () => + DisplayHint("onhandcuffed".ToIdentifier(), onUpdate: () => { if (character != null && !character.Removed && character.LockHands) { return; } ActiveHintMessageBox.Close(); @@ -413,7 +414,7 @@ namespace Barotrauma if (reactor == null) { return; } if (reactor.Item.Submarine?.Info?.Type != SubmarineType.Player || reactor.Item.Submarine.TeamID != Character.Controlled.TeamID) { return; } if (!HasValidJob("engineer")) { return; } - DisplayHint("onreactoroutoffuel", onUpdate: () => + DisplayHint("onreactoroutoffuel".ToIdentifier(), onUpdate: () => { if (reactor?.Item != null && !reactor.Item.Removed && reactor.AvailableFuel < 1) { return; } ActiveHintMessageBox.Close(); @@ -424,13 +425,13 @@ namespace Barotrauma { if (!CanDisplayHints()) { return; } if (transitionType == CampaignMode.TransitionType.None) { return; } - DisplayHint($"onavailabletransition.{transitionType.ToString().ToLowerInvariant()}"); + DisplayHint($"onavailabletransition.{transitionType}".ToIdentifier()); } public static void OnShowSubInventory(Item item) { if (item?.Prefab == null) { return; } - if (item.Prefab.Identifier.Equals("toolbelt", StringComparison.OrdinalIgnoreCase)) + if (item.Prefab.Identifier == "toolbelt") { IgnoreReminder("toolbelt"); } @@ -447,7 +448,7 @@ namespace Barotrauma if (character != Character.Controlled) { return; } if (character.IsDead) { return; } if (character.CharacterHealth != null && character.Vitality < character.CharacterHealth.MinVitality) { return; } - DisplayHint("oncharacterunconscious"); + DisplayHint("oncharacterunconscious".ToIdentifier()); } public static void OnCharacterKilled(Character character) @@ -457,21 +458,21 @@ namespace Barotrauma if (GameMain.IsMultiplayer) { return; } if (GameMain.GameSession?.CrewManager == null) { return; } if (GameMain.GameSession.CrewManager.GetCharacters().None(c => !c.IsDead)) { return; } - DisplayHint("oncharacterkilled"); + DisplayHint("oncharacterkilled".ToIdentifier()); } private static void OnStartedControlling() { if (Level.IsLoadedOutpost) { return; } if (Character.Controlled?.Info?.Job?.Prefab == null) { return; } - string hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}"; + Identifier hintIdentifier = $"onstartedcontrolling.job.{Character.Controlled.Info.Job.Prefab.Identifier}".ToIdentifier(); DisplayHint(hintIdentifier, icon: Character.Controlled.Info.Job.Prefab.Icon, iconColor: Character.Controlled.Info.Job.Prefab.UIColor, onDisplay: () => { if (!HintOrders.TryGetValue(hintIdentifier, out var orderInfo)) { return; } - var orderPrefab = Order.GetPrefab(orderInfo.identifier); + var orderPrefab = OrderPrefab.Prefabs[orderInfo.identifier]; if (orderPrefab == null) { return; } Item targetEntity = null; ItemComponent targetItem = null; @@ -481,8 +482,8 @@ namespace Barotrauma if (targetEntity == null) { return; } targetItem = orderPrefab.GetTargetItemComponent(targetEntity); } - var order = new Order(orderPrefab, targetEntity as Entity, targetItem, orderGiver: Character.Controlled); - GameMain.GameSession?.CrewManager?.SetCharacterOrder(Character.Controlled, order, orderInfo.option, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + var order = new Order(orderPrefab, orderInfo.option, targetEntity, targetItem, orderGiver: Character.Controlled).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + GameMain.GameSession.CrewManager.SetCharacterOrder(Character.Controlled, order); }); } @@ -498,7 +499,7 @@ namespace Barotrauma if (!steering.SteeringPath.Finished && steering.SteeringPath.NextNode != null) { return; } if (steering.LevelStartSelected && (Level.Loaded.StartOutpost == null || !steering.Item.Submarine.AtStartExit)) { return; } if (steering.LevelEndSelected && (Level.Loaded.EndOutpost == null || !steering.Item.Submarine.AtEndExit)) { return; } - DisplayHint("onautopilotreachedoutpost"); + DisplayHint("onautopilotreachedoutpost".ToIdentifier()); } public static void OnStatusEffectApplied(ItemComponent component, ActionType actionType, Character character) @@ -507,7 +508,7 @@ namespace Barotrauma if (character != Character.Controlled) { return; } // Could make this more generic if there will ever be any other status effect related hints if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; } - DisplayHint("onrepairfailed"); + DisplayHint("onrepairfailed".ToIdentifier()); } public static void OnActiveOrderAdded(Order order) @@ -519,7 +520,7 @@ namespace Barotrauma order.TargetEntity is Hull h && h.Submarine?.TeamID == Character.Controlled.TeamID) { - DisplayHint("onballastflorainfected"); + DisplayHint("onballastflorainfected".ToIdentifier()); } } @@ -529,7 +530,7 @@ namespace Barotrauma var divingGear = Character.Controlled.GetEquippedItem("diving", InvSlotType.OuterClothes); if (divingGear?.OwnInventory == null) { return; } if (divingGear.GetContainedItemConditionPercentage() > 0.0f) { return; } - DisplayHint("ondivinggearoutofoxygen", onUpdate: () => + DisplayHint("ondivinggearoutofoxygen".ToIdentifier(), onUpdate: () => { if (divingGear == null || divingGear.Removed || Character.Controlled == null || !Character.Controlled.HasEquippedItem(divingGear) || @@ -546,7 +547,7 @@ namespace Barotrauma if (Character.Controlled.CurrentHull == null) { return; } if (HumanAIController.IsBallastFloraNoticeable(Character.Controlled, Character.Controlled.CurrentHull)) { - if (IsOnFriendlySub() && DisplayHint("onballastflorainfected")) { return; } + if (IsOnFriendlySub() && DisplayHint("onballastflorainfected".ToIdentifier())) { return; } } foreach (var gap in Character.Controlled.CurrentHull.ConnectedGaps) { @@ -556,7 +557,7 @@ namespace Barotrauma { if (!IsWearingDivingSuit()) { continue; } if (Character.Controlled.IsProtectedFromPressure()) { continue; } - if (DisplayHint("divingsuitwarning", extendTextTag: false)) { return; } + if (DisplayHint("divingsuitwarning".ToIdentifier(), extendTextTag: false)) { return; } continue; } foreach (var me in gap.linkedTo) @@ -565,8 +566,8 @@ namespace Barotrauma if (!(me is Hull adjacentHull)) { continue; } if (!IsOnFriendlySub()) { continue; } if (IsWearingDivingSuit()) { continue; } - if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure")) { return; } - if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage")) { return; } + if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; } + if (adjacentHull.WaterPercentage > 75 && !BallastHulls.Contains(adjacentHull) && DisplayHint("onadjacenthull.highwaterpercentage".ToIdentifier())) { return; } } static bool IsWearingDivingSuit() => Character.Controlled.GetEquippedItem("deepdiving", InvSlotType.OuterClothes) is Item; @@ -586,7 +587,7 @@ namespace Barotrauma if (GameMain.GameSession.GameMode.IsSinglePlayer) { - if (DisplayHint($"{hintIdentifierBase}.characterchange")) + if (DisplayHint($"{hintIdentifierBase}.characterchange".ToIdentifier())) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; @@ -595,9 +596,8 @@ namespace Barotrauma if (Level.Loaded.Type != LevelData.LevelType.Outpost) { - if (DisplayHint($"{hintIdentifierBase}.commandinterface", - variableTags: new string[] { "[commandkey]" }, - variableValues: new string[] { GameMain.Config.KeyBindText(InputType.Command) }, + if (DisplayHint($"{hintIdentifierBase}.commandinterface".ToIdentifier(), + variables: new[] { ("[commandkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Command)) }, onUpdate: () => { if (!CrewManager.IsCommandInterfaceOpen) { return; } @@ -609,9 +609,8 @@ namespace Barotrauma } } - if (DisplayHint($"{hintIdentifierBase}.tabmenu", - variableTags: new string[] { "[infotabkey]" }, - variableValues: new string[] { GameMain.Config.KeyBindText(InputType.InfoTab) }, + if (DisplayHint($"{hintIdentifierBase}.tabmenu".ToIdentifier(), + variables: new[] { ("[infotabkey]".ToIdentifier(), GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.InfoTab)) }, onUpdate: () => { if (!GameSession.IsTabMenuOpen) { return; } @@ -624,7 +623,7 @@ namespace Barotrauma if (Character.Controlled.Inventory?.GetItemInLimbSlot(InvSlotType.Bag)?.Prefab?.Identifier == "toolbelt") { - if (DisplayHint($"{hintIdentifierBase}.toolbelt")) + if (DisplayHint($"{hintIdentifierBase}.toolbelt".ToIdentifier())) { TimeReminderLastDisplayed = GameMain.GameScreen.GameTime; return; @@ -632,25 +631,25 @@ namespace Barotrauma } } - private static bool DisplayHint(string hintIdentifier, bool extendTextTag = true, string[] variableTags = null, string[] variableValues = null, Sprite icon = null, Color? iconColor = null, Action onDisplay = null, Action onUpdate = null) + private static bool DisplayHint(Identifier hintIdentifier, bool extendTextTag = true, (Identifier Tag, LocalizedString Value)[] variables = null, Sprite icon = null, Color? iconColor = null, Action onDisplay = null, Action onUpdate = null) { - if (string.IsNullOrEmpty(hintIdentifier)) { return false; } + if (hintIdentifier == Identifier.Empty) { return false; } if (!HintIdentifiers.Contains(hintIdentifier)) { return false; } - if (GameMain.Config.IgnoredHints.Contains(hintIdentifier)) { return false; } + if (IgnoredHints.Instance.Contains(hintIdentifier)) { return false; } if (HintsIgnoredThisRound.Contains(hintIdentifier)) { return false; } - string text; - string textTag = extendTextTag ? $"hint.{hintIdentifier}" : hintIdentifier; - if (variableTags != null && variableTags != null && variableTags.Length > 0 && variableTags.Length == variableValues.Length) + LocalizedString text; + Identifier textTag = extendTextTag ? $"hint.{hintIdentifier}".ToIdentifier() : hintIdentifier; + if (variables != null && variables.Length > 0) { - text = TextManager.GetWithVariables(textTag, variableTags, variableValues, returnNull: true); + text = TextManager.GetWithVariables(textTag, variables); } else { - text = TextManager.Get(textTag, returnNull: true); + text = TextManager.Get(textTag); } - if (string.IsNullOrEmpty(text)) + if (text.IsNullOrEmpty()) { #if DEBUG DebugConsole.ThrowError($"No hint text found for text tag \"{textTag}\""); @@ -668,20 +667,20 @@ namespace Barotrauma ActiveHintMessageBox.InnerFrame.Flash(color: iconColor ?? Color.Orange, flashDuration: 0.75f); onDisplay?.Invoke(); - GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none"}:HintDisplayed:{hintIdentifier}"); + GameAnalyticsManager.AddDesignEvent($"HintManager:{GameMain.GameSession?.GameMode?.Preset?.Identifier ?? "none".ToIdentifier()}:HintDisplayed:{hintIdentifier}"); return true; } public static bool OnDontShowAgain(GUITickBox tickBox) { - IgnoreHint((string)tickBox.UserData, ignore: tickBox.Selected); + IgnoreHint((Identifier)tickBox.UserData, ignore: tickBox.Selected); return true; } - private static void IgnoreHint(string hintIdentifier, bool ignore = true) + private static void IgnoreHint(Identifier hintIdentifier, bool ignore = true) { - if (string.IsNullOrEmpty(hintIdentifier)) { return; } + if (hintIdentifier.IsEmpty) { return; } if (!HintIdentifiers.Contains(hintIdentifier)) { #if DEBUG @@ -691,29 +690,32 @@ namespace Barotrauma } if (ignore) { - GameMain.Config.IgnoredHints.Add(hintIdentifier); + IgnoredHints.Instance.Add(hintIdentifier); } else { - GameMain.Config.IgnoredHints.Remove(hintIdentifier); + IgnoredHints.Instance.Remove(hintIdentifier); } } private static void IgnoreReminder(string reminderIdentifier) { - HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}"); + HintsIgnoredThisRound.Add($"reminder.{reminderIdentifier}".ToIdentifier()); } public static bool OnDisableHints(GUITickBox tickBox) { - GameMain.Config.DisableInGameHints = tickBox.Selected; - return GameMain.Config.SaveNewPlayerConfig(); + var config = GameSettings.CurrentConfig; + config.DisableInGameHints = tickBox.Selected; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); + return true; } private static bool CanDisplayHints(bool requireGameScreen = true, bool requireControllingCharacter = true) { if (HintIdentifiers == null) { return false; } - if (GameMain.Config.DisableInGameHints) { return false; } + if (GameSettings.CurrentConfig.DisableInGameHints) { return false; } if (ActiveHintMessageBox != null) { return false; } if (requireControllingCharacter && Character.Controlled == null) { return false; } var gameMode = GameMain.GameSession?.GameMode; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index e6d7114bc..003816dae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -9,16 +9,18 @@ namespace Barotrauma { internal partial class ReadyCheck { - private static string readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); + private static LocalizedString readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); - private static string readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", new[] { "[ready]", "[total]" }, new[] { ready.ToString(), total.ToString() }); - private static string readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); + private static LocalizedString readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", + ("[ready]", ready.ToString()), + ("[total]", total.ToString())); + private static LocalizedString readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); - private static readonly string readyCheckHeader = TextManager.Get("ReadyCheck.Title"); + private static readonly LocalizedString readyCheckHeader = TextManager.Get("ReadyCheck.Title"); - private static readonly string noButton = TextManager.Get("No"), - yesButton = TextManager.Get("Yes"), - closeButton = TextManager.Get("Close"); + private static readonly LocalizedString noButton = TextManager.Get("No"), + yesButton = TextManager.Get("Yes"), + closeButton = TextManager.Get("Close"); private const string TimerData = "Timer", PromptData = "ReadyCheck", @@ -42,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, GUI.Style.Orange) { UserData = TimerData }; + new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), time / endTime, GUIStyle.Orange) { UserData = TimerData }; // Yes msgBox.Buttons[0].OnClicked = delegate @@ -222,7 +224,7 @@ namespace Barotrauma int readyCount = Clients.Count(pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; - GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount), ChatMessageType.Server, null)); + GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount).Value, ChatMessageType.Server, null)); } private void UpdateState(byte id, ReadyStatus status) @@ -256,7 +258,7 @@ namespace Barotrauma return; } - image.ApplyStyle(GUI.Style.GetComponentStyle(style)); + image.ApplyStyle(GUIStyle.GetComponentStyle(style)); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index f0db4ea5b..1a0f12157 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -59,7 +59,7 @@ namespace Barotrauma if (!singleplayer) { - SoundPlayer.OverrideMusicType = gameOver ? "crewdead" : "endround"; + SoundPlayer.OverrideMusicType = (gameOver ? "crewdead" : "endround").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; } @@ -84,7 +84,7 @@ namespace Barotrauma }; var crewHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent.RectTransform), - TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2)); @@ -101,19 +101,19 @@ namespace Barotrauma Stretch = true }; var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), - CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2)); } //header ------------------------------------------------------------------------------- - string headerText = GetHeaderText(gameOver, transitionType); + LocalizedString headerText = GetHeaderText(gameOver, transitionType); GUITextBlock headerTextBlock = null; - if (!string.IsNullOrEmpty(headerText)) + if (!headerText.IsNullOrEmpty()) { headerTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), crewFrame.RectTransform, Anchor.TopLeft, Pivot.BottomLeft), - headerText, textAlignment: Alignment.BottomLeft, font: GUI.LargeFont, wrap: true); + headerText, textAlignment: Alignment.BottomLeft, font: GUIStyle.LargeFont, wrap: true); } //traitor panel ------------------------------------------------------------------------------- @@ -130,14 +130,14 @@ namespace Barotrauma }; var traitorHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorContent.RectTransform), - TextManager.Get("traitors"), font: GUI.SubHeadingFont); + TextManager.Get("traitors"), font: GUIStyle.SubHeadingFont); traitorHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(traitorHeader.Rect.Height * 2.0f)); GUIListBox listBox = CreateCrewList(traitorContent, traitorResults.SelectMany(tr => tr.Characters.Select(c => c.Info))); foreach (var traitorResult in traitorResults) { - var traitorMission = TraitorMissionPrefab.List.Find(t => t.Identifier == traitorResult.MissionIdentifier); + var traitorMission = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == traitorResult.MissionIdentifier); if (traitorMission == null) { continue; } //spacing @@ -154,8 +154,8 @@ namespace Barotrauma Color = traitorMission.IconColor }; - string traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); - if (!string.IsNullOrEmpty(traitorMessage)) + LocalizedString traitorMessage = TextManager.GetServerMessage(traitorResult.EndMessage); + if (!traitorMessage.IsNullOrEmpty()) { var textContent = new GUILayoutGroup(new RectTransform(Vector2.One, traitorResultHorizontal.RectTransform)) { @@ -164,10 +164,10 @@ namespace Barotrauma var traitorStatusText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get(traitorResult.Success ? "missioncompleted" : "missionfailed"), - textColor: traitorResult.Success ? GUI.Style.Green : GUI.Style.Red, font: GUI.SubHeadingFont); + textColor: traitorResult.Success ? GUIStyle.Green : GUIStyle.Red, font: GUIStyle.SubHeadingFont); var traitorMissionInfo = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - traitorMessage, font: GUI.SmallFont, wrap: true); + traitorMessage, font: GUIStyle.SmallFont, wrap: true); traitorResultHorizontal.Recalculate(); @@ -196,7 +196,7 @@ namespace Barotrauma }; var reputationHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), reputationContent.RectTransform), - TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + TextManager.Get("reputation"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); reputationHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(reputationHeader.Rect.Height * 2.0f)); CreateReputationInfoPanel(reputationContent, campaignMode); @@ -233,7 +233,7 @@ namespace Barotrauma if (missionsToDisplay.Any()) { var missionHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), - TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont); missionHeader.RectTransform.MinSize = new Point(0, (int)(missionHeader.Rect.Height * 1.2f)); } @@ -271,7 +271,7 @@ namespace Barotrauma Stretch = true }; - string missionMessage = + LocalizedString missionMessage = selectedMissions.Contains(displayedMission) ? displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : displayedMission.Description; @@ -293,7 +293,7 @@ namespace Barotrauma }; missionContentHorizontal.Recalculate(); var missionNameTextBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - displayedMission.Name, font: GUI.SubHeadingFont); + displayedMission.Name, font: GUIStyle.SubHeadingFont); if (displayedMission.Difficulty.HasValue) { var groupSize = missionNameTextBlock.Rect.Size; @@ -314,12 +314,12 @@ namespace Barotrauma } } var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - missionMessage, wrap: true, parseRichText: true); + RichString.Rich(missionMessage), wrap: true); int reward = displayedMission.GetReward(Submarine.MainSub); if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) { - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), displayedMission.GetMissionRewardText(Submarine.MainSub), parseRichText: true); + LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); } if (displayedMission != missionsToDisplay.Last()) @@ -346,7 +346,7 @@ namespace Barotrauma GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), - TextManager.Get("nomission"), font: GUI.LargeFont); + TextManager.Get("nomission"), font: GUIStyle.LargeFont); } /*missionContentHorizontal.Recalculate(); @@ -397,7 +397,7 @@ namespace Barotrauma if (startLocation.Type.HasOutpost && startLocation.Reputation != null) { - var iconStyle = GUI.Style.GetComponentStyle("LocationReputationIcon"); + var iconStyle = GUIStyle.GetComponentStyle("LocationReputationIcon"); var locationFrame = CreateReputationElement( reputationList.Content, startLocation.Name, @@ -452,8 +452,8 @@ namespace Barotrauma var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; var unlockEvent = - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && string.IsNullOrEmpty(ep.BiomeIdentifier)); + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); if (unlockEvent == null) { continue; } if (string.IsNullOrEmpty(unlockEvent.UnlockPathFaction) || unlockEvent.UnlockPathFaction.Equals("location", StringComparison.OrdinalIgnoreCase)) @@ -462,7 +462,7 @@ namespace Barotrauma } else { - if (faction == null || !faction.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)) { continue; } + if (faction == null || faction.Prefab.Identifier != unlockEvent.UnlockPathFaction) { continue; } } if (unlockEvent != null) @@ -471,20 +471,20 @@ namespace Barotrauma Faction unlockFaction = null; if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); unlockReputation = unlockFaction?.Reputation; } float normalizedUnlockReputation = MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation); - string unlockText = TextManager.GetWithVariables( + RichString unlockText = RichString.Rich(TextManager.GetWithVariables( "lockedpathreputationrequirement", - new string[] { "[reputation]", "[biomename]" }, - new string[] { Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true), $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖" }); + ("[reputation]", Reputation.GetFormattedReputationText(normalizedUnlockReputation, unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[biomename]", $"‖color:gui.orange‖{connection.LevelData.Biome.DisplayName}‖end‖"))); var unlockInfoPanel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), reputationFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, GUI.IntScale(30)), AbsoluteOffset = new Point(0, GUI.IntScale(3)) }, - unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUI.Style.TextColor, parseRichText: true); + unlockText, style: "GUIButtonRound", textAlignment: Alignment.Center, textColor: GUIStyle.TextColorNormal); unlockInfoPanel.Color = Color.Lerp(unlockInfoPanel.Color, Color.Black, 0.8f); if (unlockInfoPanel.TextSize.X > unlockInfoPanel.Rect.Width * 0.7f) { - unlockInfoPanel.Font = GUI.SmallFont; + unlockInfoPanel.Font = GUIStyle.SmallFont; } } } @@ -492,7 +492,7 @@ namespace Barotrauma } } - private string GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) + private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType) { string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name; @@ -539,14 +539,14 @@ namespace Barotrauma locationName = "[UNKNOWN]"; } - string subName = string.Empty; + LocalizedString subName = string.Empty; SubmarineInfo currentOrPending = SubmarineSelection.CurrentOrPendingSubmarine(); if (currentOrPending != null) { subName = currentOrPending.DisplayName; } - return TextManager.GetWithVariables(textTag, new string[2] { "[sub]", "[location]" }, new string[2] { subName, locationName }); + return TextManager.GetWithVariables(textTag, ("[sub]", subName), ("[location]", locationName)); } private GUIListBox CreateCrewList(GUIComponent parent, IEnumerable characterInfos) @@ -566,9 +566,9 @@ namespace Barotrauma characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); statusButton.RectTransform.RelativeSize = new Vector2(statusColumnWidthPercentage * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = statusButton.TextBlock.Font = GUIStyle.HotkeyFont; jobButton.CanBeFocused = characterButton.CanBeFocused = statusButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = true; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = statusButton.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; @@ -615,10 +615,10 @@ namespace Barotrauma }; GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(characterInfo.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: characterInfo.Job.Prefab.UIColor); + ToolBox.LimitString(characterInfo.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: characterInfo.Job.Prefab.UIColor); - string statusText = TextManager.Get("StatusOK"); - Color statusColor = GUI.Style.Green; + LocalizedString statusText = TextManager.Get("StatusOK"); + Color statusColor = GUIStyle.Green; Character character = characterInfo.Character; if (character == null || character.IsDead) @@ -626,7 +626,7 @@ namespace Barotrauma if (character == null && characterInfo.IsNewHire && characterInfo.CauseOfDeath == null) { statusText = TextManager.Get("CampaignCrew.NewHire"); - statusColor = GUI.Style.Blue; + statusColor = GUIStyle.Blue; } else if (characterInfo.CauseOfDeath == null) { @@ -637,9 +637,9 @@ namespace Barotrauma { string errorMsg = "Character \"[name]\" had an invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified)."; DebugConsole.ThrowError(errorMsg.Replace("[name]", characterInfo.Name)); - GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", characterInfo.SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("RoundSummary:InvalidCauseOfDeath", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", characterInfo.SpeciesName.Value)); statusText = TextManager.Get("CauseOfDeathDescription.Unknown"); - statusColor = GUI.Style.Red; + statusColor = GUIStyle.Red; } else { @@ -664,12 +664,12 @@ namespace Barotrauma } GUITextBlock statusBlock = new GUITextBlock(new RectTransform(new Point(statusColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(statusText, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); + ToolBox.LimitString(statusText.Value, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: statusColor); } private GUIFrame CreateReputationElement(GUIComponent parent, - string name, float reputation, float normalizedReputation, float initialReputation, - string shortDescription, string fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) + LocalizedString name, float reputation, float normalizedReputation, float initialReputation, + LocalizedString shortDescription, LocalizedString fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), style: null); @@ -704,7 +704,7 @@ namespace Barotrauma factionInfoHorizontal.Recalculate(); var header = new GUITextBlock(new RectTransform(new Point(factionTextContent.Rect.Width, GUI.IntScale(40)), factionTextContent.RectTransform), - name, font: GUI.SubHeadingFont) + name, font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, UserData = "header" @@ -723,34 +723,34 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.8f, 1.0f), sliderHolder.RectTransform), onDraw: (sb, customComponent) => DrawReputationBar(sb, customComponent.Rect, normalizedReputation)); - string reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); + LocalizedString reputationText = Reputation.GetFormattedReputationText(normalizedReputation, reputation, addColorTags: true); int reputationChange = (int)Math.Round(reputation - initialReputation); if (Math.Abs(reputationChange) > 0) { string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUI.Style.Green : GUI.Style.Red); - var rtData = RichTextData.GetRichTextData($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)", out string sanitizedText); + string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - rtData, sanitizedText, - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); + richText, + textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); } else { new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), - reputationText, - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont, parseRichText: true); + RichString.Rich(reputationText), + textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); } //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), factionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(5)) }, style: null); var factionDescription = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.6f), factionTextContent.RectTransform), - shortDescription, font: GUI.SmallFont, wrap: true) + shortDescription, font: GUIStyle.SmallFont, wrap: true) { UserData = "description", Padding = Vector4.Zero }; - if (shortDescription != fullDescription && !string.IsNullOrEmpty(fullDescription)) + if (shortDescription != fullDescription && !fullDescription.IsNullOrEmpty()) { factionDescription.ToolTip = fullDescription; } @@ -771,16 +771,16 @@ namespace Barotrauma for (int i = 0; i < 5; i++) { GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), Reputation.GetReputationColor(i / 5.0f), isFilled: true); - GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), GUI.Style.ColorInventoryBackground, isFilled: false); + GUI.DrawRectangle(sb, new Rectangle(rect.X + (segmentWidth * i), rect.Y, segmentWidth, rect.Height), GUIStyle.ColorInventoryBackground, isFilled: false); } - GUI.DrawRectangle(sb, rect, GUI.Style.ColorInventoryBackground, isFilled: false); + GUI.DrawRectangle(sb, rect, GUIStyle.ColorInventoryBackground, isFilled: false); - GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUI.Style.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); - GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUI.Style.TextColor, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); + GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.ColorInventoryBackground, scale: GUI.Scale, spriteEffect: SpriteEffects.FlipVertically); + GUI.Arrow.Draw(sb, new Vector2(rect.X + rect.Width * normalizedReputation, rect.Y), GUIStyle.TextColorNormal, scale: GUI.Scale * 0.8f, spriteEffect: SpriteEffects.FlipVertically); - GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUI.Style.TextColor, font: GUI.SmallFont); - Vector2 textSize = GUI.SmallFont.MeasureString("100"); - GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUI.Style.TextColor, font: GUI.SmallFont); + GUI.DrawString(sb, new Vector2(rect.X, rect.Bottom), "-100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); + Vector2 textSize = GUIStyle.SmallFont.MeasureString("100"); + GUI.DrawString(sb, new Vector2(rect.Right - textSize.X, rect.Bottom), "100", GUIStyle.TextColorNormal, font: GUIStyle.SmallFont); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs deleted file mode 100644 index fd2e7a7cd..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ /dev/null @@ -1,1932 +0,0 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; -using OpenAL; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma -{ - public partial class GameSettings - { - public enum Tab - { - Graphics, - Audio, - VoiceChat, - Controls, - Gameplay, -#if DEBUG - Debug -#endif - } - - private readonly Point MinSupportedResolution = new Point(1024, 540); - - private GUIFrame settingsFrame; - private GUIButton applyButton; - - private GUIFrame[] tabs; - private GUIButton[] tabButtons; - - public Action OnHUDScaleChanged; - - public GUIFrame SettingsFrame - { - get - { - if (settingsFrame == null) CreateSettingsFrame(); - return settingsFrame; - } - } - - private const int inventoryHotkeyCount = 10; - - public void SetDefaultBindings(XDocument doc = null, bool legacy = false) - { - keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; - keyMapping[(int)InputType.Run] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F); - keyMapping[(int)InputType.Crouch] = new KeyOrMouse(Keys.LeftControl); - keyMapping[(int)InputType.Grab] = new KeyOrMouse(Keys.G); - keyMapping[(int)InputType.Health] = new KeyOrMouse(Keys.H); - keyMapping[(int)InputType.Ragdoll] = new KeyOrMouse(Keys.Space); - keyMapping[(int)InputType.Aim] = new KeyOrMouse(MouseButton.SecondaryMouse); - - keyMapping[(int)InputType.InfoTab] = new KeyOrMouse(Keys.Tab); - keyMapping[(int)InputType.Chat] = new KeyOrMouse(Keys.T); - keyMapping[(int)InputType.RadioChat] = new KeyOrMouse(Keys.R); - keyMapping[(int)InputType.CrewOrders] = new KeyOrMouse(Keys.C); - - keyMapping[(int)InputType.Voice] = new KeyOrMouse(Keys.V); - keyMapping[(int)InputType.LocalVoice] = new KeyOrMouse(Keys.B); - keyMapping[(int)InputType.Command] = new KeyOrMouse(MouseButton.MiddleMouse); - keyMapping[(int)InputType.PreviousFireMode] = new KeyOrMouse(MouseButton.MouseWheelDown); - keyMapping[(int)InputType.NextFireMode] = new KeyOrMouse(MouseButton.MouseWheelUp); - - keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); - - if (Language == "French") - { - keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.Z); - keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S); - keyMapping[(int)InputType.Left] = new KeyOrMouse(Keys.Q); - keyMapping[(int)InputType.Right] = new KeyOrMouse(Keys.D); - keyMapping[(int)InputType.ToggleInventory] = new KeyOrMouse(Keys.A); - - keyMapping[(int)InputType.SelectNextCharacter] = new KeyOrMouse(Keys.X); - keyMapping[(int)InputType.SelectPreviousCharacter] = new KeyOrMouse(Keys.W); - } - else - { - keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.W); - keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S); - keyMapping[(int)InputType.Left] = new KeyOrMouse(Keys.A); - keyMapping[(int)InputType.Right] = new KeyOrMouse(Keys.D); - keyMapping[(int)InputType.ToggleInventory] = new KeyOrMouse(Keys.Q); - - keyMapping[(int)InputType.SelectNextCharacter] = new KeyOrMouse(Keys.Z); - keyMapping[(int)InputType.SelectPreviousCharacter] = new KeyOrMouse(Keys.X); - } - - if (legacy) - { - keyMapping[(int)InputType.Use] = new KeyOrMouse(MouseButton.PrimaryMouse); - keyMapping[(int)InputType.Shoot] = new KeyOrMouse(MouseButton.PrimaryMouse); - keyMapping[(int)InputType.Select] = new KeyOrMouse(Keys.E); - keyMapping[(int)InputType.Deselect] = new KeyOrMouse(Keys.E); - } - else - { - keyMapping[(int)InputType.Use] = new KeyOrMouse(Keys.E); - keyMapping[(int)InputType.Select] = new KeyOrMouse(MouseButton.PrimaryMouse); - // shoot and deselect are handled in CheckBindings() so that we don't override the legacy settings. - } - - inventoryKeyMapping = new KeyOrMouse[inventoryHotkeyCount]; - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - inventoryKeyMapping[i] = new KeyOrMouse(Keys.D0 + (i + 1) % 10); - } - - if (doc != null) - { - LoadControls(doc); - } - } - - public void CheckBindings(bool useDefaults) - { - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - var binding = keyMapping[(int)inputType]; - if (binding == null) - { - switch (inputType) - { - case InputType.Deselect: - if (useDefaults) - { - binding = new KeyOrMouse(MouseButton.SecondaryMouse); - } - else - { - // Legacy support - var selectKey = keyMapping[(int)InputType.Select]; - if (selectKey != null && selectKey.Key != Keys.None) - { - binding = new KeyOrMouse(selectKey.Key); - } - } - break; - case InputType.Shoot: - if (useDefaults) - { - binding = new KeyOrMouse(MouseButton.PrimaryMouse); - } - else - { - // Legacy support - var useKey = keyMapping[(int)InputType.Use]; - if (useKey != null && useKey.MouseButton != MouseButton.None) - { - binding = new KeyOrMouse(useKey.MouseButton); - } - } - break; - default: - break; - } - if (binding == null) - { - DebugConsole.ThrowError("Key binding for the input type \"" + inputType + " not set!"); - binding = new KeyOrMouse(Keys.D1); - } - keyMapping[(int)inputType] = binding; - } - } - } - - private void LoadKeyBinds(XElement element, Version gameVersion) - { - foreach (XAttribute attribute in element.Attributes()) - { - //backwards compatibility - if (attribute.Name.ToString() == "TakeAllFromInventorySlot") - { - keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); - keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); - } - if (!Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType)) { continue; } - - if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) - { - keyMapping[(int)inputType] = new KeyOrMouse((MouseButton)mouseButtonInt); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out MouseButton mouseButton)) - { - keyMapping[(int)inputType] = new KeyOrMouse(mouseButton); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) - { - keyMapping[(int)inputType] = new KeyOrMouse(key); - } - } - //v0.15 added creature attacks that can be used with a character capable of speaking (with mudraptor or spineling genes), - //which causes the previous attack keybind R to conflict with the radio keybind - // -> automatically change it to F - if (gameVersion < new Version(0, 15, 0, 0)) - { - keyMapping[(int)InputType.Attack] = new KeyOrMouse(Keys.F); - } - } - - private void LoadInventoryKeybinds(XElement element) - { - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - XAttribute attribute = element.Attributes().ElementAt(i); - if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) - { - inventoryKeyMapping[i] = new KeyOrMouse((MouseButton)mouseButtonInt); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out MouseButton mouseButton)) - { - inventoryKeyMapping[i] = new KeyOrMouse(mouseButton); - } - else if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) - { - inventoryKeyMapping[i] = new KeyOrMouse(key); - } - } - } - - private void LoadControls(XDocument doc) - { - var gameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0")); - - XElement keyMapping = doc.Root.Element("keymapping"); - if (keyMapping != null) - { - LoadKeyBinds(keyMapping, gameVersion); - } - - XElement inventoryKeyMapping = doc.Root.Element("inventorykeymapping"); - if (inventoryKeyMapping != null) - { - LoadInventoryKeybinds(inventoryKeyMapping); - } - - XElement debugConsoleMapping = doc.Root.Element("debugconsolemapping"); - - if (debugConsoleMapping == null) { return; } - - ConsoleKeybinds.Clear(); - DebugConsole.Keybinds.Clear(); - - foreach (XElement element in debugConsoleMapping.Elements()) - { - string keyString = element.GetAttributeString("key", string.Empty); - string command = element.GetAttributeString("command", string.Empty); - - if (string.IsNullOrWhiteSpace(keyString) || string.IsNullOrWhiteSpace(command)) { continue; } - - if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object @out) && @out is Keys key) - { - ConsoleKeybinds.TryAdd(key, command); - } - } - - DebugConsole.Keybinds = new Dictionary(ConsoleKeybinds); - } - - private void LoadSubEditorImages(XDocument doc) - { - XElement element = doc.Root?.Element("editorimages"); - if (element == null) - { - SubEditorScreen.ImageManager.Clear(alsoPending: true); - return; - } - - SubEditorScreen.ImageManager.Load(element); - } - - public KeyOrMouse KeyBind(InputType inputType) - { - return keyMapping[(int)inputType]; - } - - public string KeyBindText(InputType inputType) - { - return keyMapping[(int)inputType].Name; - } - - public KeyOrMouse InventoryKeyBind(int index) - { - return inventoryKeyMapping[index]; - } - - private GUIListBox contentPackageList; - - private bool ChangeSliderText(GUIScrollBar scrollBar, float scale) - { - UnsavedSettings = true; - GUITextBlock text = scrollBar.UserData as GUITextBlock; - //search for percentage value - int index = text.Text.IndexOf("%"); - string label = text.Text; - //if "%" is found - if (index > 0) - { - while (index > 0) - { - //search for end of label - index -= 1; - if (text.Text[index] == ' ') - break; - } - label = text.Text.Substring(0, index); - } - text.Text = label + " " + (int)Math.Round(scale * 100) + "%"; - return true; - } - - public void ResetSettingsFrame() - { - if (GameMain.Client == null) - { - VoipCapture.Instance?.Dispose(); - } - settingsFrame = null; - } - - public void CreateSettingsFrame(Tab selectedTab = Tab.Graphics) - { - RectTransform settingsHolder = null; - - if (Screen.Selected == GameMain.MainMenuScreen) - { - settingsFrame = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), GUI.Canvas, Anchor.Center)); - settingsHolder = settingsFrame.RectTransform; - } - else - { - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var settingsFrameContent = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), settingsFrame.RectTransform, Anchor.Center)); - settingsHolder = settingsFrameContent.RectTransform; - } - - Vector2 textBlockScale = new Vector2(1.0f, 0.05f); - Vector2 tickBoxScale = new Vector2(1.0f, 0.05f); - - var settingsFramePadding = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.93f), settingsHolder, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }, style: null); - var buttonArea = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.06f), settingsFramePadding.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.07f, 0.0f) }, style: null) - { - IgnoreLayoutGroups = true - }; - - /// General tab -------------------------------------------------------------- - - var leftPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 0.93f), settingsFramePadding.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - var settingsTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), - TextManager.Get("Settings"), textAlignment: Alignment.TopLeft, font: GUI.LargeFont) - { ForceUpperCase = true }; - - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), settingsTitle.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), style: "GUIBugButton") - { - ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})", - OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; } - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), TextManager.Get("ContentPackages"), font: GUI.SubHeadingFont); - - var corePackageDropdown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform)) - { - ButtonEnabled = ContentPackage.CorePackages.Count > 1 - }; - - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), isHorizontal: true) - { - Stretch = true - }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => - { - foreach (GUIComponent child in contentPackageList.Content.Children) - { - if (!(child.UserData is ContentPackage cp)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : cp.Name.ToLower().Contains(text.ToLower()); - } - return true; - }; - - contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.70f), leftPanel.RectTransform)) - { - OnSelected = (gc, obj) => false, - ScrollBarVisible = true - }; - - foreach (ContentPackage contentPackage in ContentPackage.CorePackages) - { - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), corePackageDropdown.ListBox.Content.RectTransform), style: "ListBoxElement") - { - UserData = contentPackage - }; - var text = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform), contentPackage.Name); - - if (!contentPackage.IsCompatible()) - { - frame.UserData = null; - text.TextColor = GUI.Style.Red * 0.6f; - frame.ToolTip = text.ToolTip = - TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); - } - else if (!contentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - frame.UserData = null; - text.TextColor = GUI.Style.Red * 0.6f; - frame.ToolTip = text.ToolTip = - TextManager.GetWithVariables("ContentPackageMissingCoreFiles", new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { contentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true }); - } - else if (contentPackage.HasErrors) - { - text.TextColor = new Color(255, 150, 150); - frame.ToolTip = text.ToolTip = - TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", contentPackage.Name) + - "\n" + string.Join("\n", contentPackage.ErrorMessages); - } - - if (contentPackage == CurrentCorePackage) - { - corePackageDropdown.Select(corePackageDropdown.ListBox.Content.GetChildIndex(frame)); - } - } - corePackageDropdown.OnSelected = SelectCorePackage; - corePackageDropdown.ListBox.CanBeFocused = CanHotswapPackages(true); - - foreach (ContentPackage contentPackage in ContentPackage.RegularPackages) - { - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, tickBoxScale.Y), contentPackageList.Content.RectTransform), style: "ListBoxElement") - { - UserData = contentPackage - }; - - var frameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), - style: "GUIDragIndicator") - { - CanBeFocused = false - }; - var tickBox = new GUITickBox(new RectTransform(Vector2.One, frameContent.RectTransform), contentPackage.Name, - style: "GUITickBox") - { - UserData = contentPackage, - Selected = EnabledRegularPackages.Contains(contentPackage), - OnSelected = SelectContentPackage, - Enabled = CanHotswapPackages(false) - }; - frame.RectTransform.MinSize = new Point(0, (int)(tickBox.RectTransform.MinSize.Y / frameContent.RectTransform.RelativeSize.Y)); - if (!contentPackage.IsCompatible()) - { - tickBox.Enabled = false; - tickBox.TextColor = GUI.Style.Red * 0.6f; - tickBox.ToolTip = tickBox.TextBlock.ToolTip = - TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); - } - else if (contentPackage.HasErrors) - { - tickBox.TextColor = new Color(255,150,150); - tickBox.ToolTip = tickBox.TextBlock.ToolTip = - TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", contentPackage.Name) + - "\n" + string.Join("\n", contentPackage.ErrorMessages); - } - } - contentPackageList.CurrentDragMode = CanHotswapPackages(false) ? GUIListBox.DragMode.DragWithinBox : GUIListBox.DragMode.NoDragging; - contentPackageList.CanBeFocused = CanHotswapPackages(false); - contentPackageList.OnRearranged = OnContentPackagesRearranged; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("Language"), font: GUI.SubHeadingFont); - var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform)); - foreach (string language in TextManager.AvailableLanguages) - { - languageDD.AddItem(TextManager.GetTranslatedLanguageName(language), language); - } - languageDD.SelectItem(TextManager.Language); - languageDD.OnSelected = (guiComponent, obj) => - { - string newLanguage = obj as string; - if (newLanguage == Language) { return true; } - - string prevLanguage = Language; - Language = newLanguage; - UnsavedSettings = true; - - var msgBox = new GUIMessageBox( - TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredLanguage"), - buttons: new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (btn, userdata) => - { - ApplySettings(); - GameMain.Instance.Exit(); - return true; - }; - msgBox.Buttons[1].OnClicked += (btn, userdata) => - { - Language = prevLanguage; - languageDD.SelectItem(Language); - msgBox.Close(); - return true; - }; - return true; - }; - -#if !OSX - var statisticsTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("statisticsconsenttickbox")) - { - OnSelected = (GUITickBox tickBox) => - { - GameAnalyticsManager.SetConsent( - tickBox.Selected - ? GameAnalyticsManager.Consent.Ask - : GameAnalyticsManager.Consent.No); - return false; - } - }; -#if DEBUG - statisticsTickBox.Enabled = false; -#endif - void updateGATickBoxToolTip() - => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); - updateGATickBoxToolTip(); - - var cachedConsent = GameAnalyticsManager.Consent.Unknown; - var statisticsTickBoxUpdater = new GUICustomComponent( - new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), - onUpdate: (deltaTime, component) => - { - bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes; - - bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented - || statisticsTickBox.Selected != shouldTickBoxBeSelected; - - if (!shouldUpdateTickBoxState) { return; } - - updateGATickBoxToolTip(); - cachedConsent = GameAnalyticsManager.UserConsented; - GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected; - statisticsTickBox.OnSelected = null; - statisticsTickBox.Selected = shouldTickBoxBeSelected; - statisticsTickBox.OnSelected = prevHandler; - statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; - }); -#endif - - foreach (var child in leftPanel.Children) - { - if (child is GUITextBlock textBlock) - { - textBlock.RectTransform.MinSize = new Point(textBlock.RectTransform.MinSize.X, (int)Math.Max(textBlock.RectTransform.MinSize.Y, textBlock.TextSize.Y)); - } - } - - // right panel -------------------------------------- - - var rightPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.99f - leftPanel.RectTransform.RelativeSize.X, leftPanel.RectTransform.RelativeSize.Y), - settingsFramePadding.RectTransform, Anchor.TopRight)) - { - Stretch = true - }; - - var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightPanel.RectTransform, Anchor.TopCenter), isHorizontal: true); - - var paddedFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), rightPanel.RectTransform, Anchor.Center), style: null); - - tabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length]; - tabButtons = new GUIButton[tabs.Length]; - foreach (Tab tab in Enum.GetValues(typeof(Tab))) - { - tabs[(int)tab] = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), paddedFrame.RectTransform), style: "InnerFrame") - { - UserData = tab - }; - - float tabWidth = 1.0f / tabs.Length; -#if DEBUG - string buttonText = tab != Tab.Debug ? TextManager.Get("SettingsTab." + tab.ToString()) : "Debug"; -#else - string buttonText = TextManager.Get("SettingsTab." + tab.ToString()); -#endif - - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), style: "GUITabButton") - { - UserData = tab, - OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } - }; - tabButtons[(int)tab].Text = ToolBox.LimitString(buttonText, tabButtons[(int)tab].Font, (int)(0.75f * tabWidth * tabButtonHolder.Rect.Width)); - if (tabButtons[(int)tab].Text != buttonText) - { - tabButtons[(int)tab].ToolTip = buttonText; - } - } - - /// Graphics tab -------------------------------------------------------------- - - var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Graphics].RectTransform, Anchor.TopLeft) - { RelativeOffset = new Vector2(0.025f, 0.02f) }) - { RelativeSpacing = 0.01f }; - var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Graphics].RectTransform, Anchor.TopRight) - { RelativeOffset = new Vector2(0.025f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("Resolution"), font: GUI.SubHeadingFont); - var resolutionDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)) - { - ButtonEnabled = GameMain.Config.WindowMode != WindowMode.BorderlessWindowed - }; - - var supportedDisplayModes = UpdateResolutionDD(resolutionDD); - resolutionDD.OnSelected = SelectResolution; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("DisplayMode"), font: GUI.SubHeadingFont); - var displayModeDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform)); - - displayModeDD.AddItem(TextManager.Get("Fullscreen"), WindowMode.Fullscreen); - displayModeDD.AddItem(TextManager.Get("Windowed"), WindowMode.Windowed); -#if (!OSX) - displayModeDD.AddItem(TextManager.Get("BorderlessWindowed"), WindowMode.BorderlessWindowed); - displayModeDD.SelectItem(GameMain.Config.WindowMode); -#else - // Fullscreen option will just set itself to borderless on macOS. - if (GameMain.Config.WindowMode == WindowMode.BorderlessWindowed) - { - displayModeDD.SelectItem(WindowMode.Fullscreen); - } - else - { - displayModeDD.SelectItem(GameMain.Config.WindowMode); - } -#endif - - displayModeDD.OnSelected = (guiComponent, obj) => - { - UnsavedSettings = true; - GameMain.Config.WindowMode = (WindowMode)guiComponent.UserData; - supportedDisplayModes = UpdateResolutionDD(resolutionDD); - resolutionDD.ButtonEnabled = GameMain.Config.WindowMode != WindowMode.BorderlessWindowed; - GameMain.Instance.ApplyGraphicsSettings(); - if (GameMain.Config.WindowMode == WindowMode.BorderlessWindowed) - { - GraphicsWidth = GameMain.GraphicsWidth; - GraphicsHeight = GameMain.GraphicsHeight; - var displayMode = supportedDisplayModes.Find(m => m.Width == GameMain.GraphicsWidth && m.Height == GameMain.GraphicsHeight); - if (displayMode != null) - { - resolutionDD.SelectItem(displayMode); - } - } - return true; - }; - - GUITickBox vsyncTickBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("EnableVSync")) - { - ToolTip = TextManager.Get("EnableVSyncToolTip"), - Selected = VSyncEnabled - }; - vsyncTickBox.OnSelected = (GUITickBox box) => - { - VSyncEnabled = box.Selected; - GameMain.GraphicsDeviceManager.SynchronizeWithVerticalRetrace = VSyncEnabled; - GameMain.GraphicsDeviceManager.ApplyChanges(); - UnsavedSettings = true; - - return true; - }; - - - GUITickBox textureCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("EnableTextureCompression")) - { - ToolTip = TextManager.Get("EnableTextureCompressionToolTip"), - OnSelected = (GUITickBox box) => - { - if (box.Selected == TextureCompressionEnabled) { return true; } - bool prevTextureCompressionEnabled = TextureCompressionEnabled; - TextureCompressionEnabled = box.Selected; - - var msgBox = new GUIMessageBox( - TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredGeneric"), - buttons: new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (btn, userdata) => - { - ApplySettings(); - GameMain.Instance.Exit(); - return true; - }; msgBox.Buttons[1].OnClicked += (btn, userdata) => - { - TextureCompressionEnabled = prevTextureCompressionEnabled; - box.Selected = prevTextureCompressionEnabled; - msgBox.Close(); - return true; - }; - - return true; - }, - Selected = TextureCompressionEnabled - }; - - GUITextBlock particleLimitText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("ParticleLimit"), font: GUI.SubHeadingFont, wrap: true); - GUIScrollBar particleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), style: "GUISlider", - barSize: 0.1f) - { - UserData = particleLimitText, - BarScroll = (ParticleLimit - 200) / 1300.0f, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - ParticleLimit = 200 + (int)(scroll * 1300.0f); - return true; - }, - Step = 0.1f - }; - particleScrollBar.OnMoved(particleScrollBar, particleScrollBar.BarScroll); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("LosEffect"), font: GUI.SubHeadingFont, wrap: true); - var losModeDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform)); - losModeDD.AddItem(TextManager.Get("LosModeNone"), LosMode.None); - losModeDD.AddItem(TextManager.Get("LosModeTransparent"), LosMode.Transparent); - losModeDD.AddItem(TextManager.Get("LosModeOpaque"), LosMode.Opaque); - losModeDD.SelectItem(GameMain.Config.LosMode); - losModeDD.OnSelected = (guiComponent, obj) => - { - UnsavedSettings = true; - GameMain.Config.LosMode = (LosMode)guiComponent.UserData; - //don't allow changing los mode when playing as a client - if (GameMain.Client == null) - { - GameMain.LightManager.LosMode = GameMain.Config.LosMode; - } - return true; - }; - - GUITextBlock LightText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("LightMapScale"), font: GUI.SubHeadingFont, wrap: true) - { - ToolTip = TextManager.Get("LightMapScaleToolTip") - }; - GUIScrollBar lightScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = LightText, - ToolTip = TextManager.Get("LightMapScaleToolTip"), - BarScroll = MathUtils.InverseLerp(0.2f, 1.0f, LightMapScale), - OnMoved = (scrollBar, barScroll) => - { - ChangeSliderText(scrollBar, barScroll); - LightMapScale = MathHelper.Lerp(0.2f, 1.0f, barScroll); - UnsavedSettings = true; - return true; - }, - Step = 0.25f - }; - lightScrollBar.OnMoved(lightScrollBar, lightScrollBar.BarScroll); - - /*new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("SpecularLighting")) - { - ToolTip = TextManager.Get("SpecularLightingToolTip"), - Selected = SpecularityEnabled, - OnSelected = (tickBox) => - { - SpecularityEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - };*/ - - new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("RadialDistortion")) - { - ToolTip = TextManager.Get("RadialDistortionToolTip"), - Selected = EnableRadialDistortion, - OnSelected = (tickBox) => - { - EnableRadialDistortion = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("ChromaticAberration")) - { - ToolTip = TextManager.Get("ChromaticAberrationToolTip"), - Selected = ChromaticAberrationEnabled, - OnSelected = (tickBox) => - { - ChromaticAberrationEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - /// Audio tab ---------------------------------------------------------------- - - var audioContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.Audio].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = false, - RelativeSpacing = 0.01f - }; - -#if (!OSX) - AudioDeviceNames = Alc.GetStringList((IntPtr)null, Alc.AllDevicesSpecifier); - if (string.IsNullOrEmpty(AudioOutputDevice)) - { - AudioOutputDevice = Alc.GetString((IntPtr)null, Alc.DefaultDeviceSpecifier); - if (AudioDeviceNames.Any() && !AudioDeviceNames.Any(n => n.Equals(AudioOutputDevice, StringComparison.OrdinalIgnoreCase))) - { - AudioOutputDevice = AudioDeviceNames[0]; - } - } - - var outputDeviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), audioContent.RectTransform), TrimAudioDeviceName(AudioOutputDevice), AudioDeviceNames.Count); - if (AudioDeviceNames?.Count > 0) - { - foreach (string name in AudioDeviceNames) - { - outputDeviceList.AddItem(TrimAudioDeviceName(name), name); - } - outputDeviceList.OnSelected = (GUIComponent selected, object obj) => - { - string name = obj as string; - if (!GameMain.SoundManager.Disconnected && AudioOutputDevice == name) { return true; } - - AudioOutputDevice = name; - GameMain.SoundManager.InitializeAlcDevice(AudioOutputDevice); - - return true; - }; - } - else - { - outputDeviceList.AddItem(TextManager.Get("AudioNoDevices") ?? "N/A", null); - outputDeviceList.ButtonTextColor = GUI.Style.Red; - outputDeviceList.ButtonEnabled = false; - outputDeviceList.Select(0); - } -#endif - - GUITextBlock soundVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("SoundVolume"), font: GUI.SubHeadingFont); - GUIScrollBar soundScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = soundVolumeText, - BarScroll = SoundVolume, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - SoundVolume = scroll; - return true; - } - }; - soundScrollBar.OnMoved(soundScrollBar, soundScrollBar.BarScroll); - - GUITextBlock musicVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("MusicVolume"), font: GUI.SubHeadingFont); - GUIScrollBar musicScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = musicVolumeText, - BarScroll = MusicVolume, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - MusicVolume = scroll; - return true; - } - }; - musicScrollBar.OnMoved(musicScrollBar, musicScrollBar.BarScroll); - - GUITextBlock voiceChatVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("VoiceChatVolume"), font: GUI.SubHeadingFont); - GUIScrollBar voiceChatScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = voiceChatVolumeText, - Range = new Vector2(0.0f, 2.0f) - }; - voiceChatScrollBar.BarScrollValue = VoiceChatVolume; - voiceChatScrollBar.OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scrollBar.BarScrollValue); - VoiceChatVolume = scrollBar.BarScrollValue; - return true; - }; - voiceChatScrollBar.OnMoved(voiceChatScrollBar, voiceChatScrollBar.BarScroll); - - GUITickBox muteOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("MuteOnFocusLost")) - { - Selected = MuteOnFocusLost, - ToolTip = TextManager.Get("MuteOnFocusLostToolTip"), - OnSelected = (tickBox) => - { - MuteOnFocusLost = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - GUITickBox dynamicRangeCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("DynamicRangeCompression")) - { - Selected = DynamicRangeCompressionEnabled, - ToolTip = TextManager.Get("DynamicRangeCompressionToolTip"), - OnSelected = (tickBox) => - { - DynamicRangeCompressionEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - GUITickBox voipAttenuationTickBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("VoipAttenuation")) - { - Selected = VoipAttenuationEnabled, - ToolTip = TextManager.Get("VoipAttenuationToolTip"), - OnSelected = (tickBox) => - { - VoipAttenuationEnabled = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - /// Voice chat tab ---------------------------------------------------------------- - - var voiceChatContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.VoiceChat].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = false, - RelativeSpacing = 0.01f - }; - - //new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("VoiceChat"), font: GUI.SubHeadingFont); - - CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); - foreach (string name in CaptureDeviceNames) - { - DebugConsole.NewMessage(name + " " + name.Length.ToString(), Color.Lime); - } - - GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, voiceChatContent.RectTransform), TextManager.Get("DirectionalVoiceChat")) - { - Selected = UseDirectionalVoiceChat, - ToolTip = TextManager.Get("DirectionalVoiceChatToolTip"), - OnSelected = (tickBox) => - { - UseDirectionalVoiceChat = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice) || !(CaptureDeviceNames?.Contains(VoiceCaptureDevice) ?? false)) - { - VoiceCaptureDevice = CaptureDeviceNames?.Count > 0 ? CaptureDeviceNames[0] : null; - } - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice)) - { - VoiceSetting = VoiceMode.Disabled; - } -#if (!OSX) - var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); - if (CaptureDeviceNames?.Count > 0) - { - foreach (string name in CaptureDeviceNames) - { - deviceList.AddItem(TrimAudioDeviceName(name), name); - } - deviceList.OnSelected = (GUIComponent selected, object obj) => - { - string name = obj as string; - if (!(VoipCapture.Instance?.Disconnected ?? true) && VoiceCaptureDevice == name) { return true; } - - VoipCapture.ChangeCaptureDevice(name); - return true; - }; - } - else - { - deviceList.AddItem(TextManager.Get("VoipNoDevices") ?? "N/A", null); - deviceList.ButtonTextColor = GUI.Style.Red; - deviceList.ButtonEnabled = false; - deviceList.Select(0); - } - -#else - var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), voiceChatContent.RectTransform), true, Anchor.CenterLeft); - var currentDeviceTextBlock = new GUITextBlock(new RectTransform(new Vector2(.7f, 0.75f), null), - TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("CurrentDeviceToolTip.OSX"), - TextAlignment = Alignment.CenterLeft - }; - - string refreshText = ToolBox.WrapText(TextManager.Get("RefreshDefaultDevice"), defaultDeviceGroup.RectTransform.Rect.Width * 0.3f, GUI.Font); - var currentDeviceButton = new GUIButton(new RectTransform(new Vector2(.3f, 0.75f), defaultDeviceGroup.RectTransform), refreshText) - { - ToolTip = TextManager.Get("RefreshDefaultDeviceToolTip"), - OnClicked = (bt, userdata) => - { - CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); - if (CaptureDeviceNames?.Count > 0) - { - if (VoiceCaptureDevice == CaptureDeviceNames[0]) return true; - - VoipCapture.ChangeCaptureDevice(CaptureDeviceNames[0]); - currentDeviceTextBlock.Text = TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)); - currentDeviceTextBlock.Flash(Color.Blue); - } - else - { - currentDeviceTextBlock.Text = TextManager.Get("VoipNoDevices") ?? "N/A"; - currentDeviceTextBlock.Flash(GUI.Style.Red); - } - - return true; - } - }; - currentDeviceButton.OnClicked(currentDeviceButton, null); - - currentDeviceTextBlock.RectTransform.Parent = defaultDeviceGroup.RectTransform; -#endif - - var voiceModeCount = Enum.GetNames(typeof(VoiceMode)).Length; - var voiceModeDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), elementCount: voiceModeCount); - for (int i = 0; i < voiceModeCount; i++) - { - var voiceMode = "VoiceMode." + ((VoiceMode)i).ToString(); - voiceModeDropDown.AddItem(TextManager.Get(voiceMode), userData: i, toolTip: TextManager.Get(voiceMode + "ToolTip")); - } - - var micVolumeText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("MicrophoneVolume"), font: GUI.SubHeadingFont); - var micVolumeSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = micVolumeText, - BarScroll = (float)Math.Sqrt(MathUtils.InverseLerp(0.2f, MaxMicrophoneVolume, MicrophoneVolume)), - OnMoved = (scrollBar, scroll) => - { - MicrophoneVolume = MathHelper.Lerp(0.2f, MaxMicrophoneVolume, scroll * scroll); - MicrophoneVolume = (float)Math.Round(MicrophoneVolume, 1); - ChangeSliderText(scrollBar, MicrophoneVolume); - scrollBar.Step = 0.05f; - return true; - }, - Step = 0.05f - }; - micVolumeSlider.OnMoved(micVolumeSlider, micVolumeSlider.BarScroll); - - var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), voiceChatContent.RectTransform, Anchor.BottomCenter), style: null); - - var voiceActivityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform)) - { - Visible = VoiceSetting != VoiceMode.Disabled - }; - GUITickBox localVoiceByDefault = new GUITickBox( - new RectTransform(tickBoxScale, voiceActivityGroup.RectTransform), TextManager.Get("LocalVoiceByDefault")) - { - Visible = VoiceSetting == VoiceMode.Activity, - Selected = UseLocalVoiceByDefault, - ToolTip = TextManager.Get("LocalVoiceByDefaultTooltip"), - OnSelected = (tickBox) => - { - UseLocalVoiceByDefault = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - GUITextBlock noiseGateText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), voiceActivityGroup.RectTransform), TextManager.Get("NoiseGateThreshold"), font: GUI.SubHeadingFont) - { - Visible = VoiceSetting == VoiceMode.Activity, - TextGetter = () => - { - return TextManager.Get("NoiseGateThreshold") + " " + ((int)NoiseGateThreshold).ToString() + " dB"; - } - }; - var dbMeter = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.5f), voiceActivityGroup.RectTransform), 0.0f, Color.Lime); - dbMeter.ProgressGetter = () => - { - if (VoipCapture.Instance == null) { return 0.0f; } - - if (VoiceSetting == VoiceMode.Activity) - { - dbMeter.Color = VoipCapture.Instance.LastdB > NoiseGateThreshold ? GUI.Style.Green : GUI.Style.Orange; //TODO: i'm a filthy hack - } - else - { - dbMeter.Color = Color.Lime; - } - - float scrollVal = double.IsNegativeInfinity(VoipCapture.Instance.LastdB) ? 0.0f : ((float)VoipCapture.Instance.LastdB + 100.0f) / 100.0f; - return scrollVal * scrollVal; - }; - var noiseGateSlider = new GUIScrollBar(new RectTransform(Vector2.One, dbMeter.RectTransform, Anchor.Center), color: Color.White, - style: "GUISlider", barSize: 0.03f); - noiseGateSlider.Frame.Visible = false; - noiseGateSlider.Step = 0.01f; - noiseGateSlider.Range = new Vector2(-100.0f, 0.0f); - noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, NoiseGateThreshold); - noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll; - noiseGateSlider.Visible = VoiceSetting == VoiceMode.Activity; - noiseGateSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - NoiseGateThreshold = MathHelper.Lerp(-100.0f, 0.0f, (float)Math.Sqrt(scrollBar.BarScroll)); - UnsavedSettings = true; - return true; - }; - - var voiceInputContainerHorizontal = new GUILayoutGroup( - new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform) - { - RelativeOffset = new Vector2(0.0f, voiceActivityGroup.RectTransform.RelativeSize.Y + 0.1f) - }, - isHorizontal: true) - { - Visible = VoiceSetting == VoiceMode.PushToTalk - }; - - var voiceInputContainer = new GUILayoutGroup( - new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft); - - var voiceKeybindLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice"), font: GUI.SubHeadingFont); - var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.Voice)) - { - SelectedColor = Color.Gold * 0.3f, - UserData = InputType.Voice - }; - voiceKeyBox.OnSelected += KeyBoxSelected; - - var localVoiceInputContainer = new GUILayoutGroup( - new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft); - - var localVoiceKeybindLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), localVoiceInputContainer.RectTransform), TextManager.Get("InputType.LocalVoice"), font: GUI.SubHeadingFont); - var localVoiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1.0f), localVoiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.LocalVoice)) - { - SelectedColor = Color.Gold * 0.3f, - UserData = InputType.LocalVoice - }; - localVoiceKeyBox.OnSelected += KeyBoxSelected; - - voiceKeybindLabel.RectTransform.SizeChanged += () => { GUITextBlock.AutoScaleAndNormalize(voiceKeybindLabel, localVoiceKeybindLabel); }; - - var cutoffPreventionText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("CutoffPrevention"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("CutoffPreventionTooltip") - }; - var cutoffPreventionSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = micVolumeText, - Range = new Vector2(0, ((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f * 25.0f), - Step = 1.0f / 25.0f - }; - cutoffPreventionSlider.BarScrollValue = VoiceChatCutoffPrevention; - cutoffPreventionSlider.OnMoved = (scrollBar, scroll) => - { - int bufferMsLength = (int)(((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f); - VoiceChatCutoffPrevention = (int)Math.Round(scrollBar.BarScrollValue / bufferMsLength) * bufferMsLength; - cutoffPreventionText.Text = TextManager.Get("CutoffPrevention") + - " " + TextManager.GetWithVariable("timeformatmilliseconds", "[milliseconds]", VoiceChatCutoffPrevention.ToString()); - return true; - }; - cutoffPreventionSlider.OnMoved(cutoffPreventionSlider, cutoffPreventionSlider.BarScrollValue); - - voiceModeDropDown.OnSelected = (GUIComponent selected, object userData) => - { - try - { - VoiceMode vMode = (VoiceMode)userData; - if (vMode == VoiceSetting) { return true; } - VoiceSetting = vMode; - if (vMode != VoiceMode.Disabled) - { - if (GameMain.Client == null && VoipCapture.Instance == null) - { - VoipCapture.Create(GameMain.Config.VoiceCaptureDevice); - if (VoipCapture.Instance == null) - { - VoiceSetting = vMode = VoiceMode.Disabled; - voiceActivityGroup.Visible = false; - voiceInputContainerHorizontal.Visible = false; - return true; - } - } - } - else - { - if (GameMain.Client == null) - { - VoipCapture.Instance?.Dispose(); - } - } - - noiseGateText.Visible = (vMode == VoiceMode.Activity); - noiseGateSlider.Visible = (vMode == VoiceMode.Activity); - localVoiceByDefault.Visible = (vMode == VoiceMode.Activity); - voiceActivityGroup.Visible = (vMode != VoiceMode.Disabled); - voiceInputContainerHorizontal.Visible = (vMode == VoiceMode.PushToTalk); - UnsavedSettings = true; - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to set voice capture mode.", e); - GameAnalyticsManager.AddErrorEventOnce("SetVoiceCaptureMode", GameAnalyticsManager.ErrorSeverity.Error, "Failed to set voice capture mode. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - VoiceSetting = VoiceMode.Disabled; - } - - return true; - }; - - voiceModeDropDown.Select((int)VoiceSetting); - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice)) - { - voiceModeDropDown.ButtonEnabled = false; - voiceModeDropDown.Color *= 0.5f; - voiceModeDropDown.ButtonTextColor *= 0.5f; - } - - /// Controls tab ------------------------------------------------------------- - var controlsLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), tabs[(int)Tab.Controls].RectTransform, Anchor.TopCenter) - { RelativeOffset = new Vector2(0.0f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - GUITextBlock aimAssistText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), controlsLayoutGroup.RectTransform), TextManager.Get("AimAssist"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("AimAssistToolTip") - }; - GUIScrollBar aimAssistSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), controlsLayoutGroup.RectTransform), - style: "GUISlider", barSize: 0.05f) - { - UserData = aimAssistText, - BarScroll = MathUtils.InverseLerp(0.0f, 5.0f, AimAssistAmount), - ToolTip = TextManager.Get("AimAssistToolTip"), - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - AimAssistAmount = MathHelper.Lerp(0.0f, 5.0f, scroll); - return true; - }, - Step = 0.01f - }; - aimAssistSlider.OnMoved(aimAssistSlider, aimAssistSlider.BarScroll); - - new GUITickBox(new RectTransform(tickBoxScale, controlsLayoutGroup.RectTransform), TextManager.Get("EnableMouseLook")) - { - ToolTip = TextManager.Get("EnableMouseLookToolTip"), - Selected = EnableMouseLook, - OnSelected = (tickBox) => - { - EnableMouseLook = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - var controlListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), controlsLayoutGroup.RectTransform)); - - var inputFrame = new GUILayoutGroup(new RectTransform(Vector2.One, controlListBox.Content.RectTransform), isHorizontal: true) - { Stretch = true, RelativeSpacing = 0.01f }; - - var inputColumnLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), inputFrame.RectTransform)) - { Stretch = true, RelativeSpacing = 0.005f }; - var inputColumnRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), inputFrame.RectTransform)) - { Stretch = true, RelativeSpacing = 0.005f }; - - var inputNames = Enum.GetValues(typeof(InputType)); - var inputNameBlocks = new List(); - for (int i = 0; i < inputNames.Length; i++) - { - var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f),(i <= (inputNames.Length / 2) ? inputColumnLeft : inputColumnRight).RectTransform)) - { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215) }; - var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, - TextManager.Get("InputType." + ((InputType)i)), font: GUI.SmallFont) { ForceUpperCase = true }; - inputNameBlocks.Add(inputName); - string keyText = KeyBindText((InputType)i); - var keyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), inputContainer.RectTransform), - text: keyText, font: GUI.SmallFont, style: "GUITextBoxNoIcon") - { - UserData = i - }; - keyBox.RectTransform.SizeChanged += () => - { - keyBox.Text = ToolBox.LimitString(keyText, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); - }; - inputContainer.RectTransform.MinSize = keyBox.RectTransform.MinSize; - keyBox.OnSelected += KeyBoxSelected; - keyBox.SelectedColor = Color.Gold * 0.3f; - } - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), inputColumnRight.RectTransform, minSize: inputColumnRight.Children.First().RectTransform.MinSize), style: null); - - for (int i = 0; i < inventoryHotkeyCount; i++) - { - var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f), ((i + 1) <= inventoryHotkeyCount / 2 ? inputColumnLeft : inputColumnRight).RectTransform)) - { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215), CanBeFocused = true }; - var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, - TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (i + 1).ToString()), font: GUI.SmallFont) - { ForceUpperCase = true }; - inputNameBlocks.Add(inputName); - var keyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), inputContainer.RectTransform), - text: inventoryKeyMapping[i].Name, font: GUI.SmallFont, style: "GUITextBoxNoIcon") - { - UserData = i - }; - keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); - inputContainer.RectTransform.MinSize = keyBox.RectTransform.MinSize; - keyBox.OnSelected += InventoryKeyBoxSelected; - keyBox.SelectedColor = Color.Gold * 0.3f; - } - - inputNameBlocks.First().RectTransform.SizeChanged += () => - { - GUITextBlock.AutoScaleAndNormalize(inputNameBlocks); - }; - - inputFrame.RectTransform.MinSize = new Point(0, - (int)Math.Max( - inputColumnLeft.Children.Sum(c => c.Rect.Height * (1.0f + inputColumnLeft.RelativeSpacing)), - inputColumnRight.Children.Sum(c => c.Rect.Height * (1.0f + inputColumnLeft.RelativeSpacing)))); - - var resetControlsArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), controlsLayoutGroup.RectTransform), style: null); - var resetControlsHolder = new GUILayoutGroup(new RectTransform(new Vector2(buttonArea.RectTransform.RelativeSize.X / controlsLayoutGroup.RectTransform.RelativeSize.X / rightPanel.RectTransform.RelativeSize.X, 1.0f), resetControlsArea.RectTransform, Anchor.Center), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; resetControlsHolder.CanBeFocused = true; - - var defaultBindingsButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), resetControlsHolder.RectTransform), TextManager.Get("SetDefaultBindings"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("SetDefaultBindingsToolTip"), - OnClicked = (button, data) => - { - ResetControls(legacy: false); - return true; - } - }; - - var legacyBindingsButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), resetControlsHolder.RectTransform), TextManager.Get("SetLegacyBindings"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("SetLegacyBindingsToolTip"), - OnClicked = (button, data) => - { - ResetControls(legacy: true); - return true; - } - }; - - legacyBindingsButton.TextBlock.RectTransform.SizeChanged += () => - { - GUITextBlock.AutoScaleAndNormalize(defaultBindingsButton.TextBlock, legacyBindingsButton.TextBlock); - }; - - /// Gameplay tab ------------------------------------------------------------- - var gameplaySettingsGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.46f, 0.95f), tabs[(int)Tab.Gameplay].RectTransform, Anchor.TopLeft) - { RelativeOffset = new Vector2(0.025f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), - TextManager.Get("PauseOnFocusLost")) - { - Selected = PauseOnFocusLost, - ToolTip = TextManager.Get("PauseOnFocusLostToolTip"), - OnSelected = (tickBox) => - { - PauseOnFocusLost = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - new GUITickBox(new RectTransform(tickBoxScale, gameplaySettingsGroup.RectTransform), TextManager.Get("DisableInGameHints")) - { - Selected = DisableInGameHints, - ToolTip = TextManager.Get("DisableInGameHintsToolTip"), - OnSelected = (tickBox) => - { - DisableInGameHints = tickBox.Selected; - UnsavedSettings = true; - return true; - } - }; - - new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - text: TextManager.Get("ResetInGameHints"), - style: "GUIButtonSmall") - { - OnClicked = (button, userData) => - { - var msgBox = new GUIMessageBox(TextManager.Get("ResetInGameHints"), - TextManager.Get("ResetInGameHintsTooltip"), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (button, userData) => - { - GameMain.Config.IgnoredHints.Clear(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - }; - - GUITextBlock HUDScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("HUDScale"), font: GUI.SubHeadingFont, wrap: true); - GUIScrollBar HUDScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = HUDScaleText, - BarScroll = (HUDScale - MinHUDScale) / (MaxHUDScale - MinHUDScale), - OnMoved = (scrollBar, scroll) => - { - HUDScale = MathHelper.Lerp(MinHUDScale, MaxHUDScale, scroll); - ChangeSliderText(scrollBar, HUDScale); - OnHUDScaleChanged?.Invoke(); - return true; - }, - Step = 0.02f - }; - HUDScaleScrollBar.OnMoved(HUDScaleScrollBar, HUDScaleScrollBar.BarScroll); - - GUITextBlock inventoryScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("InventoryScale"), font: GUI.SubHeadingFont); - GUIScrollBar inventoryScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = inventoryScaleText, - BarScroll = (InventoryScale - MinInventoryScale) / (MaxInventoryScale - MinInventoryScale), - OnMoved = (scrollBar, scroll) => - { - InventoryScale = MathHelper.Lerp(MinInventoryScale, MaxInventoryScale, scroll); - ChangeSliderText(scrollBar, InventoryScale); - return true; - }, - Step = 0.02f - }; - inventoryScaleScrollBar.OnMoved(inventoryScaleScrollBar, inventoryScaleScrollBar.BarScroll); - - GUITextBlock textScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), TextManager.Get("TextScale"), font: GUI.SubHeadingFont); - GUIScrollBar textScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), gameplaySettingsGroup.RectTransform), - style: "GUISlider", barSize: 0.1f) - { - UserData = textScaleText, - BarScroll = (TextScale - MinTextScale) / (MaxTextScale - MinTextScale), - OnMoved = (scrollBar, scroll) => - { - TextScale = MathHelper.Lerp(MinTextScale, MaxTextScale, scroll); - textScaleDirty = true; - ChangeSliderText(scrollBar, TextScale); - return true; - }, - Step = 0.01f - }; - textScaleScrollBar.OnMoved(textScaleScrollBar, textScaleScrollBar.BarScroll); - textScaleDirty = false; - - /// Bottom buttons ------------------------------------------------------------- - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), - TextManager.Get("Cancel")) - { - IgnoreLayoutGroups = true, - OnClicked = (x, y) => - { - static void ExitSettings() - { - if (Screen.Selected == GameMain.MainMenuScreen) { GameMain.MainMenuScreen.ReturnToMainMenu(null, null); } - GUI.SettingsMenuOpen = false; - } - - if (UnsavedSettings) - { - var msgBox = new GUIMessageBox(TextManager.Get("UnsavedChangesLabel"), - TextManager.Get("UnsavedChangesVerification"), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (applyButton, obj) => - { - LoadPlayerConfig(); - ExitSettings(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - - ExitSettings(); - return true; - } - }; - - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomCenter), - TextManager.Get("Reset")) - { - IgnoreLayoutGroups = true, - OnClicked = (button, data) => - { - var msgBox = new GUIMessageBox(TextManager.Get("SettingResetLabel"), - TextManager.Get("SettingResetVerification"), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (yesButton, obj) => - { - LoadDefaultConfig(setLanguage: false, loadContentPackages: Screen.Selected != GameMain.GameScreen); - CheckBindings(true); - RefreshItemMessages(); - ApplySettings(); - if (Screen.Selected == GameMain.MainMenuScreen) - { - GameMain.MainMenuScreen.ResetSettingsFrame(currentTab); - } - else - { - ResetSettingsFrame(); - CreateSettingsFrame(currentTab); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - }; - - applyButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight), - TextManager.Get("ApplySettingsButton")) - { - IgnoreLayoutGroups = true, - Enabled = false - }; - applyButton.OnClicked = ApplyClicked; - -#if DEBUG - /// Debug tab ---------------------------------------------------------------- - var debugTickBoxes = new GUILayoutGroup(new RectTransform(new Vector2(0.28f, 0.15f), tabs[(int)Tab.Debug].RectTransform, Anchor.TopLeft) - { RelativeOffset = new Vector2(0.02f, 0.02f) }) - { RelativeSpacing = 0.01f }; - - void addDebugTickBox(bool initialValue, Action set, string label, string tooltip) - { - var tickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), label, style: "GUITickBox"); - tickBox.Selected = initialValue; - tickBox.ToolTip = tooltip; - tickBox.OnSelected = (tickBox) => - { - set(tickBox.Selected); - UnsavedSettings = true; - return true; - }; - } - - addDebugTickBox( - AutomaticQuickStartEnabled, - (b) => AutomaticQuickStartEnabled = b, - "Automatic quickstart enabled", - "Will the game automatically move on to Quickstart when the game is launched"); - - addDebugTickBox( - TestScreenEnabled, - (b) => TestScreenEnabled = b, - "Test screen enabled", - "Will the game automatically move on to a test screen when the game is launched"); - - addDebugTickBox( - AutomaticCampaignLoadEnabled, - (b) => AutomaticCampaignLoadEnabled = b, - "Automatic campaign load enabled", - "Will the game automatically load the latest campaign save when the game is launched"); - - addDebugTickBox( - EnableSplashScreen, - (b) => EnableSplashScreen = b, - "Splash screen enabled", - "Are the splash screens shown when the game is launched"); - - addDebugTickBox( - VerboseLogging, - (b) => VerboseLogging = b, - "Verbose logging enabled", - "Should verbose logging be used"); - - addDebugTickBox( - TextManagerDebugModeEnabled, - (b) => TextManagerDebugModeEnabled = b, - "TextManager debug mode enabled", - "Does the TextManager return the text tags for debug purposes?"); - - addDebugTickBox( - ModBreakerMode, - (b) => ModBreakerMode = b, - "Mod breaker mode enabled", - "Do horrible things when loading mods to see if it breaks?"); -#endif - - UnsavedSettings = false; // Reset unsaved settings to false once the UI has been created - SelectTab(selectedTab); - } - - private List UpdateResolutionDD(GUIDropDown resolutionDD) - { - var supportedDisplayModes = new List(); - foreach (DisplayMode mode in GraphicsAdapter.DefaultAdapter.SupportedDisplayModes) - { - if (supportedDisplayModes.Any(m => m.Width == mode.Width && m.Height == mode.Height)) { continue; } -#if OSX - // Monogame currently doesn't support retina displays - // so we need to disable resolutions above the viewport size. - - // In a bundled .app you just disable HiDPI in the info.plist - // but that's probably not gonna happen. - if (mode.Width > GameMain.Instance.GraphicsDevice.DisplayMode.Width || mode.Height > GameMain.Instance.GraphicsDevice.DisplayMode.Height) { continue; } -#endif - supportedDisplayModes.Add(mode); - } - supportedDisplayModes.Sort((a, b) => - { - if (a.Width < b.Width) - { - return -1; - } - if (a.Width > b.Width) - { - return 1; - } - if (a.Height < b.Height) - { - return -1; - } - if (a.Height > b.Height) - { - return 1; - } - return 0; - }); - - resolutionDD.ClearChildren(); - - foreach (DisplayMode mode in supportedDisplayModes) - { - if (mode.Width < MinSupportedResolution.X || mode.Height < MinSupportedResolution.Y) { continue; } - resolutionDD.AddItem(mode.Width + "x" + mode.Height, mode); - if (GraphicsWidth == mode.Width && GraphicsHeight == mode.Height) resolutionDD.SelectItem(mode); - } - - if (resolutionDD.SelectedItemData == null) - { - resolutionDD.SelectItem(GraphicsAdapter.DefaultAdapter.SupportedDisplayModes.Last()); - } - - resolutionDD.ListBox.RectTransform.Resize(new Point(resolutionDD.Rect.Width, resolutionDD.Rect.Height * MathHelper.Clamp(supportedDisplayModes.Count, 2, 10))); - - return supportedDisplayModes; - } - - private string TrimAudioDeviceName(string name) - { - if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } - string[] prefixes = { "OpenAL Soft on " }; - foreach (string prefix in prefixes) - { - if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return name.Remove(0, prefix.Length); - } - } - return name; - } - - private Tab currentTab; - private void SelectTab(Tab tab) - { - switch (tab) - { - case Tab.VoiceChat: - if (VoiceSetting != VoiceMode.Disabled) - { - if (GameMain.Client == null && VoipCapture.Instance == null) - { - VoipCapture.Create(GameMain.Config.VoiceCaptureDevice); - } - } - break; - default: - if (GameMain.Client == null) - { - VoipCapture.Instance?.Dispose(); - } - break; - } - for (int i = 0; i < tabs.Length; i++) - { - tabs[i].Visible = (Tab)tabs[i].UserData == tab; - tabButtons[i].Selected = tabs[i].Visible; - } - currentTab = tab; - } - - private void KeyBoxSelected(GUITextBox textBox, Keys key) - { - textBox.Text = ""; - CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, keyMapping)); - } - - private void InventoryKeyBoxSelected(GUITextBox textBox, Keys key) - { - textBox.Text = ""; - CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, inventoryKeyMapping)); - } - - private void ResetControls(bool legacy) - { - // TODO: add a prompt? - SetDefaultBindings(legacy: legacy); - CheckBindings(true); - RefreshItemMessages(); - ApplySettings(); - if (Screen.Selected == GameMain.MainMenuScreen) - { - GameMain.MainMenuScreen.ResetSettingsFrame(Tab.Controls); - } - else - { - ResetSettingsFrame(); - CreateSettingsFrame(Tab.Controls); - } - } - - private bool SelectResolution(GUIComponent selected, object userData) - { - DisplayMode mode = selected.UserData as DisplayMode; - if (mode == null) return false; - - if (GraphicsWidth == mode.Width && GraphicsHeight == mode.Height) return false; - - GraphicsWidth = mode.Width; - GraphicsHeight = mode.Height; - GameMain.Instance.ApplyGraphicsSettings(); - UnsavedSettings = true; - - return true; - } - - private bool CanHotswapPackages(bool core) - { - return GameMain.Client == null && - (ContentPackage.IngameModSwap || - Screen.Selected != GameMain.GameScreen && - Screen.Selected != GameMain.SubEditorScreen) && - (!core || - (Screen.Selected != GameMain.CharacterEditorScreen && - Screen.Selected != GameMain.ParticleEditorScreen)); - } - - private bool SelectCorePackage(GUIComponent component, object userData) - { - if (!(userData is ContentPackage contentPackage) || GameMain.Client != null) { return false; } - - SelectCorePackage(contentPackage); - - UnsavedSettings = true; - return true; - } - - private void OnContentPackagesRearranged(GUIListBox listBox, object userData) - { - if (GameMain.Client != null) { return; } - - if (userData is ContentPackage contentPackage) - { - if (!EnabledRegularPackages.Contains(contentPackage)) { return; } - } - - ContentPackage.SortContentPackages(cp => listBox.Content.GetChildIndex(listBox.Content.GetChildByUserData(cp)), true, this); - - UnsavedSettings = true; - } - - private bool SelectContentPackage(GUITickBox tickBox) - { - if (GameMain.Client != null) { return false; } - - var contentPackage = tickBox.UserData as ContentPackage; - - if (tickBox.Selected) - { - EnableRegularPackage(contentPackage); - } - else - { - DisableRegularPackage(contentPackage); - } - - ContentPackage.SortContentPackages(cp => contentPackageList.Content.GetChildIndex(contentPackageList.Content.GetChildByUserData(cp)), false, this); - - UnsavedSettings = true; - return true; - } - - private IEnumerable WaitForKeyPress(GUITextBox keyBox, KeyOrMouse[] keyArray) - { - yield return CoroutineStatus.Running; - - while (PlayerInput.PrimaryMouseButtonHeld() || PlayerInput.PrimaryMouseButtonClicked()) - { - //wait for the mouse to be released, so that we don't interpret clicking on the textbox as the keybinding - yield return CoroutineStatus.Running; - } - while (keyBox.Selected && PlayerInput.GetKeyboardState.GetPressedKeys().Length == 0 && - !PlayerInput.LeftButtonClicked() && !PlayerInput.RightButtonClicked() && !PlayerInput.MidButtonClicked() && - !PlayerInput.Mouse4ButtonClicked() && !PlayerInput.Mouse5ButtonClicked() && !PlayerInput.MouseWheelUpClicked() && !PlayerInput.MouseWheelDownClicked()) - { - if (Screen.Selected != GameMain.MainMenuScreen && !GUI.SettingsMenuOpen) yield return CoroutineStatus.Success; - - yield return CoroutineStatus.Running; - } - - UnsavedSettings = true; - - int keyIndex = (int)keyBox.UserData; - - if (PlayerInput.LeftButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.LeftMouse); - } - else if (PlayerInput.RightButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.RightMouse); - } - else if (PlayerInput.MidButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MiddleMouse); - } - else if (PlayerInput.Mouse4ButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton4); - } - else if (PlayerInput.Mouse5ButtonClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton5); - } - else if (PlayerInput.MouseWheelUpClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelUp); - } - else if (PlayerInput.MouseWheelDownClicked()) - { - keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelDown); - } - else if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0) - { - Keys key = PlayerInput.GetKeyboardState.GetPressedKeys()[0]; - keyArray[keyIndex] = new KeyOrMouse(key); - } - else - { - yield return CoroutineStatus.Success; - } - - keyBox.Text = keyArray[keyIndex].Name; - keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, keyBox.Rect.Width); - - keyBox.Deselect(); - RefreshItemMessages(); - - yield return CoroutineStatus.Success; - } - - private void RefreshItemMessages() - { - foreach (Item item in Item.ItemList) - { - foreach (Items.Components.ItemComponent ic in item.Components) - { - ic.ParseMsg(); - } - } - CharacterHUD.ShouldRecreateHudTexts = true; - } - - private void ApplySettings() - { - SaveNewPlayerConfig(); - - SettingsFrame.Flash(GUI.Style.Green); - - if (textScaleDirty || GameMain.WindowMode != GameMain.Config.WindowMode || GameMain.Config.GraphicsWidth != GameMain.GraphicsWidth || GameMain.Config.GraphicsHeight != GameMain.GraphicsHeight) - { - GameMain.Instance.ApplyGraphicsSettings(); - textScaleDirty = false; - } - } - - private bool ApplyClicked(GUIButton button, object userData) - { - ApplySettings(); - if (Screen.Selected != GameMain.MainMenuScreen) { GUI.SettingsMenuOpen = false; } - WarnIfContentPackageSelectionDirty(); - return true; - } - - public void WarnIfContentPackageSelectionDirty() - { - if (ContentPackageSelectionDirtyNotification) - { - new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredContentPackage", fallBackTag: "RestartRequiredGeneric")); - ContentPackageSelectionDirtyNotification = false; - } - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 88d4d7135..e19ace1a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -109,7 +109,7 @@ namespace Barotrauma indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; indicatorGroup.ChildAnchor = Anchor.TopCenter; - indicatorSpriteSize = GUI.Style.GetComponentStyle("EquipmentIndicatorDivingSuit").GetDefaultSprite().size; + 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"); @@ -522,7 +522,7 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam, bool isSubInventory = false) { - if (!AccessibleWhenAlive && !character.IsDead) + if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { syncItemsDelay = Math.Max(syncItemsDelay - deltaTime, 0.0f); return; @@ -814,21 +814,21 @@ namespace Barotrauma if (conditionPercentage != -1) { - indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUI.Style.EquipmentIndicatorRunningOut, GUI.Style.EquipmentIndicatorEquipped); + indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUIStyle.EquipmentIndicatorRunningOut, GUIStyle.EquipmentIndicatorEquipped); } else { - indicators[i].Color = GUI.Style.EquipmentIndicatorRunningOut; + indicators[i].Color = GUIStyle.EquipmentIndicatorRunningOut; } } else { - indicators[i].Color = GUI.Style.EquipmentIndicatorEquipped; + indicators[i].Color = GUIStyle.EquipmentIndicatorEquipped; } } else { - indicators[i].Color = GUI.Style.EquipmentIndicatorNotEquipped; + indicators[i].Color = GUIStyle.EquipmentIndicatorNotEquipped; } } } @@ -1007,7 +1007,7 @@ namespace Barotrauma var slot = invSlots[i]; if (item.ParentInventory.GetItemAt(i) == item) { - slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); SoundPlayer.PlayUISound(GUISoundType.PickItem); break; } @@ -1033,7 +1033,7 @@ namespace Barotrauma { if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "equipconfirmation")) { return; } var equipConfirmation = new GUIMessageBox(string.Empty, TextManager.Get(item.Prefab.EquipConfirmationText), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }) + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }) { UserData = "equipconfirmation" }; @@ -1138,7 +1138,7 @@ namespace Barotrauma success = true; for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(heldItem)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); } + if (slots[j].Contains(heldItem)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } break; } @@ -1150,7 +1150,7 @@ namespace Barotrauma { for (int i = 0; i < capacity; i++) { - if (slots[i].Contains(item)) { visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); } + if (slots[i].Contains(item)) { visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } } @@ -1163,7 +1163,7 @@ namespace Barotrauma public void DrawOwn(SpriteBatch spriteBatch) { - if (!AccessibleWhenAlive && !character.IsDead) { return; } + if (!AccessibleWhenAlive && !character.IsDead && !AccessibleByOwner) { return; } if (capacity == 0) { return; } if (visualSlots == null) { CreateSlots(); } if (GameMain.GraphicsWidth != screenResolution.X || @@ -1182,7 +1182,7 @@ namespace Barotrauma CalculateBackgroundFrame(); GUI.DrawRectangle(spriteBatch, BackgroundFrame, Color.Black * 0.8f, true); GUI.DrawString(spriteBatch, - new Vector2((int)(BackgroundFrame.Center.X - GUI.Font.MeasureString(character.Name).X / 2), (int)BackgroundFrame.Y + 5), + new Vector2((int)(BackgroundFrame.Center.X - GUIStyle.Font.MeasureString(character.Name).X / 2), (int)BackgroundFrame.Y + 5), character.Name, Color.White * 0.9f); } @@ -1218,7 +1218,7 @@ namespace Barotrauma if (LimbSlotIcons.ContainsKey(SlotTypes[i])) { var icon = LimbSlotIcons[SlotTypes[i]]; - icon.Draw(spriteBatch, visualSlots[i].Rect.Center.ToVector2() + visualSlots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: visualSlots[i].Rect.Width / icon.size.X); + icon.Draw(spriteBatch, visualSlots[i].Rect.Center.ToVector2() + visualSlots[i].DrawOffset, GUIStyle.EquipmentSlotIconColor, origin: icon.size / 2, scale: visualSlots[i].Rect.Width / icon.size.X); } continue; } @@ -1292,14 +1292,14 @@ namespace Barotrauma if (Locked) { GUI.DrawRectangle(spriteBatch, inventoryArea, new Color(30,30,30,100), isFilled: true); - var lockIcon = GUI.Style.GetComponentStyle("LockIcon")?.GetDefaultSprite(); + var lockIcon = GUIStyle.GetComponentStyle("LockIcon")?.GetDefaultSprite(); lockIcon?.Draw(spriteBatch, inventoryArea.Center.ToVector2(), scale: Math.Min(inventoryArea.Height / lockIcon.size.Y * 0.7f, 1.0f)); if (inventoryArea.Contains(PlayerInput.MousePosition)) { GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("handcuffed"), new Rectangle(inventoryArea.Center - new Point(inventoryArea.Height / 2), new Point(inventoryArea.Height))); } } - else if (highlightedQuickUseSlot != null && !string.IsNullOrEmpty(highlightedQuickUseSlot.QuickUseButtonToolTip)) + else if (highlightedQuickUseSlot != null && !highlightedQuickUseSlot.QuickUseButtonToolTip.IsNullOrEmpty()) { GUIComponent.DrawToolTip(spriteBatch, highlightedQuickUseSlot.QuickUseButtonToolTip, highlightedQuickUseSlot.EquipButtonRect); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 1b8a90f60..5ed620e91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components //openState when the vertices of the convex hull were last calculated private float lastConvexHullState; - [Serialize("1,1", false, description: "The scale of the shadow-casting area of the door (relative to the actual size of the door).")] + [Serialize("1,1", IsPropertySaveable.No, description: "The scale of the shadow-casting area of the door (relative to the actual size of the door).")] public Vector2 ShadowScale { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs index 97c661961..a7229fbf9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -17,12 +17,12 @@ namespace Barotrauma.Items.Components case AreaShape.Rectangle: { RectangleF rect = GetAreaRectangle(SpawnAreaBounds, SpawnAreaOffset, draw: true); - GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Red, isFilled: false, 0f, 4f); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUIStyle.Red, isFilled: false, 0f, 4f); if (MaximumAmountRangePadding > 0f) { rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding); - GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Red, isFilled: false, 0f, 2f); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUIStyle.Red, isFilled: false, 0f, 2f); } break; } @@ -30,11 +30,11 @@ namespace Barotrauma.Items.Components Vector2 center = item.WorldPosition; center += SpawnAreaOffset; center.Y = -center.Y; - spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUI.Style.Red, thickness: 4f); + spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUIStyle.Red, thickness: 4f); if (MaximumAmountRangePadding > 0f) { - spriteBatch.DrawCircle(center, SpawnAreaRadius + MaximumAmountRangePadding, 32, GUI.Style.Red, thickness: 2f); + spriteBatch.DrawCircle(center, SpawnAreaRadius + MaximumAmountRangePadding, 32, GUIStyle.Red, thickness: 2f); } break; } @@ -46,14 +46,14 @@ namespace Barotrauma.Items.Components case AreaShape.Rectangle: { RectangleF rect = GetAreaRectangle(CrewAreaBounds, CrewAreaOffset, draw: true); - GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUI.Style.Green, isFilled: false, 0f, 4f); + GUI.DrawRectangle(spriteBatch, rect.Location, rect.Size, GUIStyle.Green, isFilled: false, 0f, 4f); break; } case AreaShape.Circle: Vector2 center = item.WorldPosition; center += CrewAreaOffset; center.Y = -center.Y; - spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUI.Style.Green); + spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUIStyle.Green); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 174fba2b0..33dafbe91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -6,17 +6,17 @@ namespace Barotrauma.Items.Components { partial class GeneticMaterial : ItemComponent { - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float TooltipValueMin { get; set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float TooltipValueMax { get; set; } - public override void AddTooltipInfo(ref string name, ref string description) + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { - if (!string.IsNullOrEmpty(materialName) && item.ContainedItems.Count() > 0) + if (!materialName.IsNullOrEmpty() && item.ContainedItems.Count() > 0) { - string mergedMaterialName = materialName; + LocalizedString mergedMaterialName = materialName; foreach (Item containedItem in item.ContainedItems) { var containedMaterial = containedItem.GetComponent(); @@ -31,30 +31,30 @@ namespace Barotrauma.Items.Components name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name); } - if (TextManager.ContainsTag("entitydescription." + Item.prefab.Identifier)) + if (TextManager.ContainsTag("entitydescription." + Item.Prefab.Identifier)) { int value = (int)MathHelper.Lerp(TooltipValueMin, TooltipValueMax, item.ConditionPercentage / 100.0f); - description = TextManager.GetWithVariable("entitydescription." + Item.prefab.Identifier, "[value]", value.ToString()); + description = TextManager.GetWithVariable("entitydescription." + Item.Prefab.Identifier, "[value]", value.ToString()); } foreach (Item containedItem in item.ContainedItems) { var containedGeneticMaterial = containedItem.GetComponent(); if (containedGeneticMaterial == null) { continue; } - string _ = string.Empty; - string containedDescription = containedItem.Description; + LocalizedString _ = string.Empty; + LocalizedString containedDescription = containedItem.Description; containedGeneticMaterial.AddTooltipInfo(ref _, ref containedDescription); - if (!string.IsNullOrEmpty(containedDescription)) + if (!containedDescription.IsNullOrEmpty()) { description += '\n' + containedDescription; } } } - public void ModifyDeconstructInfo(Deconstructor deconstructor, ref string buttonText, ref string infoText) + public void ModifyDeconstructInfo(Deconstructor deconstructor, ref LocalizedString buttonText, ref LocalizedString infoText) { if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) { - if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.prefab == item.prefab)) + if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.Prefab == item.Prefab)) { buttonText = TextManager.Get("researchstation.combine"); infoText = TextManager.Get("researchstation.combine.infotext"); @@ -74,12 +74,12 @@ namespace Barotrauma.Items.Components if (Tainted) { uint selectedTaintedEffectId = msg.ReadUInt32(); - selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedTaintedEffectId); + selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.UintIdentifier == selectedTaintedEffectId); } else { uint selectedEffectId = msg.ReadUInt32(); - selectedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedEffectId); + selectedEffect = AfflictionPrefab.Prefabs.Find(a => a.UintIdentifier == selectedEffectId); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index f5a2cda6e..67ce2148e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -10,15 +11,15 @@ namespace Barotrauma.Items.Components { internal class VineSprite { - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Rectangle SourceRect { get; private set; } - [Serialize("0.5,0.5", false)] + [Serialize("0.5,0.5", IsPropertySaveable.No)] public Vector2 Origin { get; private set; } public Vector2 AbsoluteOrigin; - public VineSprite(XElement element) + public VineSprite(ContentXElement element) { SerializableProperty.DeserializeProperties(this, element); AbsoluteOrigin = new Vector2(SourceRect.Width * Origin.X, SourceRect.Height * Origin.Y); @@ -109,28 +110,27 @@ namespace Barotrauma.Items.Components } } - partial void LoadVines(XElement element) + partial void LoadVines(ContentXElement element) { - string? vineAtlasPath = element.GetAttributeString("vineatlas", null); - string? decayAtlasPath = element.GetAttributeString("decayatlas", null); + ContentPath vineAtlasPath = element.GetAttributeContentPath("vineatlas") ?? ContentPath.Empty; + ContentPath decayAtlasPath = element.GetAttributeContentPath("decayatlas") ?? ContentPath.Empty; - if (vineAtlasPath != null) + if (!vineAtlasPath.IsNullOrEmpty()) { - VineAtlas = new Sprite(vineAtlasPath, Rectangle.Empty); + VineAtlas = new Sprite(vineAtlasPath.Value, Rectangle.Empty); } - if (decayAtlasPath != null) + if (!decayAtlasPath.IsNullOrEmpty()) { - DecayAtlas = new Sprite(decayAtlasPath, Rectangle.Empty); + DecayAtlas = new Sprite(decayAtlasPath.Value, Rectangle.Empty); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "vinesprite": - var tileType = subElement.GetAttributeString("type", null); - VineTileType type = Enum.Parse(tileType); + VineTileType type = subElement.GetAttributeEnum("type", VineTileType.Stem); VineSprites.Add(type, new VineSprite(subElement)); break; case "flowersprite": @@ -145,11 +145,11 @@ namespace Barotrauma.Items.Components leafVariants = LeafSprites.Count; } - foreach (VineTileType type in Enum.GetValues(typeof(VineTileType))) + foreach (VineTileType type in Enum.GetValues(typeof(VineTileType)).Cast()) { if (!VineSprites.ContainsKey(type)) { - DebugConsole.ThrowError($"Vine sprite missing from {item.prefab.Identifier}: {type}"); + DebugConsole.ThrowError($"Vine sprite missing from {item.Prefab.Identifier}: {type}"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 4ee0d2b1f..47a8e6089 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components item.SpriteColor * 0.5f, 0.0f, item.Scale, SpriteEffects.None, 0.0f); - GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUI.Style.Red, thickness: 3); + GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUIStyle.Red, thickness: 3); } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index ebd3e27a8..b68703a72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -21,107 +21,44 @@ namespace Barotrauma.Items.Components public Color FacialHairColor; public Color SkinColor; - public void ExtractJobPrefab(string[] tags) + public void ExtractJobPrefab(IReadOnlyDictionary tags) { - string jobIdTag = tags.FirstOrDefault(s => s.StartsWith("jobid:")); - - if (jobIdTag != null && jobIdTag.Length > 6) + if (!tags.TryGetValue("jobid".ToIdentifier(), out string jobId)) { return; } + + if (!jobId.IsNullOrEmpty()) { - string jobId = jobIdTag.Substring(6); - if (jobId != string.Empty) - { - JobPrefab = JobPrefab.Get(jobId); - } + JobPrefab = JobPrefab.Get(jobId); } } - public void ExtractAppearance(CharacterInfo characterInfo, string[] tags) + public void ExtractAppearance(CharacterInfo characterInfo, IdCard idCard) { - Gender disguisedGender = Gender.None; - Race disguisedRace = Race.None; - int disguisedHeadSpriteId = -1; - int disguisedHairIndex = -1; - int disguisedBeardIndex = -1; - int disguisedMoustacheIndex = -1; - int disguisedFaceAttachmentIndex = -1; - Color hairColor = Color.Black; - Color facialHairColor = Color.Black; - Color skinColor = Color.Black; + int disguisedHairIndex = idCard.OwnerHairIndex; + int disguisedBeardIndex = idCard.OwnerBeardIndex; + int disguisedMoustacheIndex = idCard.OwnerMoustacheIndex; + int disguisedFaceAttachmentIndex = idCard.OwnerFaceAttachmentIndex; + Color hairColor = idCard.OwnerHairColor; + Color facialHairColor = idCard.OwnerFacialHairColor; + Color skinColor = idCard.OwnerSkinColor; + var tags = idCard.OwnerTagSet; - foreach (string tag in tags) - { - string[] s = tag.Split(':'); - - switch (s[0].ToLowerInvariant()) - { - case "haircolor": - hairColor = XMLExtensions.ParseColor(s[1]); - break; - - case "facialhaircolor": - facialHairColor = XMLExtensions.ParseColor(s[1]); - break; - - case "skincolor": - skinColor = XMLExtensions.ParseColor(s[1]); - break; - - case "gender": - Enum.TryParse(s[1], ignoreCase: true, out disguisedGender); - break; - - case "race": - Enum.TryParse(s[1], ignoreCase: true, out disguisedRace); - break; - - case "headspriteid": - int.TryParse(s[1], NumberStyles.Any, CultureInfo.InvariantCulture, out disguisedHeadSpriteId); - break; - - case "hairindex": - disguisedHairIndex = int.Parse(s[1]); - break; - - case "beardindex": - disguisedBeardIndex = int.Parse(s[1]); - break; - - case "moustacheindex": - disguisedMoustacheIndex = int.Parse(s[1]); - break; - - case "faceattachmentindex": - disguisedFaceAttachmentIndex = int.Parse(s[1]); - break; - - case "sheetindex": - string[] vectorValues = s[1].Split(";"); - SheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); - break; - } - } - - if ((characterInfo.HasGenders && disguisedGender == Gender.None) - || (characterInfo.HasRaces && disguisedRace == Race.None) - || disguisedHeadSpriteId <= 0) + if ((characterInfo.HasSpecifierTags && !tags.Any())) { Portrait = null; Attachments = null; return; } - foreach (XElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) + foreach (ContentXElement limbElement in characterInfo.Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - XElement spriteElement = limbElement.Element("sprite"); + ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; - spritePath = spritePath.Replace("[GENDER]", disguisedGender.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[RACE]", disguisedRace.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId.ToString()); + spritePath = characterInfo.ReplaceVars(spritePath); string fileName = Path.GetFileNameWithoutExtension(spritePath); @@ -144,13 +81,11 @@ namespace Barotrauma.Items.Components if (characterInfo.Wearables != null) { - float baldnessChance = disguisedGender == Gender.Female ? 0.05f : 0.2f; + float baldnessChance = 0.1f; - List createElementList(WearableType wearableType, float emptyCommonness = 1.0f) + List createElementList(WearableType wearableType, float emptyCommonness = 1.0f) => CharacterInfo.AddEmpty( - characterInfo.FilterByTypeAndHeadID( - characterInfo.FilterElementsByGenderAndRace(characterInfo.Wearables, disguisedGender, disguisedRace), - wearableType, disguisedHeadSpriteId), + characterInfo.FilterElements(characterInfo.Wearables, tags, wearableType), wearableType, emptyCommonness); var disguisedHairs = createElementList(WearableType.Hair, baldnessChance); @@ -158,7 +93,7 @@ namespace Barotrauma.Items.Components var disguisedMoustaches = createElementList(WearableType.Moustache); var disguisedFaceAttachments = createElementList(WearableType.FaceAttachment); - XElement getElementFromList(List list, int index) + ContentXElement getElementFromList(List list, int index) => CharacterInfo.IsValidIndex(index, list) ? list[index] : characterInfo.GetRandomElement(list); @@ -170,9 +105,9 @@ namespace Barotrauma.Items.Components Attachments = new List(); - void loadAttachments(List attachments, XElement element, WearableType wearableType) + void loadAttachments(List attachments, ContentXElement element, WearableType wearableType) { - foreach (var s in element?.Elements("sprite") ?? Enumerable.Empty()) + foreach (var s in element?.GetChildElements("sprite") ?? Enumerable.Empty()) { attachments.Add(new WearableSprite(s, wearableType)); } @@ -185,7 +120,7 @@ namespace Barotrauma.Items.Components loadAttachments(Attachments, characterInfo.OmitJobInPortraitClothing - ? JobPrefab.NoJobElement?.Element("PortraitClothing") + ? JobPrefab.NoJobElement?.GetChildElement("PortraitClothing") : JobPrefab?.ClothingElement, WearableType.JobIndicator); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 0a3f1ddb7..39982b6cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -26,29 +26,28 @@ namespace Barotrauma.Items.Components private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); - [Serialize(1.0f, false, description: "The scale of the crosshair sprite (if there is one).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "The scale of the crosshair sprite (if there is one).")] public float CrossHairScale { get; private set; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { + string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) { case "crosshair": { - string texturePath = subElement.GetAttributeString("texture", ""); - crosshairSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairSprite = new Sprite(subElement, path: textureDir); } break; case "crosshairpointer": { - string texturePath = subElement.GetAttributeString("texture", ""); - crosshairPointerSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairPointerSprite = new Sprite(subElement, path: textureDir); } break; case "particleemitter": @@ -58,7 +57,7 @@ namespace Barotrauma.Items.Components particleEmitterCharges.Add(new ParticleEmitter(subElement)); break; case "chargesound": - chargeSound = Submarine.LoadRoundSound(subElement, false); + chargeSound = RoundSound.Load(subElement, false); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index b3057fb3e..f97cdc3be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -30,11 +30,11 @@ namespace Barotrauma.Items.Components private Color color; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { currentCrossHairPointerScale = element.GetAttributeFloat("crosshairscale", 0.1f); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components if (liquidItem == null) { return; } bool isCleaning = false; - liquidColors.TryGetValue(liquidItem.prefab.Identifier, out color); + liquidColors.TryGetValue(liquidItem.Prefab.Identifier, out color); // Ethanol or other cleaning solvent if (color.A == 0) { isCleaning = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 49d0725d2..595945e37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components public readonly RoundSound RoundSound; public readonly ActionType Type; - public string VolumeProperty; + public Identifier VolumeProperty; public float VolumeMultiplier { @@ -145,7 +145,7 @@ namespace Barotrauma.Items.Components public GUIFrame GuiFrame { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool AllowUIOverlap { get; @@ -153,21 +153,21 @@ namespace Barotrauma.Items.Components } private ItemComponent linkToUIComponent; - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string LinkUIToComponent { get; set; } - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int HudPriority { get; private set; } - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int HudLayer { get; @@ -457,14 +457,14 @@ namespace Barotrauma.Items.Components { } - private bool LoadElemProjSpecific(XElement subElement) + private bool LoadElemProjSpecific(ContentXElement subElement) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "guiframe": if (subElement.Attribute("rect") != null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - GUIFrame defined as rect, use RectTransform instead."); + DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead."); break; } GuiFrameSource = subElement; @@ -475,21 +475,18 @@ namespace Barotrauma.Items.Components break; case "itemsound": case "sound": - string filePath = subElement.GetAttributeString("file", ""); + //TODO: this validation stuff should probably go somewhere else + string filePath = subElement.GetAttributeStringUnrestricted("file", ""); - if (filePath == "") filePath = subElement.GetAttributeString("sound", ""); + if (filePath.IsNullOrEmpty()) { filePath = subElement.GetAttributeStringUnrestricted("sound", ""); } - if (filePath == "") + if (filePath.IsNullOrEmpty()) { - DebugConsole.ThrowError("Error when instantiating item \"" + item.Name + "\" - sound with no file path set"); + DebugConsole.ThrowError( + $"Error when instantiating item \"{item.Name}\" - sound with no file path set"); break; } - if (!filePath.Contains("/") && !filePath.Contains("\\") && !filePath.Contains(Path.DirectorySeparatorChar)) - { - filePath = Path.Combine(Path.GetDirectoryName(item.Prefab.FilePath), filePath); - } - ActionType type; try { @@ -501,11 +498,11 @@ namespace Barotrauma.Items.Components break; } - RoundSound sound = Submarine.LoadRoundSound(subElement); + RoundSound sound = RoundSound.Load(subElement); if (sound == null) { break; } ItemSound itemSound = new ItemSound(sound, type, subElement.GetAttributeBool("loop", false)) { - VolumeProperty = subElement.GetAttributeString("volumeproperty", "").ToLowerInvariant() + VolumeProperty = subElement.GetAttributeIdentifier("volumeproperty", "") }; if (soundSelectionModes == null) soundSelectionModes = new Dictionary(); @@ -621,6 +618,7 @@ namespace Barotrauma.Items.Components } OnResolutionChanged(); } - public virtual void AddTooltipInfo(ref string name, ref string description) { } + + public virtual void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 2154f25ea..597c90daa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -49,25 +49,25 @@ namespace Barotrauma.Items.Components /// /// Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used. /// - [Serialize(-1.0f, false, description: "Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used.")] + [Serialize(-1.0f, IsPropertySaveable.No, description: "Depth at which the contained sprites are drawn. If not set, the original depth of the item sprites is used.")] public float ContainedSpriteDepth { get; set; } - [Serialize(null, false, description: "An optional text displayed above the item's inventory.")] + [Serialize(null, IsPropertySaveable.No, description: "An optional text displayed above the item's inventory.")] public string UILabel { get; set; } public GUIComponentStyle IndicatorStyle { get; set; } - [Serialize(null, false)] + [Serialize(null, IsPropertySaveable.No)] public string ContainedStateIndicatorStyle { get; set; } - [Serialize(-1, false, description: "Can be used to make the contained state indicator display the condition of the item in a specific slot even when the container's capacity is more than 1.")] + [Serialize(-1, IsPropertySaveable.No, description: "Can be used to make the contained state indicator display the condition of the item in a specific slot even when the container's capacity is more than 1.")] public int ContainedStateIndicatorSlot { get; set; } - [Serialize(true, false, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ - "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] + [Serialize(true, IsPropertySaveable.No, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ + "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] public bool ShowContainedStateIndicator { get; set; } - [Serialize(false, false, description: "If enabled, the condition of this item is displayed in the indicator that would normally show the state of the contained items." + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the condition of this item is displayed in the indicator that would normally show the state of the contained items." + " May be useful for items such as ammo boxes and magazines that spawn projectiles as needed," + " and use the condition to determine how many projectiles can be spawned in total.")] public bool ShowConditionInContainedStateIndicator @@ -76,13 +76,13 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] + [Serialize(false, IsPropertySaveable.No, description: "If true, the contained state indicator calculates how full the item is based on the total amount of items that can be stacked inside it, as opposed to how many of the inventory slots are occupied.")] public bool ShowTotalStackCapacityInContainedStateIndicator { get; set; } - [Serialize(false, false, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the inventory of this item be kept open when the item is equipped by a character.")] public bool KeepOpenWhenEquipped { get; set; } - [Serialize(false, false, description: "Can the inventory of this item be moved around on the screen by the player.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the inventory of this item be moved around on the screen by the player.")] public bool MovableFrame { get; set; } public Vector2 DrawSize @@ -91,10 +91,10 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { slotIcons = new Sprite[capacity]; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -132,12 +132,12 @@ namespace Barotrauma.Items.Components //if neither a style or a custom sprite is defined, use default style if (ContainedStateIndicator == null) { - IndicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); + IndicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator.Default"); } } else { - IndicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); + IndicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); if (ContainedStateIndicator != null || ContainedStateIndicatorEmpty != null) { DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite..."); @@ -165,7 +165,7 @@ namespace Barotrauma.Items.Components CreateGUI(); } - containedSpriteDepths = element.GetAttributeFloatArray("containedspritedepths", new float[0]); + containedSpriteDepths = element.GetAttributeFloatArray("containedspritedepths", Array.Empty()); } protected override void CreateGUI() @@ -176,12 +176,12 @@ namespace Barotrauma.Items.Components CanBeFocused = false }; - string labelText = GetUILabel(); + LocalizedString labelText = GetUILabel(); GUITextBlock label = null; - if (!string.IsNullOrEmpty(labelText)) + if (!labelText.IsNullOrEmpty()) { label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter), - labelText, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); } float minInventoryAreaSize = 0.5f; @@ -212,12 +212,12 @@ namespace Barotrauma.Items.Components Inventory.RectTransform = guiCustomComponent.RectTransform; } - public string GetUILabel() + public LocalizedString GetUILabel() { if (UILabel == string.Empty) { return string.Empty; } if (UILabel != null) { - return TextManager.Get("UILabel." + UILabel, returnNull: true) ?? TextManager.Get(UILabel); + return TextManager.Get("UILabel." + UILabel).Fallback(TextManager.Get(UILabel)); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 8aba265ce..076f6621b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components private Vector4 padding; - [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] public Vector4 Padding { get { return padding; } @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components } private string text; - [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] + [Serialize("", IsPropertySaveable.Yes, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { get { return text; } @@ -60,7 +60,7 @@ namespace Barotrauma.Items.Components private bool ignoreLocalization; - [Editable, Serialize(false, true, "Whether or not to skip localization and always display the raw value.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, "Whether or not to skip localization and always display the raw value.")] public bool IgnoreLocalization { get => ignoreLocalization; @@ -71,13 +71,13 @@ namespace Barotrauma.Items.Components } } - public string DisplayText + public LocalizedString DisplayText { get; private set; } - [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).", alwaysUseInstanceValues: true)] + [Editable, Serialize("0,0,0,255", IsPropertySaveable.Yes, description: "The color of the text displayed on the label (R,G,B,A).", alwaysUseInstanceValues: true)] public Color TextColor { get { return textColor; } @@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] + [Editable(0.0f, 10.0f), Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { get { return textBlock == null ? 1.0f : textBlock.TextScale; } @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components } private bool scrollable; - [Serialize(false, true, description: "Should the text scroll horizontally across the item if it's too long to be displayed all at once.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the text scroll horizontally across the item if it's too long to be displayed all at once.")] public bool Scrollable { get { return scrollable; } @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(20.0f, true, description: "How fast the text scrolls across the item (only valid if Scrollable is set to true).")] + [Serialize(20.0f, IsPropertySaveable.Yes, description: "How fast the text scrolls across the item (only valid if Scrollable is set to true).")] public float ScrollSpeed { get; @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components } } - public ItemLabel(Item item, XElement element) + public ItemLabel(Item item, ContentXElement element) : base(item, element) { } @@ -148,13 +148,13 @@ namespace Barotrauma.Items.Components //(so the text can scroll entirely out of view before we reset it back to start) needsScrolling = true; float spaceWidth = textBlock.Font.MeasureChar(' ').X; - scrollingText = new string(' ', (int)Math.Ceiling(textAreaWidth / spaceWidth)) + DisplayText; + scrollingText = new string(' ', (int)Math.Ceiling(textAreaWidth / spaceWidth)) + DisplayText.Value; } else { //whole text can fit in the textblock, no need to scroll needsScrolling = false; - scrollingText = DisplayText; + scrollingText = DisplayText.Value; scrollPadding = 0; scrollAmount = 0.0f; scrollIndex = 0; @@ -176,7 +176,7 @@ namespace Barotrauma.Items.Components private void SetDisplayText(string value) { - DisplayText = IgnoreLocalization ? value : TextManager.Get(value, returnNull: true) ?? value; + DisplayText = IgnoreLocalization ? value : TextManager.Get(value).Fallback(value); TextBlock.Text = DisplayText; if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) { @@ -189,7 +189,7 @@ namespace Barotrauma.Items.Components private void RecreateTextBlock() { textBlock = new GUITextBlock(new RectTransform(item.Rect.Size), "", - textColor: textColor, font: GUI.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: !scrollable, style: null) + textColor: textColor, font: GUIStyle.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: !scrollable, style: null) { TextDepth = item.SpriteDepth - 0.00001f, RoundToNearestPixel = false, @@ -261,6 +261,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { + if (item.ParentInventory != null) { return; } if (editing) { if (!MathUtils.NearlyEqual(prevScale, item.Scale) || prevRect != item.Rect) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs index 52f925579..8911109d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Ladder.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components depth: BackgroundSpriteDepth); } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { var backgroundSpriteElement = element.GetChildElement("backgroundsprite"); if (backgroundSpriteElement != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index c2a3d7957..448c56945 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -44,17 +44,22 @@ namespace Barotrauma.Items.Components { if (ParentBody != null) { - Light.Position = ParentBody.Position; + Light.ParentBody = ParentBody; } else if (turret != null) { Light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); } + else if (item.body != null) + { + Light.ParentBody = item.body; + } else { - Light.Position = item.Position; + Light.Position = item.DrawPosition; + if (item.Submarine != null) { Light.Position -= item.Submarine.DrawPosition; } } - PhysicsBody body = ParentBody ?? item.body; + PhysicsBody body = Light.ParentBody; if (body != null) { Light.Rotation = body.Dir > 0.0f ? body.DrawRotation : body.DrawRotation - MathHelper.Pi; @@ -74,7 +79,9 @@ namespace Barotrauma.Items.Components Vector2 origin = Light.LightSprite.Origin; if ((Light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = Light.LightSprite.SourceRect.Width - origin.X; } if ((Light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = Light.LightSprite.SourceRect.Height - origin.Y; } - Light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); + + Vector2 drawPos = item.body?.DrawPosition ?? item.DrawPosition; + Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), lightColor * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index ae6905a0b..67cc5fe8b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -21,13 +21,13 @@ namespace Barotrauma.Items.Components private GUITextBlock infoArea; - [Serialize("DeconstructorDeconstruct", true)] + [Serialize("DeconstructorDeconstruct", IsPropertySaveable.Yes)] public string ActivateButtonText { get; set; } - - [Serialize("", true)] + + [Serialize("", IsPropertySaveable.Yes)] public string InfoText { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float InfoAreaWidth { get; set; } partial void InitProjSpecific(XElement element) @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.08f }; - new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleHorizontal = true @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.05f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, inputLabelArea.RectTransform), TextManager.Get("deconstructor.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, inputLabelArea.RectTransform), style: "HorizontalLine"); @@ -80,9 +80,9 @@ namespace Barotrauma.Items.Components TextBlock = { AutoScaleHorizontal = true }, OnClicked = ToggleActive }; - inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("DeconstructorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) - { + 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) + { HoverColor = Color.Black, IgnoreLayoutGroups = true, Visible = false, @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.05f }; - var outputLabel = new GUITextBlock(new RectTransform(new Vector2(0f, 1.0f), outputLabelArea.RectTransform), TextManager.Get("uilabel.output"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var outputLabel = new GUITextBlock(new RectTransform(new Vector2(0f, 1.0f), outputLabelArea.RectTransform), TextManager.Get("uilabel.output"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; outputLabel.RectTransform.Resize(new Point((int) outputLabel.Font.MeasureString(outputLabel.Text).X, outputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, outputLabelArea.RectTransform), style: "HorizontalLine"); @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components } else { - infoArea.Text = TextManager.Get(InfoText, returnNull: true) ?? InfoText; + infoArea.Text = TextManager.Get(InfoText).Fallback(InfoText); } if (IsActive) { @@ -137,11 +137,11 @@ namespace Barotrauma.Items.Components outputsFound = true; if (!string.IsNullOrEmpty(deconstructItem.ActivateButtonText)) { - string buttonText = TextManager.Get(deconstructItem.ActivateButtonText, returnNull: true) ?? deconstructItem.ActivateButtonText; - string infoText = string.Empty; + LocalizedString buttonText = TextManager.Get(deconstructItem.ActivateButtonText).Fallback(deconstructItem.ActivateButtonText); + LocalizedString infoText = string.Empty; if (!string.IsNullOrEmpty(deconstructItem.InfoText)) { - infoText = TextManager.Get(deconstructItem.InfoText, returnNull: true) ?? deconstructItem.InfoText; + infoText = TextManager.Get(deconstructItem.InfoText).Fallback(deconstructItem.InfoText); } inputItem.GetComponent()?.ModifyDeconstructInfo(this, ref buttonText, ref infoText); activateButton.Text = buttonText; @@ -159,7 +159,7 @@ namespace Barotrauma.Items.Components { if (deconstructItem.RequiredOtherItem.Any() && !string.IsNullOrEmpty(deconstructItem.InfoTextOnOtherItemMissing)) { - string missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First(), returnNull: true); + LocalizedString missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First()); infoArea.Text = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName); } } @@ -207,7 +207,7 @@ namespace Barotrauma.Items.Components overlayComponent.RectTransform.SetAsLastChild(); if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } - + if (DeconstructItemsSimultaneously) { for (int i = 0; i < InputContainer.Inventory.Capacity; i++) @@ -227,13 +227,13 @@ namespace Barotrauma.Items.Components new Rectangle( slot.Rect.X, slot.Rect.Y + (int)(slot.Rect.Height * (1.0f - progressState)), slot.Rect.Width, (int)(slot.Rect.Height * progressState)), - GUI.Style.Green * 0.5f, isFilled: true); + GUIStyle.Green * 0.5f, isFilled: true); } } public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - inSufficientPowerWarning.Visible = CurrPowerConsumption > 0 && !hasPower; + inSufficientPowerWarning.Visible = IsActive && !hasPower; } private bool ToggleActive(GUIButton button, object obj) @@ -246,7 +246,6 @@ namespace Barotrauma.Items.Components else { SetActive(!IsActive, Character.Controlled); - currPowerConsumption = IsActive ? powerConsumption : 0.0f; } return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 159e1c3f9..89280fa88 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components get { return Vector2.Zero; } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.85f, 0.65f), GuiFrame.RectTransform, Anchor.Center) { @@ -43,27 +43,27 @@ namespace Barotrauma.Items.Components powerIndicator = new GUITickBox(new RectTransform(new Vector2(0.45f, 0.8f), lightsArea.RectTransform, Anchor.Center, Pivot.CenterRight) { RelativeOffset = new Vector2(-0.05f, 0) - }, TextManager.Get("EnginePowered"), font: GUI.SubHeadingFont, style: "IndicatorLightGreen") + }, TextManager.Get("EnginePowered"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightGreen") { CanBeFocused = false }; autoControlIndicator = new GUITickBox(new RectTransform(new Vector2(0.45f, 0.8f), lightsArea.RectTransform, Anchor.Center, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0) - }, TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") + }, TextManager.Get("PumpAutoControl", "ReactorAutoControl"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightYellow") { Selected = false, Enabled = false, ToolTip = TextManager.Get("AutoControlTip") }; powerIndicator.TextBlock.Wrap = autoControlIndicator.TextBlock.Wrap = true; - powerIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - autoControlIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + autoControlIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, autoControlIndicator.TextBlock); var sliderArea = new GUIFrame(new RectTransform(new Vector2(1, 0.6f), paddedFrame.RectTransform, Anchor.BottomLeft), style: null); - string powerLabel = TextManager.Get("EngineForce"); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + LocalizedString powerLabel = TextManager.Get("EngineForce"); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), sliderArea.RectTransform, Anchor.TopCenter), "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) { AutoScaleHorizontal = true, TextGetter = () => { return TextManager.AddPunctuation(':', powerLabel, (int)(targetForce) + " %"); } @@ -90,12 +90,12 @@ namespace Barotrauma.Items.Components var textsArea = new GUIFrame(new RectTransform(new Vector2(1, 0.25f), sliderArea.RectTransform, Anchor.BottomCenter), style: null); var backwardsLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), textsArea.RectTransform, Anchor.CenterLeft), TextManager.Get("EngineBackwards"), - textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); var forwardsLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), textsArea.RectTransform, Anchor.CenterRight), TextManager.Get("EngineForwards"), - textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight); + textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); GUITextBlock.AutoScaleAndNormalize(backwardsLabel, forwardsLabel); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components Vector2 drawPos = item.DrawPosition; drawPos += PropellerPos * item.Scale; drawPos.Y = -drawPos.Y; - spriteBatch.DrawCircle(drawPos, propellerDamage.DamageRange * item.Scale, 16, GUI.Style.Red, thickness: 2); + spriteBatch.DrawCircle(drawPos, propellerDamage.DamageRange * item.Scale, 16, GUIStyle.Red, thickness: 2); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 495315dff..4a3da093d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -37,7 +37,12 @@ namespace Barotrauma.Items.Components private FabricationRecipe pendingFabricatedItem; - private (Rectangle area, string text)? tooltip; + private class ToolTip + { + public Rectangle TargetElement; + public LocalizedString Tooltip; + } + private ToolTip tooltip; private GUITextBlock requiredTimeBlock; @@ -57,7 +62,7 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter); // === LABEL === // - new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Name, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Name, font: GUIStyle.SubHeadingFont) { TextAlignment = Alignment.Center, AutoScaleVertical = true @@ -84,7 +89,7 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.03f, UserData = "filterarea" }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, AutoScaleVertical = true @@ -132,7 +137,7 @@ namespace Barotrauma.Items.Components Stretch = true, RelativeSpacing = 0.03f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", fallBackTag: "uilabel.input"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); @@ -154,7 +159,7 @@ namespace Barotrauma.Items.Components }; // === POWER WARNING === // inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("FabricatorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) + TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) { HoverColor = Color.Black, IgnoreLayoutGroups = true, @@ -168,7 +173,7 @@ namespace Barotrauma.Items.Components { itemList.Content.RectTransform.ClearChildren(); - foreach (FabricationRecipe fi in fabricationRecipes) + foreach (FabricationRecipe fi in fabricationRecipes.Values) { var frame = new GUIFrame(new RectTransform(new Point(itemList.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) { @@ -180,8 +185,8 @@ namespace Barotrauma.Items.Components var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f }; - - var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.sprite; + + var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.Sprite; if (itemIcon != null) { new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform), @@ -201,14 +206,13 @@ namespace Barotrauma.Items.Components } } - private string GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) + private LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) { if (fabricationRecipe == null) { return ""; } if (fabricationRecipe.Amount > 1) { return TextManager.GetWithVariables("fabricationrecipenamewithamount", - new string[2] { "[name]", "[amount]" }, - new string[2] { fabricationRecipe.DisplayName, fabricationRecipe.Amount.ToString() }); + ("[name]", fabricationRecipe.DisplayName), ("[amount]", fabricationRecipe.Amount.ToString())); } else { @@ -226,27 +230,6 @@ namespace Barotrauma.Items.Components partial void SelectProjSpecific(Character character) { - // TODO, This works fine as of now but if GUI.PreventElementOverlap ever gets fixed this block of code may become obsolete or detrimental. - // Only do this if there's only one linked component. If you link more containers then may - // GUI.PreventElementOverlap have mercy on your HUD layout - if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item { DisplaySideBySideWhenLinked: true }) == 1) - { - foreach (MapEntity linkedTo in item.linkedTo) - { - if (!(linkedTo is Item { DisplaySideBySideWhenLinked: true } linkedItem)) { continue; } - if (!linkedItem.Components.Any()) { continue; } - - var itemContainer = linkedItem.GetComponent(); - if (itemContainer?.GuiFrame == null || itemContainer.AllowUIOverlap) { continue; } - - // how much spacing do we want between the components - var padding = (int) (8 * GUI.Scale); - // Move the linked container to the right and move the fabricator to the left - itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(GuiFrame.Rect.Width / -2 - padding, 0); - GuiFrame.RectTransform.AbsoluteOffset = new Point(itemContainer.GuiFrame.Rect.Width / 2 + padding, 0); - } - } - var nonItems = itemList.Content.Children.Where(c => !(c.UserData is FabricationRecipe)).ToList(); nonItems.ForEach(i => itemList.Content.RemoveChild(i)); @@ -266,11 +249,11 @@ namespace Barotrauma.Items.Components return itemPlacement1 > itemPlacement2 ? -1 : 1; } - return string.Compare(item1.DisplayName, item2.DisplayName); + return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value); }); var sufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorsufficientskills"), textColor: GUI.Style.Green, font: GUI.SubHeadingFont) + TextManager.Get("fabricatorsufficientskills"), textColor: GUIStyle.Green, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, CanBeFocused = false @@ -278,8 +261,8 @@ namespace Barotrauma.Items.Components sufficientSkillsText.RectTransform.SetAsFirstChild(); var insufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUI.SubHeadingFont) - { + TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUIStyle.SubHeadingFont) + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -290,8 +273,8 @@ namespace Barotrauma.Items.Components } var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), - TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUI.SubHeadingFont) - { + TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont) + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -330,7 +313,7 @@ namespace Barotrauma.Items.Components } foreach (Item item in inputContainer.Inventory.AllItems) { - missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.prefab))); + missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.Prefab))); } var missingCounts = missingItems.GroupBy(missingItem => missingItem).ToDictionary(x => x.Key, x => x.Count()); missingItems = missingItems.Distinct().ToList(); @@ -356,10 +339,10 @@ namespace Barotrauma.Items.Components if (availableSlotIndex < 0) { continue; } if (rootInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { - inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f); } } } @@ -367,7 +350,7 @@ namespace Barotrauma.Items.Components if (slotIndex >= inputContainer.Capacity) { break; } - var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().sprite; + var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().Sprite; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; itemIcon.Draw( spriteBatch, @@ -380,9 +363,9 @@ namespace Barotrauma.Items.Components { Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); string stackCountText = "x" + missingCounts[requiredItem]; - stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) @@ -401,13 +384,13 @@ namespace Barotrauma.Items.Components GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, slotRect.Width - spacing * 2, height), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, (int)((slotRect.Width - spacing * 2) * condition), height), - GUI.Style.Green * 0.8f, true); + GUIStyle.Green * 0.8f, true); } if (slotRect.Contains(PlayerInput.MousePosition)) { var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); - string toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); + LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { @@ -428,11 +411,11 @@ namespace Barotrauma.Items.Components { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); } - if (!string.IsNullOrEmpty(requiredItem.ItemPrefabs.First().Description)) + if (!requiredItem.ItemPrefabs.First().Description.IsNullOrEmpty()) { toolTipText += '\n' + requiredItem.ItemPrefabs.First().Description; } - tooltip = (slotRect, toolTipText); + tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText }; } slotIndex++; @@ -456,12 +439,12 @@ namespace Barotrauma.Items.Components new Rectangle( slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - clampedProgressState)), slotRect.Width, (int)(slotRect.Height * clampedProgressState)), - GUI.Style.Green * 0.5f, isFilled: true); + GUIStyle.Green * 0.5f, isFilled: true); } if (outputContainer.Inventory.IsEmpty()) { - var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.sprite; + var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.Sprite; itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), @@ -472,7 +455,7 @@ namespace Barotrauma.Items.Components if (tooltip != null) { - GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.text, tooltip.Value.area); + GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement); tooltip = null; } } @@ -485,12 +468,11 @@ namespace Barotrauma.Items.Components return true; } - filter = filter.ToLower(); foreach (GUIComponent child in itemList.Content.Children) { FabricationRecipe recipe = child.UserData as FabricationRecipe; if (recipe?.DisplayName == null) { continue; } - child.Visible = recipe.DisplayName.ToLower().Contains(filter); + child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase); } HideEmptyItemListCategories(); @@ -538,24 +520,26 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; - string itemName = GetRecipeNameAndAmount(selectedItem); - string name = itemName; + LocalizedString itemName = GetRecipeNameAndAmount(selectedItem); + LocalizedString name = itemName; float quality = GetFabricatedItemQuality(selectedItem, user); if (quality > 0) { - name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n', fallBackTag: "itemname.quality3"); + name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n') + .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n')); } var nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - name, textAlignment: Alignment.TopLeft, textColor: Color.Aqua, font: GUI.SubHeadingFont, parseRichText: true) + RichString.Rich(name), textAlignment: Alignment.TopLeft, textColor: Color.Aqua, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W); if (nameBlock.TextScale < 0.7f) { - nameBlock.SetRichText(TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName, fallBackTag: "itemname.quality3")); + nameBlock.SetRichText(TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName) + .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName))); nameBlock.AutoScaleHorizontal = false; nameBlock.TextScale = 0.7f; nameBlock.Wrap = true; @@ -563,35 +547,35 @@ namespace Barotrauma.Items.Components nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale)); } - if (!string.IsNullOrWhiteSpace(selectedItem.TargetItem.Description)) + if (!selectedItem.TargetItem.Description.IsNullOrEmpty()) { var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), selectedItem.TargetItem.Description, - font: GUI.SmallFont, wrap: true); + font: GUIStyle.SmallFont, wrap: true); description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W); while (description.Rect.Height + nameBlock.Rect.Height > paddedFrame.Rect.Height) { var lines = description.WrappedText.Split('\n'); - if (lines.Length <= 1) { break; } - var newString = string.Join('\n', lines.Take(lines.Length - 1)); + if (lines.Count <= 1) { break; } + var newString = string.Join('\n', lines.Take(lines.Count - 1)); description.Text = newString.Substring(0, newString.Length - 4) + "..."; description.CalculateHeightFromText(); description.ToolTip = selectedItem.TargetItem.Description; } } - List inadequateSkills = new List(); + IEnumerable inadequateSkills = Enumerable.Empty(); if (user != null) { - inadequateSkills = selectedItem.RequiredSkills.FindAll(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); + inadequateSkills = selectedItem.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier)); } if (selectedItem.RequiredSkills.Any()) { - string text = ""; + LocalizedString text = ""; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), - TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUI.Style.Red : GUI.Style.Green, font: GUI.SubHeadingFont) + TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, }; @@ -600,7 +584,7 @@ namespace Barotrauma.Items.Components text += TextManager.Get("SkillName." + skill.Identifier) + " " + TextManager.Get("Lvl").ToLower() + " " + Math.Round(skill.Level * SkillRequirementMultiplier); if (skill != selectedItem.RequiredSkills.Last()) { text += "\n"; } } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), text, font: GUI.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), text, font: GUIStyle.SmallFont); } float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedItem.RequiredSkills); @@ -610,13 +594,13 @@ namespace Barotrauma.Items.Components (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), - TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUI.Style.Red, Color.Yellow, GUI.Style.Green), font: GUI.SubHeadingFont) + TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, }; requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); return true; } @@ -649,7 +633,7 @@ namespace Barotrauma.Items.Components if (fabricatedItem == null && !outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { - outputSlot.Flash(GUI.Style.Red); + outputSlot.Flash(GUIStyle.Red); return false; } @@ -676,7 +660,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { activateButton.Enabled = false; - inSufficientPowerWarning.Visible = currPowerConsumption > 0 && !hasPower; + inSufficientPowerWarning.Visible = IsActive && !hasPower; if (!IsActive) { @@ -723,31 +707,31 @@ namespace Barotrauma.Items.Components public void ClientWrite(IWriteMessage msg, object[] extraData = null) { - int itemIndex = pendingFabricatedItem == null ? -1 : fabricationRecipes.IndexOf(pendingFabricatedItem); - msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1); + uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; + msg.Write(recipeHash); } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { FabricatorState newState = (FabricatorState)msg.ReadByte(); float newTimeUntilReady = msg.ReadSingle(); - int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); + uint recipeHash = msg.ReadUInt32(); UInt16 userID = msg.ReadUInt16(); Character user = Entity.FindEntityByID(userID) as Character; State = newState; - if (newState == FabricatorState.Stopped || itemIndex == -1) + if (newState == FabricatorState.Stopped || recipeHash == 0) { CancelFabricating(); } else if (newState == FabricatorState.Active || newState == FabricatorState.Paused) { //if already fabricating the selected item, return - if (fabricatedItem != null && fabricationRecipes.IndexOf(fabricatedItem) == itemIndex) { return; } - if (itemIndex < 0 || itemIndex >= fabricationRecipes.Count) { return; } + if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; } + if (recipeHash == 0) { return; } - SelectItem(user, fabricationRecipes[itemIndex]); - StartFabricating(fabricationRecipes[itemIndex], user); + SelectItem(user, fabricationRecipes[recipeHash]); + StartFabricating(fabricationRecipes[recipeHash], user); } timeUntilReady = newTimeUntilReady; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index b8464aec8..a1ea10952 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components { private GUIFrame submarineContainer; - private GUIFrame hullInfoFrame; + private GUIFrame? hullInfoFrame; private GUIScissorComponent? scissorComponent; private GUIComponent? miniMapContainer; private GUIComponent miniMapFrame; @@ -160,7 +160,7 @@ namespace Barotrauma.Items.Components private GUITextBlock tooltipHeader, tooltipFirstLine, tooltipSecondLine, tooltipThirdLine; - private string noPowerTip = string.Empty; + private LocalizedString noPowerTip = string.Empty; private readonly List displayedSubs = new List(); @@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components public static readonly Color MiniMapBaseColor = new Color(15, 178, 107); private static readonly Color WetHullColor = new Color(11, 122, 205), - DoorIndicatorColor = GUI.Style.Green, + DoorIndicatorColor = GUIStyle.Green, NoPowerDoorColor = DoorIndicatorColor * 0.1f, DefaultNeutralColor = MiniMapBaseColor * 0.8f, HoverColor = Color.White, @@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components HullWaterColor = new Color(17, 173, 179) * 0.5f, HullWaterLineColor = Color.LightBlue * 0.5f, NoPowerColor = MiniMapBaseColor * 0.1f, - ElectricalBaseColor = GUI.Style.Orange, + ElectricalBaseColor = GUIStyle.Orange, NoPowerElectricalColor = ElectricalBaseColor * 0.1f; partial void InitProjSpecific() @@ -292,7 +292,7 @@ namespace Barotrauma.Items.Components } } - List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); + OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsReport && o.SymbolSprite != null && !o.Hidden).ToArray(); GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { @@ -414,7 +414,7 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList(int order = 0) { base.AddToGUIUpdateList(order); - hullInfoFrame.AddToGUIUpdateList(order: order + 1); + hullInfoFrame?.AddToGUIUpdateList(order: order + 1); if (currentMode == MiniMapMode.ItemFinder && searchBar.Selected) { searchAutoComplete?.AddToGUIUpdateList(order: order + 1); @@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components { Vector2 origin = weaponSprite.Origin; float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y); - Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUI.Style.Green; + Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green; weaponSprite.Draw(batch, center, color, origin, rotation, scale, it.SpriteEffects); } }); @@ -556,7 +556,7 @@ namespace Barotrauma.Items.Components dragMapStart = PlayerInput.MousePosition; } } - + if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) { float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); @@ -664,11 +664,11 @@ namespace Barotrauma.Items.Components { if (Voltage < MinVoltage) { - Vector2 textSize = GUI.Font.MeasureString(noPowerTip); + Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); - Color noPowerColor = GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)); + Color noPowerColor = GUIStyle.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)); - GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUIStyle.SubHeadingFont); return; } @@ -679,7 +679,7 @@ namespace Barotrauma.Items.Components spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect; - var sprite = GUI.Style.UIGlowSolidCircular?.Sprite; + var sprite = GUIStyle.UIGlowSolidCircular.Value?.Sprite; float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f; if (sprite != null) { @@ -693,7 +693,7 @@ namespace Barotrauma.Items.Components Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f; - Color color = ToolBox.GradientLerp(gap.Open, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorLow) * alpha; + Color color = ToolBox.GradientLerp(gap.Open, GUIStyle.HealthBarColorMedium, GUIStyle.HealthBarColorLow) * alpha; sprite.Draw(spriteBatch, miniMapFrame.Rect.Location.ToVector2() + entityRect.Center, color, origin: sprite.Origin, rotate: 0.0f, scale: scale); @@ -710,7 +710,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull is { } currentHull && currentHull == hull) { - Sprite? pingCircle = GUI.Style.YouAreHereCircle?.Sprite; + Sprite pingCircle = GUIStyle.YouAreHereCircle.Value.Sprite; if (pingCircle is null) { continue; } Vector2 charPos = item.WorldPosition; @@ -725,7 +725,7 @@ namespace Barotrauma.Items.Components Vector2 drawPos = component.RectComponent.Rect.Location.ToVector2() + relativePos; drawPos -= new Vector2(spriteSize, spriteSize) / 2f; - pingCircle.Draw(spriteBatch, drawPos, GUI.Style.Red * 0.8f, Vector2.Zero, 0f, parentWidth / pingCircle.size.X); + pingCircle.Draw(spriteBatch, drawPos, GUIStyle.Red * 0.8f, Vector2.Zero, 0f, parentWidth / pingCircle.size.X); } } } @@ -805,7 +805,7 @@ namespace Barotrauma.Items.Components private void CreateItemFrame(ItemPrefab prefab, RectTransform parent) { - Sprite sprite = prefab.InventoryIcon ?? prefab.sprite; + Sprite sprite = prefab.InventoryIcon ?? prefab.Sprite; if (sprite is null) { return; } GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement") { @@ -821,7 +821,7 @@ namespace Barotrauma.Items.Components Color = prefab.InventoryIconColor, UserData = prefab }; - + var nameText = new GUITextBlock(new RectTransform(Vector2.One, layout.RectTransform), prefab.Name); nameText.RectTransform.SizeChanged += () => { @@ -837,7 +837,7 @@ namespace Barotrauma.Items.Components if (first is null) { - searchBar.Flash(GUI.Style.Red); + searchBar.Flash(GUIStyle.Red); return; } searchedPrefab = first; @@ -890,7 +890,7 @@ namespace Barotrauma.Items.Components { if (item.Submarine == null) { return; } - hullInfoFrame.Visible = false; + if (hullInfoFrame != null) { hullInfoFrame.Visible = false; } reportFrame.Visible = false; searchBarFrame.Visible = false; electricalFrame.Visible = false; @@ -1029,7 +1029,7 @@ namespace Barotrauma.Items.Components { float amount = 1f + hullData.LinkedHulls.Count; gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => !g.IsRoomToRoom && !g.HiddenInGame).Sum(g => g.Open) / amount; - borderColor = Color.Lerp(neutralColor, GUI.Style.Red, Math.Min(gapOpenSum, 1.0f)); + borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f)); } bool isHoveringOver = canHoverOverHull && GUI.MouseOn == component; @@ -1037,28 +1037,28 @@ namespace Barotrauma.Items.Components // When drawing tooltip we are only interested in the component we are hovering over if (isHoveringOver) { - string header = hull.DisplayName; + LocalizedString header = hull.DisplayName; float? oxygenAmount = hullData.HullOxygenAmount, waterAmount = hullData.HullWaterAmount; - string line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; - Color line1Color = GUI.Style.Red; + LocalizedString line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; + Color line1Color = GUIStyle.Red; - string line2 = oxygenAmount == null ? + LocalizedString line2 = oxygenAmount == null ? TextManager.Get("MiniMapAirQualityUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), (int)Math.Round(oxygenAmount.Value) + "%"); - Color line2Color = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); + Color line2Color = oxygenAmount == null ? GUIStyle.Red : Color.Lerp(GUIStyle.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); - string line3 = waterAmount == null ? + LocalizedString line3 = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value * 100.0f) + "%"); - Color line3Color = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); + Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount); SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); } - bool draggingReport = GameMain.GameSession?.CrewManager?.DraggedOrder != null; + bool draggingReport = GameMain.GameSession?.CrewManager?.DraggedOrderPrefab != null; // When setting the colors we want to know the linked hulls too or else the linked hull will not realize its being hovered over and reset the border color foreach (Hull linkedHull in hullData.LinkedHulls) { @@ -1090,7 +1090,7 @@ namespace Barotrauma.Items.Components foreach (var (entity, miniMapGuiComponent) in electricalMapComponents) { if (!(entity is Item it)) { continue; } - if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent component)) { continue; } + if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; } if (entity.Removed) { @@ -1106,12 +1106,12 @@ namespace Barotrauma.Items.Components if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; } int durability = (int)(it.Condition / (it.MaxCondition / it.MaxRepairConditionMultiplier) * 100f); - Color color = ToolBox.GradientLerp(durability / 100f, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green, GUI.Style.Green); + Color color = ToolBox.GradientLerp(durability / 100f, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green, GUIStyle.Green); if (GUI.MouseOn == component) { - string line1 = string.Empty; - string line2 = string.Empty; + LocalizedString line1 = string.Empty; + LocalizedString line2 = string.Empty; if (it.GetComponent() is { } battery) { @@ -1120,13 +1120,21 @@ namespace Barotrauma.Items.Components } else if (it.GetComponent() is { } powerTransfer) { - int current = (int)-powerTransfer.CurrPowerConsumption, load = (int)powerTransfer.PowerLoad; + int current = 0, load = 0; + if (powerTransfer.PowerConnections.Count > 0 && powerTransfer.PowerConnections[0].Grid != null) + { + current = (int)powerTransfer.PowerConnections[0].Grid.Power; + load = (int)powerTransfer.PowerConnections[0].Grid.Load; + } - line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString(), fallBackTag: "statusmonitor.junctioncurrent.tooltip"); - line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip", new string[] { "[amount]", "[load]" }, new string[] { load.ToString(), load.ToString() }); + line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString()) + .Fallback(TextManager.GetWithVariable("statusmonitor.junctioncurrent.tooltip", "[amount]", current.ToString())); + line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip", + ("[amount]", load.ToString()), + ("[load]", load.ToString())); } - string line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); + LocalizedString line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); SetTooltip(component.Rect.Center, it.Prefab.Name, line1, line2, line3, line3Color: color); color = HoverColor; } @@ -1154,12 +1162,12 @@ namespace Barotrauma.Items.Components foreach (Vector2 blip in MiniMapBlips) { Vector2 parentSize = miniMapFrame.Rect.Size.ToVector2(); - Sprite pingCircle = GUI.Style.PingCircle.Sprite; + Sprite pingCircle = GUIStyle.PingCircle.Value.Sprite; Vector2 targetSize = new Vector2(parentSize.X / 4f); Vector2 spriteScale = targetSize / pingCircle.size; float scale = Math.Min(blipState, maxBlipState / 2f); float alpha = 1.0f - Math.Clamp((blipState - maxBlipState * 0.25f) * 2f, 0f, 1f); - pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUI.Style.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); + pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUIStyle.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); } } } @@ -1199,7 +1207,7 @@ namespace Barotrauma.Items.Components if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount) { - GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUI.Style.Red * 0.5f, GUI.Style.Green * 0.3f, oxygenAmount / 100.0f), true); + GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUIStyle.Red * 0.5f, GUIStyle.Green * 0.3f, oxygenAmount / 100.0f), true); } } } @@ -1210,8 +1218,9 @@ namespace Barotrauma.Items.Components spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } - private void SetTooltip(Point pos, string header, string line1, string line2, string line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null) + private void SetTooltip(Point pos, LocalizedString header, LocalizedString line1, LocalizedString line2, LocalizedString line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null) { + if (hullInfoFrame == null) { return; } hullInfoFrame.RectTransform.ScreenSpaceOffset = pos; if (hullInfoFrame.Rect.Left > submarineContainer.Rect.Right) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(submarineContainer.Rect.Right, hullInfoFrame.RectTransform.ScreenSpaceOffset.Y); } @@ -1224,13 +1233,13 @@ namespace Barotrauma.Items.Components tooltipHeader.Text = header; tooltipFirstLine.Text = line1; - tooltipFirstLine.TextColor = line1Color ?? GUI.Style.TextColor; + tooltipFirstLine.TextColor = line1Color ?? GUIStyle.TextColorNormal; tooltipSecondLine.Text = line2; - tooltipSecondLine.TextColor = line2Color ?? GUI.Style.TextColor; + tooltipSecondLine.TextColor = line2Color ?? GUIStyle.TextColorNormal; tooltipThirdLine.Text = line3; - tooltipThirdLine.TextColor = line3Color ?? GUI.Style.TextColor; + tooltipThirdLine.TextColor = line3Color ?? GUIStyle.TextColorNormal; } private void BakeSubmarine(Submarine sub, Rectangle container) @@ -1355,9 +1364,9 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession?.CrewManager is { ActiveOrders: { } orders }) { - foreach (var pair in orders) + foreach (var activeOrder in orders) { - Order order = pair.First; + Order order = activeOrder.Order; if (order is { SymbolSprite: { }, TargetEntity: Hull _ } && order.TargetEntity == hull) { cardsToDraw.Add(new MiniMapSprite(order)); @@ -1367,7 +1376,7 @@ namespace Barotrauma.Items.Components foreach (IdCard card in data.Cards) { - if (card.GetJob() is { Icon: { }} job) + if (card.OwnerJob is { Icon: { }} job) { cardsToDraw.Add(new MiniMapSprite(job)); } @@ -1415,17 +1424,17 @@ namespace Barotrauma.Items.Components if (amountLeft > 0) { string text = $"+{amountLeft}"; // TODO localization - var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable + var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable float maxWidth = Math.Max(sizeX, sizeY); Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f); - UISprite icon = GUI.Style.IconOverflowIndicator; + UISprite icon = GUIStyle.IconOverflowIndicator; if (icon != null) { const int iconPadding = 4; - icon.Draw(spriteBatch, new Rectangle((int) drawPos.X - iconPadding, (int) drawPos.Y - iconPadding, (int) maxWidth + iconPadding * 2, (int) maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); + icon.Draw(spriteBatch, new Rectangle((int)drawPos.X - iconPadding, (int)drawPos.Y - iconPadding, (int)maxWidth + iconPadding * 2, (int)maxWidth + iconPadding * 2), Color.White, SpriteEffects.None); } - GUI.DrawString(spriteBatch, drawPos, text, GUI.Style.TextColor, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, drawPos, text, GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont); } break; } @@ -1485,7 +1494,7 @@ namespace Barotrauma.Items.Components if (settings.CreateHullElements) { - hullList = Hull.hullList.Where(IsPartofSub).ToImmutableArray(); + hullList = Hull.HullList.Where(IsPartofSub).ToImmutableArray(); combinedHulls = CombinedHulls(hullList); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index d0bf73438..c65e6c41a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -19,9 +19,9 @@ namespace Barotrauma.Items.Components private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpOutEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpInEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -47,12 +47,12 @@ namespace Barotrauma.Items.Components var paddedPowerArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.8f), powerArea.RectTransform, Anchor.Center), style: "PowerButtonFrame"); var powerLightArea = new GUIFrame(new RectTransform(new Vector2(0.87f, 0.2f), powerArea.RectTransform, Anchor.TopRight), style: null); powerLight = new GUITickBox(new RectTransform(Vector2.One, powerLightArea.RectTransform, Anchor.Center), - TextManager.Get("PowerLabel"), font: GUI.SubHeadingFont, style: "IndicatorLightPower") + TextManager.Get("PowerLabel"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightPower") { CanBeFocused = false }; powerLight.TextBlock.AutoScaleHorizontal = true; - powerLight.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerLight.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); PowerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.75f), paddedPowerArea.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0, 0.1f) @@ -75,23 +75,23 @@ namespace Barotrauma.Items.Components var rightArea = new GUIFrame(new RectTransform(new Vector2(0.65f, 1), paddedFrame.RectTransform, Anchor.CenterRight), style: null); autoControlIndicator = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), rightArea.RectTransform, Anchor.TopLeft), - TextManager.Get("PumpAutoControl", fallBackTag: "ReactorAutoControl"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") + TextManager.Get("PumpAutoControl", "ReactorAutoControl"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightYellow") { Selected = false, Enabled = false, ToolTip = TextManager.Get("AutoControlTip") }; autoControlIndicator.TextBlock.AutoScaleHorizontal = true; - autoControlIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + autoControlIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); var sliderArea = new GUIFrame(new RectTransform(new Vector2(1, 0.65f), rightArea.RectTransform, Anchor.BottomLeft), style: null); var pumpSpeedText = new GUITextBlock(new RectTransform(new Vector2(1, 0.3f), sliderArea.RectTransform, Anchor.TopLeft), "", - textColor: GUI.Style.TextColor, textAlignment: Alignment.CenterLeft, wrap: false, font: GUI.SubHeadingFont) + textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterLeft, wrap: false, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; - string pumpSpeedStr = TextManager.Get("PumpSpeed"); - pumpSpeedText.TextGetter = () => { return TextManager.AddPunctuation(':', pumpSpeedStr, (int)flowPercentage + " %"); }; + LocalizedString pumpSpeedStr = TextManager.Get("PumpSpeed"); + pumpSpeedText.TextGetter = () => { return TextManager.AddPunctuation(':', pumpSpeedStr, (int)Math.Round(flowPercentage) + " %"); }; pumpSpeedSlider = new GUIScrollBar(new RectTransform(new Vector2(1, 0.35f), sliderArea.RectTransform, Anchor.Center), barSize: 0.1f, style: "DeviceSlider") { Step = 0.05f, @@ -116,9 +116,9 @@ namespace Barotrauma.Items.Components }; var textsArea = new GUIFrame(new RectTransform(new Vector2(1, 0.25f), sliderArea.RectTransform, Anchor.BottomCenter), style: null); var outLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textsArea.RectTransform, Anchor.CenterLeft), TextManager.Get("PumpOut"), - textColor: GUI.Style.TextColor, textAlignment: Alignment.CenterLeft, wrap: false, font: GUI.SubHeadingFont); + textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterLeft, wrap: false, font: GUIStyle.SubHeadingFont); var inLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textsArea.RectTransform, Anchor.CenterRight), TextManager.Get("PumpIn"), - textColor: GUI.Style.TextColor, textAlignment: Alignment.CenterRight, wrap: false, font: GUI.SubHeadingFont); + textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.CenterRight, wrap: false, font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(outLabel, inLabel); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index aa5e644f5..6627d8491 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components "ReactorWarningOverheating", "ReactorWarningHighOutput", "ReactorWarningFuelOut", "ReactorWarningSCRAM" }; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { // TODO: need to recreate the gui when the resolution changes @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components }; /*new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), inventoryContent.RectTransform), "", - textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true);*/ + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true);*/ inventoryContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), inventoryContent.RectTransform), style: null); //---------------------------------------------------------- @@ -131,28 +131,28 @@ namespace Barotrauma.Items.Components Point maxIndicatorSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)); criticalHeatWarning = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("ReactorWarningCriticalTemp"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorHeatTip") }; criticalOutputWarning = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalOutput"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("ReactorWarningCriticalOutput"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorOutputTip") }; lowTemperatureWarning = new GUITickBox(new RectTransform(new Vector2(0.4f, 1.0f), topLeftArea.RectTransform) { MaxSize = maxIndicatorSize }, - TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("ReactorWarningCriticalLowTemp"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { Selected = false, Enabled = false, ToolTip = TextManager.Get("ReactorTempTip") }; List indicatorLights = new List() { criticalHeatWarning, lowTemperatureWarning, criticalOutputWarning }; - indicatorLights.ForEach(l => l.TextBlock.OverrideTextColor(GUI.Style.TextColor)); + indicatorLights.ForEach(l => l.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal)); topLeftArea.Recalculate(); new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), columnLeft.RectTransform), style: "HorizontalLine"); @@ -167,7 +167,7 @@ namespace Barotrauma.Items.Components var rightArea = new GUIFrame(new RectTransform(new Vector2(0.49f, 1), meterArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), style: null); var fissionRateTextBox = new GUITextBlock(new RectTransform(relativeTextSize, leftArea.RectTransform, Anchor.TopCenter), - TextManager.Get("ReactorFissionRate"), textColor: GUI.Style.TextColor, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + TextManager.Get("ReactorFissionRate"), textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components }; var turbineOutputTextBox = new GUITextBlock(new RectTransform(relativeTextSize, rightArea.RectTransform, Anchor.TopCenter), - TextManager.Get("ReactorTurbineOutput"), textColor: GUI.Style.TextColor, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + TextManager.Get("ReactorTurbineOutput"), textColor: GUIStyle.TextColorNormal, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -254,7 +254,7 @@ namespace Barotrauma.Items.Components var b = new GUIButton(new RectTransform(Vector2.One, (i < 4) ? upperButtons.RectTransform : lowerButtons.RectTransform), TextManager.Get(text), style: "IndicatorButton") { - Font = GUI.SubHeadingFont, + Font = GUIStyle.SubHeadingFont, CanBeFocused = false }; warningButtons.Add(text, b); @@ -298,14 +298,14 @@ namespace Barotrauma.Items.Components AutoTempSwitch.RectTransform.MaxSize = new Point((int)(AutoTempSwitch.Rect.Height * 0.4f), int.MaxValue); autoTempLight = new GUITickBox(new RectTransform(new Vector2(0.4f, 1.0f), topRightArea.RectTransform), - TextManager.Get("ReactorAutoTemp"), font: GUI.SubHeadingFont, style: "IndicatorLightYellow") + TextManager.Get("ReactorAutoTemp"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightYellow") { ToolTip = TextManager.Get("ReactorTipAutoTemp"), CanBeFocused = false, Selected = AutoTemp }; autoTempLight.RectTransform.MaxSize = new Point(int.MaxValue, criticalHeatWarning.Rect.Height); - autoTempLight.TextBlock.OverrideTextColor(GUI.Style.TextColor); + autoTempLight.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); new GUIFrame(new RectTransform(new Vector2(0.01f, 1.0f), topRightArea.RectTransform), style: "VerticalLine"); @@ -313,14 +313,14 @@ namespace Barotrauma.Items.Components var powerArea = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), topRightArea.RectTransform), style: null); var paddedPowerArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), powerArea.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: "PowerButtonFrame"); powerLight = new GUITickBox(new RectTransform(new Vector2(0.87f, 0.3f), paddedPowerArea.RectTransform, Anchor.TopCenter, Pivot.Center), - TextManager.Get("PowerLabel"), font: GUI.SubHeadingFont, style: "IndicatorLightPower") + TextManager.Get("PowerLabel"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightPower") { CanBeFocused = false, Selected = _powerOn }; powerLight.TextBlock.Padding = new Vector4(5.0f, 0.0f, 0.0f, 0.0f); powerLight.TextBlock.AutoScaleHorizontal = true; - powerLight.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerLight.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); PowerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.75f), paddedPowerArea.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) @@ -337,7 +337,7 @@ namespace Barotrauma.Items.Components topRightArea.Recalculate(); autoTempLight.TextBlock.Padding = new Vector4(autoTempLight.TextBlock.Padding.X, 0.0f, 0.0f, 0.0f); - autoTempLight.TextBlock.Text = autoTempLight.TextBlock.Text.Replace(' ', '\n'); + autoTempLight.TextBlock.Text = autoTempLight.TextBlock.Text.Replace(" ", "\n"); autoTempLight.TextBlock.AutoScaleHorizontal = true; GUITextBlock.AutoScaleAndNormalize(indicatorLights.Select(l => l.TextBlock)); @@ -364,23 +364,23 @@ namespace Barotrauma.Items.Components relativeTextSize = new Vector2(1.0f, 0.15f); var loadText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform), - "Load", textColor: loadColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + "Load", textColor: loadColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("ReactorTipLoad") }; - string loadStr = TextManager.Get("ReactorLoad"); - string kW = TextManager.Get("kilowatt"); + LocalizedString loadStr = TextManager.Get("ReactorLoad"); + LocalizedString kW = TextManager.Get("kilowatt"); loadText.TextGetter += () => $"{loadStr.Replace("[kw]", ((int)Load).ToString())} {kW}"; - + var graph = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), graphArea.RectTransform), style: "InnerFrameRed"); new GUICustomComponent(new RectTransform(new Vector2(0.9f, 0.98f), graph.RectTransform, Anchor.Center), DrawGraph, null); var outputText = new GUITextBlock(new RectTransform(relativeTextSize, graphArea.RectTransform), - "Output", textColor: outputColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + "Output", textColor: outputColor, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("ReactorTipPower") }; - string outputStr = TextManager.Get("ReactorOutput"); + LocalizedString outputStr = TextManager.Get("ReactorOutput"); outputText.TextGetter += () => $"{outputStr.Replace("[kw]", ((int)-currPowerConsumption).ToString())} {kW}"; } @@ -610,7 +610,7 @@ namespace Barotrauma.Items.Components if (optimalRangeNormalized.X == optimalRangeNormalized.Y) { - sectorSprite.Draw(spriteBatch, pointerPos, GUI.Style.Red, MathHelper.PiOver2, scale); + sectorSprite.Draw(spriteBatch, pointerPos, GUIStyle.Red, MathHelper.PiOver2, scale); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index efda23637..8b549c0b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -56,7 +56,7 @@ namespace Barotrauma.Items.Components private Sprite sonarBlip; private Sprite lineSprite; - private readonly Dictionary> targetIcons = new Dictionary>(); + private readonly Dictionary> targetIcons = new Dictionary>(); private float displayBorderSize; @@ -130,10 +130,10 @@ namespace Barotrauma.Items.Components private bool isConnectedToSteering; - private static string caveLabel; + private static LocalizedString caveLabel; - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.Yes)] public bool RightLayout { get; @@ -143,16 +143,16 @@ namespace Barotrauma.Items.Components private bool AllowUsingMineralScanner => HasMineralScanner && !isConnectedToSteering; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); sonarBlips = new List(); - caveLabel = - TextManager.Get("cave", returnNull: true) ?? - TextManager.Get("missiontype.nest"); + caveLabel = + TextManager.Get("cave").Fallback( + TextManager.Get("missiontype.nest")); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components case "icon": var targetIconSprite = new Sprite(subElement); var color = subElement.GetAttributeColor("color", Color.White); - targetIcons.Add(subElement.GetAttributeString("identifier", ""), + targetIcons.Add(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), new Tuple(targetIconSprite, color)); break; } @@ -238,21 +238,21 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(SonarModeSwitch.RectTransform.RelativeSize.X, 0) }, style: null); passiveTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.TopLeft), - TextManager.Get("SonarPassive"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SonarPassive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { ToolTip = TextManager.Get("SonarTipPassive"), Selected = true, Enabled = false }; activeTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.BottomLeft), - TextManager.Get("SonarActive"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SonarActive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { ToolTip = TextManager.Get("SonarTipActive"), Selected = false, Enabled = false }; - passiveTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); - activeTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); + passiveTickBox.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + activeTickBox.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); textBlocksToScaleAndNormalize.Clear(); textBlocksToScaleAndNormalize.Add(passiveTickBox.TextBlock); @@ -261,7 +261,7 @@ namespace Barotrauma.Items.Components lowerAreaFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.BottomCenter), style: null); var zoomContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.TopCenter), style: null); var zoomText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.6f), zoomContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get("SonarZoom"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight); + TextManager.Get("SonarZoom"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); textBlocksToScaleAndNormalize.Add(zoomText); zoomSlider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 0.8f), zoomContainer.RectTransform, Anchor.CenterLeft) { @@ -299,7 +299,7 @@ namespace Barotrauma.Items.Components } }; var directionalModeSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), directionalModeFrame.RectTransform, Anchor.CenterRight), - TextManager.Get("SonarDirectionalPing"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); + TextManager.Get("SonarDirectionalPing"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(directionalModeSwitchText); if (AllowUsingMineralScanner) @@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components (spriteBatch, guiCustomComponent) => { DrawSonar(spriteBatch, guiCustomComponent.Rect); }, null); signalWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), sonarView.RectTransform, Anchor.Center, Pivot.BottomCenter), - "", warningColor, GUI.LargeFont, Alignment.Center); + "", warningColor, GUIStyle.LargeFont, Alignment.Center); // Setup layout for nav terminal if (isConnectedToSteering || RightLayout) @@ -421,7 +421,7 @@ namespace Barotrauma.Items.Components } }; var mineralScannerSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), mineralScannerFrame.RectTransform, Anchor.CenterRight), - TextManager.Get("SonarMineralScanner"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); + TextManager.Get("SonarMineralScanner"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(mineralScannerSwitchText); } @@ -570,7 +570,7 @@ namespace Barotrauma.Items.Components { levelTriggerFlows.Add(trigger, flow); } - if (!string.IsNullOrWhiteSpace(trigger.InfectIdentifier) && + if (!trigger.InfectIdentifier.IsEmpty && Vector2.DistanceSquared(transducerCenter, trigger.WorldPosition) < pingRange / 2 * pingRange / 2) { ballastFloraSpores.Add(trigger); @@ -918,12 +918,12 @@ namespace Barotrauma.Items.Components foreach (AITarget aiTarget in AITarget.List) { if (aiTarget.InDetectable) { continue; } - if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) { continue; } + if (aiTarget.SonarLabel.IsNullOrEmpty() || aiTarget.SoundRange <= 0.0f) { continue; } if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) { DrawMarker(spriteBatch, - aiTarget.SonarLabel, + aiTarget.SonarLabel.Value, aiTarget.SonarIconIdentifier, aiTarget, aiTarget.WorldPosition, transducerCenter, @@ -937,7 +937,7 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, - Level.Loaded.StartOutpost != null ? "outpost" : "location", + (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(), Level.Loaded.StartLocation.Name, Level.Loaded.StartExitPosition, transducerCenter, displayScale, center, DisplayRadius); @@ -947,7 +947,7 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, - Level.Loaded.EndOutpost != null ? "outpost" : "location", + (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(), Level.Loaded.EndLocation.Name, Level.Loaded.EndExitPosition, transducerCenter, displayScale, center, DisplayRadius); @@ -958,8 +958,8 @@ namespace Barotrauma.Items.Components var cave = Level.Loaded.Caves[i]; if (!cave.DisplayOnSonar) { continue; } DrawMarker(spriteBatch, - caveLabel, - "cave", + caveLabel.Value, + "cave".ToIdentifier(), "cave" + i, cave.StartPos.ToVector2(), transducerCenter, displayScale, center, DisplayRadius); @@ -968,13 +968,13 @@ namespace Barotrauma.Items.Components int missionIndex = 0; foreach (Mission mission in GameMain.GameSession.Missions) { - if (!string.IsNullOrWhiteSpace(mission.SonarLabel)) + if (!mission.SonarLabel.IsNullOrWhiteSpace()) { int i = 0; foreach (Vector2 sonarPosition in mission.SonarPositions) { DrawMarker(spriteBatch, - mission.SonarLabel, + mission.SonarLabel.Value, mission.SonarIconIdentifier, "mission" + missionIndex + ":" + i, sonarPosition, transducerCenter, @@ -995,7 +995,7 @@ namespace Barotrauma.Items.Components var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } DrawMarker(spriteBatch, - i.Name, "mineral", "mineralcluster" + i, + i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, c.center, transducerCenter, displayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); @@ -1022,8 +1022,8 @@ namespace Barotrauma.Items.Components } DrawMarker(spriteBatch, - sub.Info.DisplayName, - sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", + sub.Info.DisplayName.Value, + (sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine").ToIdentifier(), sub, sub.WorldPosition, transducerCenter, displayScale, center, DisplayRadius * 0.95f); @@ -1611,7 +1611,7 @@ namespace Barotrauma.Items.Components sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f * blip.Alpha, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); } - private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, + private void DrawMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, bool onlyShowTextOnMouseOver = false) { float linearDist = Vector2.Distance(worldPosition, transducerPosition); @@ -1704,11 +1704,11 @@ namespace Barotrauma.Items.Components if (alpha <= 0.0f) { return; } - string wrappedLabel = ToolBox.WrapText(label, 150, GUI.SmallFont); + string wrappedLabel = ToolBox.WrapText(label, 150, GUIStyle.SmallFont.Value); wrappedLabel += "\n" + ((int)(dist * Physics.DisplayToRealWorldRatio) + " m"); Vector2 labelPos = markerPos; - Vector2 textSize = GUI.SmallFont.MeasureString(wrappedLabel); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(wrappedLabel); //flip the text to left side when the marker is on the left side or goes outside the right edge of the interface if (GuiFrame != null && (dir.X < 0.0f || labelPos.X + textSize.X + 10 > GuiFrame.Rect.X) && labelPos.X - textSize.X > 0) @@ -1720,7 +1720,7 @@ namespace Barotrauma.Items.Components new Vector2(labelPos.X + 10, labelPos.Y), wrappedLabel, Color.LightBlue * textAlpha * alpha, Color.Black * textAlpha * 0.8f * alpha, - 2, GUI.SmallFont); + 2, GUIStyle.SmallFont); } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index ee079c02a..2d2786db4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private bool dockingNetworkMessagePending; private GUIButton dockingButton; - private string dockText, undockText; + private LocalizedString dockText, undockText; private GUIComponent steerArea; @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components private GUITextBlock tipContainer; - private string noPowerTip, autoPilotMaintainPosTip, autoPilotLevelStartTip, autoPilotLevelEndTip; + private LocalizedString noPowerTip, autoPilotMaintainPosTip, autoPilotLevelStartTip, autoPilotLevelEndTip; private Sprite maintainPosIndicator, maintainPosOriginIndicator; private Sprite steeringIndicator; @@ -90,9 +90,9 @@ namespace Barotrauma.Items.Components } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -141,26 +141,26 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(steeringModeSwitch.RectTransform.RelativeSize.X, 0) }, style: null); manualPilotIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), steeringModeRightSide.RectTransform, Anchor.TopLeft), - TextManager.Get("SteeringManual"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SteeringManual"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { Selected = !autoPilot, Enabled = false }; autopilotIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), steeringModeRightSide.RectTransform, Anchor.BottomLeft), - TextManager.Get("SteeringAutoPilot"), font: GUI.SubHeadingFont, style: "IndicatorLightRedSmall") + TextManager.Get("SteeringAutoPilot"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall") { Selected = autoPilot, Enabled = false }; - manualPilotIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - autopilotIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + manualPilotIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + autopilotIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(manualPilotIndicator.TextBlock, autopilotIndicator.TextBlock); var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), - TextManager.Get("SteeringMaintainPos"), font: GUI.SmallFont, style: "GUIRadioButton") + TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = maintainPos, @@ -194,10 +194,10 @@ namespace Barotrauma.Items.Components return true; } }; - int textLimit = (int)(MathHelper.Clamp(25 * GUI.xScale, 15, 35)); + int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f); levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), - GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, textLimit), - font: GUI.SmallFont, style: "GUIRadioButton") + GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), + font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelStartSelected, @@ -223,8 +223,8 @@ namespace Barotrauma.Items.Components }; levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.BottomCenter), - (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, textLimit), - font: GUI.SmallFont, style: "GUIRadioButton") + (GameMain.GameSession?.EndLocation == null || Level.IsLoadedOutpost) ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, GUIStyle.SmallFont, textLimit), + font: GUIStyle.SmallFont, style: "GUIRadioButton") { Enabled = autoPilot, Selected = levelEndSelected, @@ -290,7 +290,7 @@ namespace Barotrauma.Items.Components leftElements.Add(left); centerElements.Add(center); rightElements.Add(right); - string leftText = string.Empty, centerText = string.Empty; + LocalizedString leftText = string.Empty, centerText = string.Empty; GUITextBlock.TextGetterHandler rightTextGetter = null; switch (i) { @@ -325,10 +325,10 @@ namespace Barotrauma.Items.Components }; break; } - new GUITextBlock(new RectTransform(Vector2.One, left.RectTransform), leftText, font: GUI.SubHeadingFont, wrap: leftText.Contains(' '), textAlignment: Alignment.CenterRight); - new GUITextBlock(new RectTransform(Vector2.One, center.RectTransform), centerText, font: GUI.Font, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(Vector2.One, left.RectTransform), leftText, font: GUIStyle.SubHeadingFont, wrap: leftText.Contains(" "), textAlignment: Alignment.CenterRight); + new GUITextBlock(new RectTransform(Vector2.One, center.RectTransform), centerText, font: GUIStyle.Font, textAlignment: Alignment.Center) { Padding = Vector4.Zero }; var digitalFrame = new GUIFrame(new RectTransform(Vector2.One, right.RectTransform), style: "DigitalFrameDark"); - new GUITextBlock(new RectTransform(Vector2.One * 0.85f, digitalFrame.RectTransform, Anchor.Center), "12345", GUI.Style.TextColorDark, GUI.DigitalFont, Alignment.CenterRight) + new GUITextBlock(new RectTransform(Vector2.One * 0.85f, digitalFrame.RectTransform, Anchor.Center), "12345", GUIStyle.TextColorDark, GUIStyle.DigitalFont, Alignment.CenterRight) { TextGetter = rightTextGetter }; @@ -345,8 +345,8 @@ namespace Barotrauma.Items.Components RelativeOffset = new Vector2(Sonar.controlBoxOffset.X + 0.05f, -0.05f) }, style: null); - dockText = TextManager.Get("label.navterminaldock", fallBackTag: "captain.dock"); - undockText = TextManager.Get("label.navterminalundock", fallBackTag: "captain.undock"); + dockText = TextManager.Get("label.navterminaldock", "captain.dock"); + undockText = TextManager.Get("label.navterminalundock", "captain.undock"); dockingButton = new GUIButton(new RectTransform(new Vector2(elementScale), dockingContainer.RectTransform, Anchor.Center), dockText, style: "PowerButton") { OnClicked = (btn, userdata) => @@ -376,13 +376,13 @@ namespace Barotrauma.Items.Components enterOutpostPrompt = new GUIMessageBox( TextManager.GetWithVariable("enterlocation", "[locationname]", DockingTarget.Item.Submarine.Info.Name), TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); } else { enterOutpostPrompt = new GUIMessageBox("", TextManager.GetWithVariable("campaignenteroutpostprompt", "[locationname]", DockingTarget.Item.Submarine.Info.Name), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); } enterOutpostPrompt.Buttons[0].OnClicked += (btn, userdata) => { @@ -411,11 +411,11 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } } - dockingButton.Font = GUI.SubHeadingFont; + dockingButton.Font = GUIStyle.SubHeadingFont; dockingButton.TextBlock.RectTransform.MaxSize = new Point((int)(dockingButton.Rect.Width * 0.7f), int.MaxValue); dockingButton.TextBlock.AutoScaleHorizontal = true; - var style = GUI.Style.GetComponentStyle("DockingButtonUp"); + var style = GUIStyle.GetComponentStyle("DockingButtonUp"); Sprite buttonSprite = style.Sprites.FirstOrDefault().Value.FirstOrDefault()?.Sprite; Point buttonSize = buttonSprite != null ? buttonSprite.size.ToPoint() : new Point(149, 52); Point horizontalButtonSize = buttonSize.Multiply(elementScale * GUI.Scale * dockingButtonSize); @@ -447,18 +447,18 @@ namespace Barotrauma.Items.Components steerRadius = steerArea.Rect.Width / 2; iceSpireWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), - TextManager.Get("NavTerminalIceSpireWarning"), GUI.Style.Red, GUI.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f, wrap: true) + TextManager.Get("NavTerminalIceSpireWarning"), GUIStyle.Red, GUIStyle.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f, wrap: true) { Visible = false }; pressureWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), - TextManager.Get("SteeringDepthWarning"), GUI.Style.Red, GUI.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f) + TextManager.Get("SteeringDepthWarning"), GUIStyle.Red, GUIStyle.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f) { Visible = false }; // Tooltip/helper text tipContainer = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), steerArea.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) - , "", font: GUI.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center) + , "", font: GUIStyle.Font, wrap: true, style: "GUIToolTip", textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; @@ -524,7 +524,7 @@ namespace Barotrauma.Items.Components if (velRect.Contains(PlayerInput.MousePosition)) { - GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 4, (int)steeringInputPos.Y - 4, 8, 8), GUI.Style.Red, thickness: 2); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringInputPos.X - 4, (int)steeringInputPos.Y - 4, 8, 8), GUIStyle.Red, thickness: 2); } } else if (posToMaintain.HasValue && !LevelStartSelected && !LevelEndSelected) @@ -537,7 +537,7 @@ namespace Barotrauma.Items.Components displayPosToMaintain = displayPosToMaintain.ClampLength(velRect.Width / 2); displayPosToMaintain = steerArea.Rect.Center.ToVector2() + displayPosToMaintain; - Color crosshairColor = GUI.Style.Orange * (0.5f + ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 4.0f); + Color crosshairColor = GUIStyle.Orange * (0.5f + ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 4.0f); if (maintainPosIndicator != null) { maintainPosIndicator.Draw(spriteBatch, displayPosToMaintain, crosshairColor, scale: 0.5f * sonar.Zoom); @@ -551,11 +551,11 @@ namespace Barotrauma.Items.Components if (maintainPosOriginIndicator != null) { - maintainPosOriginIndicator.Draw(spriteBatch, steeringOrigin, GUI.Style.Orange, scale: 0.5f * sonar.Zoom); + maintainPosOriginIndicator.Draw(spriteBatch, steeringOrigin, GUIStyle.Orange, scale: 0.5f * sonar.Zoom); } else { - GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringOrigin.X - 5, (int)steeringOrigin.Y - 5, 10, 10), GUI.Style.Orange); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)steeringOrigin.X - 5, (int)steeringOrigin.Y - 5, 10, 10), GUIStyle.Orange); } } } @@ -596,11 +596,11 @@ namespace Barotrauma.Items.Components pos.Y = -pos.Y; pos += center; - GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 3 / 2, (int)pos.Y - 3, 6, 6), (SteeringPath.CurrentNode == wp) ? Color.LightGreen : GUI.Style.Green, false); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 3 / 2, (int)pos.Y - 3, 6, 6), (SteeringPath.CurrentNode == wp) ? Color.LightGreen : GUIStyle.Green, false); if (prevPos != Vector2.Zero) { - GUI.DrawLine(spriteBatch, pos, prevPos, GUI.Style.Green); + GUI.DrawLine(spriteBatch, pos, prevPos, GUIStyle.Green); } prevPos = pos; @@ -618,14 +618,14 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, pos1, pos2, - GUI.Style.Red * 0.6f, width: 3); + GUIStyle.Red * 0.6f, width: 3); if (obstacle.Intersection.HasValue) { Vector2 intersectionPos = (obstacle.Intersection.Value - transducerCenter) *displayScale; intersectionPos.Y = -intersectionPos.Y; intersectionPos += center; - GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUI.Style.Red); + GUI.DrawRectangle(spriteBatch, intersectionPos - Vector2.One * 2, Vector2.One * 4, GUIStyle.Red); } Vector2 obstacleCenter = (pos1 + pos2) / 2; @@ -634,7 +634,7 @@ namespace Barotrauma.Items.Components GUI.DrawLine(spriteBatch, obstacleCenter, obstacleCenter + new Vector2(obstacle.AvoidStrength.X, -obstacle.AvoidStrength.Y) * 100, - Color.Lerp(GUI.Style.Green, GUI.Style.Orange, obstacle.Dot), width: 2); + Color.Lerp(GUIStyle.Green, GUIStyle.Orange, obstacle.Dot), width: 2); } } } @@ -673,7 +673,7 @@ namespace Barotrauma.Items.Components dockingButton.Text = dockText; if (dockingButton.FlashTimer <= 0.0f) { - dockingButton.Flash(GUI.Style.Blue, 0.5f, useCircularFlash: true); + dockingButton.Flash(GUIStyle.Blue, 0.5f, useCircularFlash: true); dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } @@ -689,7 +689,7 @@ namespace Barotrauma.Items.Components statusContainer.Visible = false; if (dockingButton.FlashTimer <= 0.0f) { - dockingButton.Flash(GUI.Style.Orange, useCircularFlash: true); + dockingButton.Flash(GUIStyle.Orange, useCircularFlash: true); dockingButton.Pulsate(Vector2.One, Vector2.One * 1.2f, dockingButton.FlashTimer); } } @@ -733,7 +733,10 @@ namespace Barotrauma.Items.Components item.Submarine.RealWorldDepth > Level.Loaded.RealWorldCrushDepth - depthEffectThreshold && item.Submarine.RealWorldDepth > item.Submarine.RealWorldCrushDepth - depthEffectThreshold) { pressureWarningText.Visible = true; - pressureWarningText.Text = item.Submarine.AtDamageDepth ? TextManager.Get("SteeringDepthWarning") : TextManager.Get("SteeringDepthWarningLow").Replace("[crushdepth]", ((int)item.Submarine.RealWorldCrushDepth).ToString()); + pressureWarningText.Text = + item.Submarine.AtDamageDepth ? + TextManager.Get("SteeringDepthWarning") : + TextManager.GetWithVariable("SteeringDepthWarningLow", "[crushdepth]", ((int)item.Submarine.RealWorldCrushDepth).ToString()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index f8962c80e..2a011d6df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -10,10 +10,10 @@ namespace Barotrauma.Items.Components private GUIProgressBar chargeIndicator; private GUIScrollBar rechargeSpeedSlider; - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float RechargeWarningIndicatorLow { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float RechargeWarningIndicatorHigh { get; set; } public Vector2 DrawSize @@ -36,17 +36,17 @@ namespace Barotrauma.Items.Components var rechargeRateContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), upperArea.RectTransform), style: null); var rechargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), rechargeRateContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get("rechargerate"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); - string kW = TextManager.Get("kilowatt"); + TextManager.Get("rechargerate"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); + LocalizedString kW = TextManager.Get("kilowatt"); var rechargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), rechargeRateContainer.RectTransform, Anchor.CenterRight), - "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) + "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { TextGetter = () => $"{(int)MathF.Round(currPowerConsumption)} {kW} ({(int)MathF.Round(RechargeRatio * 100)} %)" }; - if (rechargeText.TextSize.X > rechargeText.Rect.Width) { rechargeText.Font = GUI.SmallFont; } + if (rechargeText.TextSize.X > rechargeText.Rect.Width) { rechargeText.Font = GUIStyle.SmallFont; } var rechargeSliderContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.4f), upperArea.RectTransform, Anchor.BottomCenter)); - + if (RechargeWarningIndicatorLow > 0.0f || RechargeWarningIndicatorHigh > 0.0f) { var rechargeSliderFill = new GUICustomComponent(new RectTransform(new Vector2(0.95f, 0.9f), rechargeSliderContainer.RectTransform, Anchor.Center), (SpriteBatch sb, GUICustomComponent c) => @@ -54,12 +54,12 @@ namespace Barotrauma.Items.Components if (RechargeWarningIndicatorLow > 0.0f) { float warningLow = c.Rect.Width * RechargeWarningIndicatorLow; - GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningLow, c.Rect.Y), new Vector2(c.Rect.Width - warningLow, c.Rect.Height), GUI.Style.Orange, isFilled: true); + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningLow, c.Rect.Y), new Vector2(c.Rect.Width - warningLow, c.Rect.Height), GUIStyle.Orange, isFilled: true); } if (RechargeWarningIndicatorHigh > 0.0f) { float warningHigh = c.Rect.Width * RechargeWarningIndicatorHigh; - GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningHigh, c.Rect.Y), new Vector2(c.Rect.Width - warningHigh, c.Rect.Height), GUI.Style.Red, isFilled: true); + GUI.DrawRectangle(sb, new Vector2(c.Rect.X + warningHigh, c.Rect.Y), new Vector2(c.Rect.Width - warningHigh, c.Rect.Height), GUIStyle.Red, isFilled: true); } }); } @@ -88,17 +88,17 @@ namespace Barotrauma.Items.Components var chargeTextContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.4f), lowerArea.RectTransform), style: null); var chargeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), chargeTextContainer.RectTransform, Anchor.CenterLeft), - TextManager.Get("charge"), textColor: GUI.Style.TextColor, font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + TextManager.Get("charge"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { ToolTip = TextManager.Get("PowerTransferTipPower") }; - string kWmin = TextManager.Get("kilowattminute"); + LocalizedString kWmin = TextManager.Get("kilowattminute"); var chargeText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), chargeTextContainer.RectTransform, Anchor.CenterRight), - "", textColor: GUI.Style.TextColor, font: GUI.Font, textAlignment: Alignment.CenterRight) + "", textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font, textAlignment: Alignment.CenterRight) { TextGetter = () => $"{(int)MathF.Round(charge)}/{(int)capacity} {kWmin} ({(int)MathF.Round(MathUtils.Percentage(charge, capacity))} %)" }; - if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUI.SmallFont; } + if (chargeText.TextSize.X > chargeText.Rect.Width) { chargeText.Font = GUIStyle.SmallFont; } chargeIndicator = new GUIProgressBar(new RectTransform(new Vector2(1.1f, 0.5f), lowerArea.RectTransform, Anchor.BottomCenter) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs index 809f7b497..0a1cbd605 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerTransfer.cs @@ -25,25 +25,25 @@ namespace Barotrauma.Items.Components Stretch = true }; powerIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), - TextManager.Get("PowerTransferPowered"), font: GUI.SubHeadingFont, style: "IndicatorLightGreen") + TextManager.Get("PowerTransferPowered"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightGreen") { CanBeFocused = false }; highVoltageIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), - TextManager.Get("PowerTransferHighVoltage"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("PowerTransferHighVoltage"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { ToolTip = TextManager.Get("PowerTransferTipOvervoltage"), Enabled = false }; lowVoltageIndicator = new GUITickBox(new RectTransform(new Vector2(1, 0.33f), lightsArea.RectTransform), - TextManager.Get("PowerTransferLowVoltage"), font: GUI.SubHeadingFont, style: "IndicatorLightRed") + TextManager.Get("PowerTransferLowVoltage"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRed") { ToolTip = TextManager.Get("PowerTransferTipLowvoltage"), Enabled = false }; - powerIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - highVoltageIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); - lowVoltageIndicator.TextBlock.OverrideTextColor(GUI.Style.TextColor); + powerIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + highVoltageIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); + lowVoltageIndicator.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal); GUITextBlock.AutoScaleAndNormalize(powerIndicator.TextBlock, highVoltageIndicator.TextBlock, lowVoltageIndicator.TextBlock); var textContainer = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), paddedFrame.RectTransform, Anchor.CenterRight), style: null); @@ -57,26 +57,33 @@ namespace Barotrauma.Items.Components }; var powerLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), upperTextArea.RectTransform), - TextManager.Get("PowerTransferPowerLabel"), textColor: GUI.Style.TextColorBright, font: GUI.LargeFont, textAlignment: Alignment.CenterRight) + TextManager.Get("PowerTransferPowerLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipPower") }; var loadLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), lowerTextArea.RectTransform), - TextManager.Get("PowerTransferLoadLabel"), textColor: GUI.Style.TextColorBright, font: GUI.LargeFont, textAlignment: Alignment.CenterRight) + TextManager.Get("PowerTransferLoadLabel"), textColor: GUIStyle.TextColorBright, font: GUIStyle.LargeFont, textAlignment: Alignment.CenterRight) { ToolTip = TextManager.Get("PowerTransferTipLoad") }; var digitalBackground = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.8f), upperTextArea.RectTransform), style: "DigitalFrameDark"); var powerText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.95f), digitalBackground.RectTransform, Anchor.Center), - "", font: GUI.DigitalFont, textColor: GUI.Style.TextColorDark) + "", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark) { TextAlignment = Alignment.CenterRight, ToolTip = TextManager.Get("PowerTransferTipPower"), - TextGetter = () => ((int)Math.Round(-currPowerConsumption)).ToString() + TextGetter = () => { + float currPower = powerLoad < 0 ? -powerLoad: 0; + if (!(this is RelayComponent) && PowerConnections != null && PowerConnections.Count > 0 && PowerConnections[0].Grid != null) + { + currPower = PowerConnections[0].Grid.Power; + } + return ((int)Math.Round(currPower)).ToString(); + } }; var kw1 = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.5f), upperTextArea.RectTransform), - TextManager.Get("kilowatt"), textColor: GUI.Style.TextColor, font: GUI.Font) + TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font) { Padding = Vector4.Zero, TextAlignment = Alignment.BottomCenter @@ -84,14 +91,26 @@ namespace Barotrauma.Items.Components digitalBackground = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.8f), lowerTextArea.RectTransform), style: "DigitalFrameDark"); var loadText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.95f), digitalBackground.RectTransform, Anchor.Center), - "", font: GUI.DigitalFont, textColor: GUI.Style.TextColorDark) + "", font: GUIStyle.DigitalFont, textColor: GUIStyle.TextColorDark) { TextAlignment = Alignment.CenterRight, ToolTip = TextManager.Get("PowerTransferTipLoad"), - TextGetter = () => ((int)Math.Round(this is RelayComponent relay ? relay.DisplayLoad : powerLoad)).ToString() + TextGetter = () => + { + float load = PowerLoad; + if (this is RelayComponent relay) + { + load = relay.DisplayLoad; + } + else if (load < 0) + { + load = 0; + } + return ((int)Math.Round(load)).ToString(); + } }; var kw2 = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.5f), lowerTextArea.RectTransform), - TextManager.Get("kilowatt"), textColor: GUI.Style.TextColor, font: GUI.Font) + TextManager.Get("kilowatt"), textColor: GUIStyle.TextColorNormal, font: GUIStyle.Font) { Padding = Vector4.Zero, TextAlignment = Alignment.BottomCenter @@ -106,8 +125,8 @@ namespace Barotrauma.Items.Components { if (GuiFrame == null) return; - float voltage = powerLoad <= 0.0f ? 1.0f : -currPowerConsumption / powerLoad; - powerIndicator.Selected = IsActive && currPowerConsumption < -0.1f; + float voltage = (PowerConnections.Count > 0 && PowerConnections[0].Grid != null) ? PowerConnections[0].Grid.Voltage : 0f; + powerIndicator.Selected = IsActive && voltage > 0; highVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage > 1.2f; lowVoltageIndicator.Selected = Timing.TotalTime % 0.5f < 0.25f && powerIndicator.Selected && voltage < 0.8f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs index acf4b27c2..e3d4e35c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/Powered.cs @@ -9,14 +9,14 @@ namespace Barotrauma.Items.Components private RoundSound powerOnSound; private bool powerOnSoundPlayed; - partial void InitProjectSpecific(XElement element) + partial void InitProjectSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "poweronsound": - powerOnSound = Submarine.LoadRoundSound(subElement, false); + powerOnSound = RoundSound.Load(subElement, false); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 6fce23838..bd467a7b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -115,9 +115,9 @@ namespace Barotrauma.Items.Components } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs index 8c17e9d90..deeac3cbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs @@ -7,14 +7,14 @@ namespace Barotrauma.Items.Components { partial class Quality : ItemComponent { - public override void AddTooltipInfo(ref string name, ref string description) + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { foreach (var statValue in statValues) { int roundedValue = (int)Math.Round(statValue.Value * qualityLevel * 100); if (roundedValue == 0) { return; } - string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); - description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}%‖color:end‖ {TextManager.Get("qualitystattypenames." + statValue.Key.ToString(), true) ?? statValue.Key.ToString()}"; + string colorStr = XMLExtensions.ColorToString(GUIStyle.Green); + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}%‖color:end‖ {TextManager.Get("qualitystattypenames." + statValue.Key.ToString()).Fallback(statValue.Key.ToString())}"; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 93af5d8f2..759f67ad6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -31,9 +31,9 @@ namespace Barotrauma.Items.Components private float prevProgressBarState; private Item prevProgressBarTarget = null; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -41,10 +41,10 @@ namespace Barotrauma.Items.Components particleEmitters.Add(new ParticleEmitter(subElement)); break; case "particleemitterhititem": - string[] identifiers = subElement.GetAttributeStringArray("identifiers", new string[0]); - if (identifiers.Length == 0) identifiers = subElement.GetAttributeStringArray("identifier", new string[0]); - string[] excludedIdentifiers = subElement.GetAttributeStringArray("excludedidentifiers", new string[0]); - if (excludedIdentifiers.Length == 0) excludedIdentifiers = subElement.GetAttributeStringArray("excludedidentifier", new string[0]); + Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifiers", Array.Empty()); + if (identifiers.Length == 0) { identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); } + Identifier[] excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifiers", Array.Empty()); + if (excludedIdentifiers.Length == 0) { excludedIdentifiers = subElement.GetAttributeIdentifierArray("excludedidentifier", Array.Empty()); } particleEmitterHitItem.Add( new Pair( @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components targetStructure.ID * 1000 + sectionIndex, //unique "identifier" for each wall section progressBarPos, MathUtils.InverseLerp(targetStructure.Prefab.MinHealth, targetStructure.Health, targetStructure.Health - targetStructure.SectionDamage(sectionIndex)), - GUI.Style.Red, GUI.Style.Green); + GUIStyle.Red, GUIStyle.Green); if (progressBar != null) progressBar.Size = new Vector2(60.0f, 20.0f); @@ -128,7 +128,7 @@ namespace Barotrauma.Items.Components targetItem, progressBarPos, progressBarState, - GUI.Style.Red, GUI.Style.Green, + GUIStyle.Red, GUIStyle.Green, progressBarState < prevProgressBarState ? "progressbar.cutting" : ""); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 0900f4fed..1a1497aba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -29,9 +29,9 @@ namespace Barotrauma.Items.Components private SoundChannel repairSoundChannel; - private string repairButtonText, repairingText; - private string sabotageButtonText, sabotagingText; - private string tinkerButtonText, tinkeringText; + private LocalizedString repairButtonText, repairingText; + private LocalizedString sabotageButtonText, sabotagingText; + private LocalizedString tinkerButtonText, tinkeringText; private FixActions requestStartFixAction; @@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components public float FakeBrokenTimer; - [Serialize("", false, description: "An optional description of the needed repairs displayed in the repair interface.")] + [Serialize("", IsPropertySaveable.No, description: "An optional description of the needed repairs displayed in the repair interface.")] public string Description { get; @@ -78,10 +78,10 @@ namespace Barotrauma.Items.Components return false; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { CreateGUI(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -124,18 +124,18 @@ namespace Barotrauma.Items.Components }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), - header, textAlignment: Alignment.TopCenter, font: GUI.LargeFont); + header, textAlignment: Alignment.TopCenter, font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - Description, font: GUI.SmallFont, wrap: true); + Description, font: GUIStyle.SmallFont, wrap: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("RequiredRepairSkills"), font: GUI.SubHeadingFont); + TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); for (int i = 0; i < requiredSkills.Count; i++) { var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()), - font: GUI.SmallFont) + font: GUIStyle.SmallFont) { UserData = requiredSkills[i] }; @@ -148,9 +148,9 @@ namespace Barotrauma.Items.Components }; progressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), progressBarHolder.RectTransform), - color: GUI.Style.Green, barSize: 0.0f, style: "DeviceProgressBar"); + color: GUIStyle.Green, barSize: 0.0f, style: "DeviceProgressBar"); - progressBarOverlayText = new GUITextBlock(new RectTransform(Vector2.One, progressBar.RectTransform), string.Empty, font: GUI.SubHeadingFont, textAlignment: Alignment.Center) + progressBarOverlayText = new GUITextBlock(new RectTransform(Vector2.One, progressBar.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center) { IgnoreLayoutGroups = true }; @@ -203,8 +203,8 @@ namespace Barotrauma.Items.Components } }; - tinkerButtonText = TextManager.Get("TinkerButton", returnNull: true) ?? "Tinker"; - tinkeringText = TextManager.Get("Tinkering", returnNull: true) ?? "Tinkering"; + tinkerButtonText = TextManager.Get("TinkerButton").Fallback("Tinker"); + tinkeringText = TextManager.Get("Tinkering").Fallback("Tinkering"); TinkerButton = new GUIButton(new RectTransform(Vector2.One, extraButtonContainer.RectTransform), tinkerButtonText, style: "GUIButtonSmall") { IgnoreLayoutGroups = true, @@ -295,24 +295,24 @@ namespace Barotrauma.Items.Components public override void DrawHUD(SpriteBatch spriteBatch, Character character) { IsActive = true; - + float defaultMaxCondition = (item.MaxCondition / item.MaxRepairConditionMultiplier); progressBar.BarSize = item.Condition / defaultMaxCondition; - progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, GUI.Style.Red, GUI.Style.Orange, GUI.Style.Green); + progressBar.Color = ToolBox.GradientLerp(progressBar.BarSize, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green); Rectangle sliderRect = progressBar.GetSliderRect(1.0f); Color qteSliderColor = Color.White; if (qteCooldown > 0.0f) { - qteSliderColor = qteSuccess ? GUI.Style.Green : GUI.Style.Red * 0.5f; + qteSliderColor = qteSuccess ? GUIStyle.Green : GUIStyle.Red * 0.5f; progressBar.Color = ToolBox.GradientLerp(qteCooldown / QteCooldownDuration, progressBar.Color, qteSliderColor, Color.White); } else { if (qteTimer / QteDuration <= item.Condition / item.MaxCondition) { - qteSliderColor = Color.Lerp(qteSliderColor, GUI.Style.Green, 0.5f); + qteSliderColor = Color.Lerp(qteSliderColor, GUIStyle.Green, 0.5f); } } @@ -324,7 +324,7 @@ namespace Barotrauma.Items.Components if (item.Condition > defaultMaxCondition) { float extraCondition = item.MaxCondition * (item.MaxRepairConditionMultiplier - 1.0f); - progressBar.Color = ToolBox.GradientLerp((item.Condition - defaultMaxCondition) / extraCondition, GUI.Style.ColorReputationHigh, GUI.Style.ColorReputationVeryHigh); + progressBar.Color = ToolBox.GradientLerp((item.Condition - defaultMaxCondition) / extraCondition, GUIStyle.ColorReputationHigh, GUIStyle.ColorReputationVeryHigh); progressBarOverlayText.Visible = true; progressBarOverlayText.Text = $"{(int)Math.Round((item.Condition / defaultMaxCondition) * 100)}%"; } @@ -364,7 +364,7 @@ namespace Barotrauma.Items.Components GUITextBlock textBlock = (GUITextBlock)c; if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier)) { - textBlock.TextColor = GUI.Style.Red; + textBlock.TextColor = GUIStyle.Red; } else { @@ -388,11 +388,11 @@ namespace Barotrauma.Items.Components { GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), "Deteriorating at " + (int)(DeteriorationSpeed * 60.0f) + " units/min" + (paused ? " [PAUSED]" : ""), - paused ? Color.Cyan : GUI.Style.Red, Color.Black * 0.5f); + paused ? Color.Cyan : GUIStyle.Red, Color.Black * 0.5f); } GUI.DrawString(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + 20), "Condition: " + (int)item.Condition + "/" + (int)item.MaxCondition, - GUI.Style.Orange); + GUIStyle.Orange); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index e98bf3f58..783864f76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -10,28 +10,28 @@ namespace Barotrauma.Items.Components { private Sprite sprite, startSprite, endSprite; - [Serialize(5, false)] + [Serialize(5, IsPropertySaveable.No)] public int SpriteWidth { get; set; } - [Serialize("255,255,255,255", false)] + [Serialize("255,255,255,255", IsPropertySaveable.No)] public Color SpriteColor { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool Tile { get; set; } - [Serialize("0.5,0.5)", false)] + [Serialize("0.5,0.5)", IsPropertySaveable.No)] public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); public Vector2 DrawSize @@ -62,9 +62,9 @@ namespace Barotrauma.Items.Components return sourcePos; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs index a44dca68e..865f0915d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components Character.Controlled?.UpdateHUDProgressBar(this, item.WorldPosition, ScanTimer / ScanDuration, - GUI.Style.Red, GUI.Style.Green, + GUIStyle.Red, GUIStyle.Green, textTag: "progressbar.scanning"); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index d0a181824..2202530c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components private GUIImage containerIndicator; private GUIComponentStyle indicatorStyleRed, indicatorStyleGreen; - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { terminalButtonStyles = new string[RequiredSignalCount]; int i = 0; @@ -24,8 +24,8 @@ namespace Barotrauma.Items.Components if (style == null) { continue; } terminalButtonStyles[i++] = style; } - indicatorStyleRed = GUI.Style.GetComponentStyle("IndicatorLightRed"); - indicatorStyleGreen = GUI.Style.GetComponentStyle("IndicatorLightGreen"); + indicatorStyleRed = GUIStyle.GetComponentStyle("IndicatorLightRed"); + indicatorStyleGreen = GUIStyle.GetComponentStyle("IndicatorLightGreen"); CreateGUI(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 2e8e07a2b..cfe0fa95b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -190,7 +190,7 @@ namespace Barotrauma.Items.Components if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wire.OtherConnection(null); - string label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; + LocalizedString label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; if (wire.Locked) { label += "\n" + TextManager.Get("ConnectionLocked"); } DrawWire(spriteBatch, wire, new Vector2(x, y + height - 100 * GUI.Scale), new Vector2(x, y + height), @@ -208,17 +208,17 @@ namespace Barotrauma.Items.Components private void DrawConnection(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 labelPos, Vector2 scale) { - string text = DisplayName.ToUpper(); + string text = DisplayName.Value.ToUpper(); //nasty - if (GUI.Style.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) + if (GUIStyle.GetComponentStyle("ConnectionPanelLabel")?.Sprites.Values.First().First() is UISprite labelSprite) { Rectangle labelArea = GetLabelArea(labelPos, text, scale); - labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUI.Style.Red : Color.SteelBlue); + labelSprite.Draw(spriteBatch, labelArea, IsPower ? GUIStyle.Red : Color.SteelBlue); } - GUI.DrawString(spriteBatch, labelPos + Vector2.UnitY, text, Color.Black * 0.8f, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, labelPos, text, GUI.Style.TextColorBright, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, labelPos + Vector2.UnitY, text, Color.Black * 0.8f, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont); float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale); @@ -234,7 +234,7 @@ namespace Barotrauma.Items.Components if (wires[i].HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } Connection recipient = wires[i].OtherConnection(this); - string label = recipient == null ? "" : recipient.item.Name + $" ({recipient.DisplayName})"; + 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); @@ -295,7 +295,7 @@ namespace Barotrauma.Items.Components { FlashTimer = flashDuration; this.flashDuration = flashDuration; - flashColor = (color == null) ? GUI.Style.Red : (Color)color; + flashColor = (color == null) ? GUIStyle.Red : (Color)color; } public void UpdateFlashTimer(float deltaTime) @@ -304,7 +304,7 @@ namespace Barotrauma.Items.Components FlashTimer -= deltaTime; } - private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, string label) + private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, LocalizedString label) { int textX = (int)start.X; if (start.X < end.X) @@ -324,20 +324,20 @@ namespace Barotrauma.Items.Components Vector2.Distance(end, PlayerInput.MousePosition) < 20.0f || new Rectangle((start.X < end.X) ? textX - 100 : textX, (int)start.Y - 5, 100, 14).Contains(PlayerInput.MousePosition)); - if (!string.IsNullOrEmpty(label)) + if (!label.IsNullOrEmpty()) { if (start.Y > panel.GuiFrame.Rect.Bottom - 1.0f) { //wire at the bottom of the panel -> draw the text below the panel, tilted 45 degrees - GUI.Font.DrawString(spriteBatch, label, start + Vector2.UnitY * 20 * GUI.Scale, Color.White, 45.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0.0f); + GUIStyle.Font.DrawString(spriteBatch, label, start + Vector2.UnitY * 20 * GUI.Scale, Color.White, 45.0f, Vector2.Zero, 1.0f, SpriteEffects.None, 0.0f); } else { GUI.DrawString(spriteBatch, - new Vector2(start.X < end.X ? textX - GUI.SmallFont.MeasureString(label).X : textX, start.Y - 5.0f), + new Vector2(start.X < end.X ? textX - GUIStyle.SmallFont.MeasureString(label).X : textX, start.Y - 5.0f), label, - wire.Locked ? GUI.Style.TextColorDim : (mouseOn ? Wire.higlightColor : GUI.Style.TextColor), Color.Black * 0.9f, - 3, GUI.SmallFont); + wire.Locked ? GUIStyle.TextColorDim : (mouseOn ? Wire.higlightColor : GUIStyle.TextColorNormal), Color.Black * 0.9f, + 3, GUIStyle.SmallFont); } } @@ -401,13 +401,13 @@ namespace Barotrauma.Items.Components { if (c.IsOutput) { - var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.ToUpper(), scale); + var labelArea = GetLabelArea(GetOutputLabelPosition(rightPos, panel, c), c.DisplayName.Value.ToUpper(), scale); labelAreas.Add(labelArea); rightPos.Y += connectorIntervalLeft; } else { - var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.ToUpper(), scale); + var labelArea = GetLabelArea(GetInputLabelPosition(leftPos, panel, c), c.DisplayName.Value.ToUpper(), scale); labelAreas.Add(labelArea); leftPos.Y += connectorIntervalRight; } @@ -455,19 +455,19 @@ namespace Barotrauma.Items.Components { return new Vector2( connectorPosition.X + 25 * panel.Scale, - connectorPosition.Y - 5 * panel.Scale - GUI.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); + connectorPosition.Y - 5 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).Y); } private static Vector2 GetOutputLabelPosition(Vector2 connectorPosition, ConnectionPanel panel, Connection connection) { return new Vector2( - connectorPosition.X - 25 * panel.Scale - GUI.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, + connectorPosition.X - 25 * panel.Scale - GUIStyle.SmallFont.MeasureString(connection.DisplayName.ToUpper()).X, connectorPosition.Y + 5 * panel.Scale); } private static Rectangle GetLabelArea(Vector2 labelPos, string text, Vector2 scale) { - Vector2 textSize = GUI.SmallFont.MeasureString(text); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Rectangle labelArea = new Rectangle(labelPos.ToPoint(), textSize.ToPoint()); labelArea.Inflate(10 * scale.X, 3 * scale.Y); return labelArea; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 72e8482da..057679a25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components HighlightedWire = null; Connection.DrawConnections(spriteBatch, this, user); - foreach (UISprite sprite in GUI.Style.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None]) + foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None]) { sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 5a24346cc..f22fbcce1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components UserData = ciElement }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label); + TextManager.Get(ciElement.Label).Fallback(ciElement.Label)); if (!ciElement.IsIntegerInput) { var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform) { MaxSize = ElementMaxSize - }, TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label) + }, TextManager.Get(ciElement.Label).Fallback(ciElement.Label)) { UserData = ciElement }; @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components else { var btn = new GUIButton(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label, style: "DeviceButton") + TextManager.Get(ciElement.Label).Fallback(ciElement.Label), style: "DeviceButton") { UserData = ciElement }; @@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components } } - string CreateLabelText(int elementIndex) + LocalizedString CreateLabelText(int elementIndex) { return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index f9c20a0ff..7538b6fc4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -90,7 +90,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 ? GUI.MonospacedFont : GUI.GlobalFont) + textColor: color, wrap: true, font: UseMonospaceFont ? GUIStyle.MonospacedFont : GUIStyle.GlobalFont) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index f4b055a39..4e1de585c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components if (width <= 0f) { return; } RecalculateVertices(wire, width); - for (int i=0;i draggingWire; } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { if (defaultWireSprite == null) { @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components }; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("wiresprite", StringComparison.OrdinalIgnoreCase)) { @@ -286,7 +286,7 @@ namespace Barotrauma.Items.Components WireSection.Draw( spriteBatch, this, start, endPos, - GUI.Style.Orange, depth + 0.00001f, 0.2f); + GUIStyle.Orange, depth + 0.00001f, 0.2f); WireSection.Draw( spriteBatch, this, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 88367f1a0..e956c1721 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class StatusHUD : ItemComponent { - private static readonly string[] BleedingTexts = + private static readonly LocalizedString[] BleedingTexts = { TextManager.Get("MinorBleeding"), TextManager.Get("Bleeding"), @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components TextManager.Get("CatastrophicBleeding") }; - private static readonly string[] OxygenTexts = + private static readonly LocalizedString[] OxygenTexts = { TextManager.Get("OxygenNormal"), TextManager.Get("OxygenReduced"), @@ -25,42 +25,42 @@ namespace Barotrauma.Items.Components TextManager.Get("NotBreathing") }; - [Serialize(500.0f, false, description: "How close to a target the user must be to see their health data (in pixels).")] + [Serialize(500.0f, IsPropertySaveable.No, description: "How close to a target the user must be to see their health data (in pixels).")] public float Range { get; private set; } - [Serialize(50.0f, false, description: "The range within which the health info texts fades out.")] + [Serialize(50.0f, IsPropertySaveable.No, description: "The range within which the health info texts fades out.")] public float FadeOutRange { get; private set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool ThermalGoggles { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool ShowDeadCharacters { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool ShowTexts { get; private set; } - [Serialize("72,119,72,120", false)] + [Serialize("72,119,72,120", IsPropertySaveable.No)] public Color OverlayColor { get; @@ -159,7 +159,8 @@ namespace Barotrauma.Items.Components if (OverlayColor.A > 0) { - GUI.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), OverlayColor); + GUIStyle.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), + OverlayColor); } if (ShowTexts) @@ -208,7 +209,7 @@ namespace Barotrauma.Items.Components float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist > Range * Range) { continue; } - Sprite pingCircle = GUI.Style.UIThermalGlow.Sprite; + Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; foreach (Limb limb in c.AnimController.Limbs) { if (limb.Mass < 1.0f) { continue; } @@ -230,71 +231,71 @@ namespace Barotrauma.Items.Components Vector2 hudPos = GameMain.GameScreen.Cam.WorldToScreen(target.DrawPosition); hudPos += Vector2.UnitX * 50.0f; - List texts = new List(); + List texts = new List(); List textColors = new List(); texts.Add(target.Info == null ? target.DisplayName : target.Info.DisplayName); - Color nameColor = GUI.Style.TextColor; + Color nameColor = GUIStyle.TextColorNormal; if (Character.Controlled != null && target.TeamID != Character.Controlled.TeamID) { - nameColor = target.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + nameColor = target.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUIStyle.Red; } textColors.Add(nameColor); if (target.IsDead) { texts.Add(TextManager.Get("Deceased")); - textColors.Add(GUI.Style.Red); + textColors.Add(GUIStyle.Red); if (target.CauseOfDeath != null) { texts.Add( target.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + target.CauseOfDeath.Type.ToString()))); - textColors.Add(GUI.Style.Red); + textColors.Add(GUIStyle.Red); } } else { - if (!string.IsNullOrEmpty(target.customInteractHUDText) && target.AllowCustomInteract) + if (!target.CustomInteractHUDText.IsNullOrEmpty() && target.AllowCustomInteract) { - texts.Add(target.customInteractHUDText); - textColors.Add(GUI.Style.Green); + texts.Add(target.CustomInteractHUDText); + textColors.Add(GUIStyle.Green); } if (!target.IsIncapacitated && target.IsPet) { - texts.Add(CharacterHUD.GetCachedHudText("PlayHint", GameMain.Config.KeyBindText(InputType.Use))); - textColors.Add(GUI.Style.Green); + texts.Add(CharacterHUD.GetCachedHudText("PlayHint", InputType.Use)); + textColors.Add(GUIStyle.Green); } if (target.CharacterHealth.UseHealthWindow && !target.DisableHealthWindow && equipper?.FocusedCharacter == target && equipper.CanInteractWith(target, 160f, false)) { - texts.Add(CharacterHUD.GetCachedHudText("HealHint", GameMain.Config.KeyBindText(InputType.Health))); - textColors.Add(GUI.Style.Green); + texts.Add(CharacterHUD.GetCachedHudText("HealHint", InputType.Health)); + textColors.Add(GUIStyle.Green); } if (target.CanBeDragged) { - texts.Add(CharacterHUD.GetCachedHudText("GrabHint", GameMain.Config.KeyBindText(InputType.Grab))); - textColors.Add(GUI.Style.Green); + texts.Add(CharacterHUD.GetCachedHudText("GrabHint", InputType.Grab)); + textColors.Add(GUIStyle.Green); } if (target.IsUnconscious) { texts.Add(TextManager.Get("Unconscious")); - textColors.Add(GUI.Style.Orange); + textColors.Add(GUIStyle.Orange); } if (target.Stun > 0.01f) { texts.Add(TextManager.Get("Stunned")); - textColors.Add(GUI.Style.Orange); + textColors.Add(GUIStyle.Orange); } int oxygenTextIndex = MathHelper.Clamp((int)Math.Floor((1.0f - (target.Oxygen / 100.0f)) * OxygenTexts.Length), 0, OxygenTexts.Length - 1); texts.Add(OxygenTexts[oxygenTextIndex]); - textColors.Add(Color.Lerp(GUI.Style.Red, GUI.Style.Green, target.Oxygen / 100.0f)); + textColors.Add(Color.Lerp(GUIStyle.Red, GUIStyle.Green, target.Oxygen / 100.0f)); if (target.Bleeding > 0.0f) { int bleedingTextIndex = MathHelper.Clamp((int)Math.Floor(target.Bleeding / 100.0f) * BleedingTexts.Length, 0, BleedingTexts.Length - 1); texts.Add(BleedingTexts[bleedingTextIndex]); - textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, target.Bleeding / 100.0f)); + textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, target.Bleeding / 100.0f)); } var allAfflictions = target.CharacterHealth.GetAllAfflictions(); @@ -315,21 +316,21 @@ namespace Barotrauma.Items.Components foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max((int)combinedAfflictionStrengths[affliction], 1).ToString() + " %")); - textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); + textColors.Add(Color.Lerp(GUIStyle.Orange, GUIStyle.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } - GUI.DrawString(spriteBatch, hudPos, texts[0], textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, hudPos, texts[0], textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont); hudPos.X += 5.0f; - hudPos.Y += 24.0f; + hudPos.Y += 24.0f * GameSettings.CurrentConfig.Graphics.TextScale; hudPos.X = (int)hudPos.X; hudPos.Y = (int)hudPos.Y; for (int i = 1; i < texts.Count; i++) { - GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUI.SmallFont); - hudPos.Y += 18.0f; + GUI.DrawString(spriteBatch, hudPos, texts[i], textColors[i] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SmallFont); + hudPos.Y += (int)(18.0f * GameSettings.CurrentConfig.Graphics.TextScale); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 98f9490aa..ded8496e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -57,42 +57,42 @@ namespace Barotrauma.Items.Components private readonly List particleEmitters = new List(); private readonly List particleEmitterCharges = new List(); - [Editable, Serialize("0,0,0,0", true, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] + [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A).")] public Color HudTint { get; private set; } - [Serialize(false, false, description: "Should the charge of the connected batteries/supercapacitors be shown at the top of the screen when operating the item.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the charge of the connected batteries/supercapacitors be shown at the top of the screen when operating the item.")] public bool ShowChargeIndicator { get; private set; } - [Serialize(false, false, description: "Should the available ammunition be shown at the top of the screen when operating the item.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the available ammunition be shown at the top of the screen when operating the item.")] public bool ShowProjectileIndicator { get; private set; } - [Serialize(0.0f, false, description: "How far the barrel \"recoils back\" when the turret is fired (in pixels).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How far the barrel \"recoils back\" when the turret is fired (in pixels).")] public float RecoilDistance { get; private set; } - [Serialize(0.0f, false, description: "The distance in which the spinning barrels rotate. Only used if spinning barrels are created.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The distance in which the spinning barrels rotate. Only used if spinning barrels are created.")] public float SpinningBarrelDistance { get; private set; } - [Serialize(false, false, description: "Use firing offset for muzzleflash? This field shouldn't be needed but I'm using it for prototyping")] + [Serialize(false, IsPropertySaveable.No, description: "Use firing offset for muzzleflash? This field shouldn't be needed but I'm using it for prototyping")] public bool UseFiringOffsetForMuzzleFlash { get; @@ -125,33 +125,33 @@ namespace Barotrauma.Items.Components get { return barrelSprite; } } - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - string texturePath = subElement.GetAttributeString("texture", ""); + string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) { case "crosshair": - crosshairSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairSprite = new Sprite(subElement, path: textureDir); break; case "weaponindicator": - WeaponIndicatorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + WeaponIndicatorSprite = new Sprite(subElement, path: textureDir); break; case "crosshairpointer": - crosshairPointerSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + crosshairPointerSprite = new Sprite(subElement, path: textureDir); break; case "startmovesound": - startMoveSound = Submarine.LoadRoundSound(subElement, false); + startMoveSound = RoundSound.Load(subElement, false); break; case "endmovesound": - endMoveSound = Submarine.LoadRoundSound(subElement, false); + endMoveSound = RoundSound.Load(subElement, false); break; case "movesound": - moveSound = Submarine.LoadRoundSound(subElement, false); + moveSound = RoundSound.Load(subElement, false); break; case "chargesound": - chargeSound = Submarine.LoadRoundSound(subElement, false); + chargeSound = RoundSound.Load(subElement, false); break; case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); @@ -437,15 +437,15 @@ namespace Barotrauma.Items.Components if (Math.Abs(minRotation - maxRotation) < 0.02f) { - spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUI.Style.Green, thickness: lineThickness); + spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUIStyle.Green, thickness: lineThickness); } else if (radians > Math.PI * 2) { - spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUI.Style.Red, thickness: lineThickness); + spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUIStyle.Red, thickness: lineThickness); } else { - spriteBatch.DrawSector(drawPos, circleRadius, radians, (int)Math.Abs(90 * radians), GUI.Style.Green, offset: minRotation, thickness: lineThickness); + spriteBatch.DrawSector(drawPos, circleRadius, radians, (int)Math.Abs(90 * radians), GUIStyle.Green, offset: minRotation, thickness: lineThickness); } int baseWidgetScale = GUI.IntScale(16); @@ -459,7 +459,7 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUI.Style.Green; + widget.color = GUIStyle.Green; prevAngle = minRotation; }; widget.Deselected += () => @@ -469,7 +469,7 @@ namespace Barotrauma.Items.Components RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { - SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits".ToIdentifier(), RotationLimits, oldRotation)); } }; widget.MouseHeld += (deltaTime) => @@ -503,7 +503,7 @@ namespace Barotrauma.Items.Components }; widget.MouseDown += () => { - widget.color = GUI.Style.Green; + widget.color = GUIStyle.Green; prevAngle = maxRotation; }; widget.Deselected += () => @@ -513,7 +513,7 @@ namespace Barotrauma.Items.Components RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { - SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits".ToIdentifier(), RotationLimits, oldRotation)); } }; widget.MouseHeld += (deltaTime) => @@ -654,8 +654,8 @@ namespace Barotrauma.Items.Components if (ShowChargeIndicator && PowerConsumption > 0.0f) { powerIndicator.Color = charged ? - (HasPowerToShoot() ? GUI.Style.Green : GUI.Style.Orange) : - GUI.Style.Red; + (HasPowerToShoot() ? GUIStyle.Green : GUIStyle.Orange) : + GUIStyle.Red; if (flashLowPower) { powerIndicator.BarSize = 1; @@ -693,7 +693,7 @@ namespace Barotrauma.Items.Components Rectangle rect = new Rectangle(invSlotPos.X, invSlotPos.Y, totalWidth, slotSize.Y); float inflate = MathHelper.Lerp(3, 8, (float)Math.Abs(Math.Sin(flashTimer * 5))); rect.Inflate(inflate, inflate); - Color color = GUI.Style.Red * Math.Max(0.5f, (float)Math.Sin(flashTimer * 12)); + Color color = GUIStyle.Red * Math.Max(0.5f, (float)Math.Sin(flashTimer * 12)); if (flashNoAmmo) { GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3); @@ -701,7 +701,7 @@ namespace Barotrauma.Items.Components else if (flashLoaderBroken) { GUI.DrawRectangle(spriteBatch, rect, color, thickness: 3); - GUI.BrokenIcon.Draw(spriteBatch, rect.Center.ToVector2(), color, scale: rect.Height / GUI.BrokenIcon.size.Y); + GUIStyle.BrokenIcon.Value.Sprite.Draw(spriteBatch, rect.Center.ToVector2(), color, scale: rect.Height / GUIStyle.BrokenIcon.Value.Sprite.size.Y); GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("turretloaderbroken"), new Rectangle(invSlotPos.X + totalWidth + GUI.IntScale(10), invSlotPos.Y + slotSize.Y / 2 - GUI.IntScale(9), 0, 0)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 26665699e..4c5848a45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -1,25 +1,25 @@ using System; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma.Items.Components { - partial class Wearable + partial class Wearable : Pickable, IServerSerializable { - private void GetDamageModifierText(ref string description, DamageModifier damageModifier, string afflictionIdentifier) + private void GetDamageModifierText(ref LocalizedString description, DamageModifier damageModifier, Identifier afflictionIdentifier) { int roundedValue = (int)Math.Round((1 - damageModifier.DamageMultiplier * damageModifier.ProbabilityMultiplier) * 100); if (roundedValue == 0) { return; } - string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + string colorStr = XMLExtensions.ColorToString(GUIStyle.Green); - string afflictionName = - AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? - TextManager.Get($"afflictiontype.{afflictionIdentifier}", returnNull: true) ?? - afflictionIdentifier; + LocalizedString afflictionName = + AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier)?.Name ?? + TextManager.Get($"afflictiontype.{afflictionIdentifier}").Fallback(afflictionIdentifier.Value); description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {afflictionName}"; } - - public override void AddTooltipInfo(ref string name, ref string description) + + public override void AddTooltipInfo(ref LocalizedString name, ref LocalizedString description) { if (damageModifiers.Any(d => !MathUtils.NearlyEqual(d.DamageMultiplier, 1f) || !MathUtils.NearlyEqual(d.ProbabilityMultiplier, 1f)) || SkillModifiers.Any()) { @@ -35,11 +35,11 @@ namespace Barotrauma.Items.Components continue; } - foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) + foreach (Identifier afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) { GetDamageModifierText(ref description, damageModifier, afflictionIdentifier); } - foreach (string afflictionType in damageModifier.ParsedAfflictionTypes) + foreach (Identifier afflictionType in damageModifier.ParsedAfflictionTypes) { GetDamageModifierText(ref description, damageModifier, afflictionType); } @@ -49,10 +49,10 @@ namespace Barotrauma.Items.Components { foreach (var skillModifier in SkillModifiers) { - string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + string colorStr = XMLExtensions.ColorToString(GUIStyle.Green); int roundedValue = (int)Math.Round(skillModifier.Value); if (roundedValue == 0) { continue; } - description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}‖color:end‖ {TextManager.Get("SkillName." + skillModifier.Key, true) ?? skillModifier.Key}"; + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}‖color:end‖ {TextManager.Get($"SkillName.{skillModifier.Key}").Fallback(skillModifier.Key.Value)}"; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 86116fbbd..19ea4b3b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -43,7 +43,7 @@ namespace Barotrauma } public float QuickUseTimer; - public string QuickUseButtonToolTip; + public LocalizedString QuickUseButtonToolTip; public bool IsMoving = false; private static Rectangle offScreenRect = new Rectangle(new Point(-1000, 0), Point.Zero); @@ -144,7 +144,7 @@ namespace Barotrauma { public static float UIScale { - get { return (GameMain.GraphicsWidth / 1920.0f + GameMain.GraphicsHeight / 1080.0f) / 2.5f * GameSettings.InventoryScale; } + get { return (GameMain.GraphicsWidth / 1920.0f + GameMain.GraphicsHeight / 1080.0f) / 2.5f * GameSettings.CurrentConfig.Graphics.InventoryScale; } } public static int ContainedIndicatorHeight @@ -215,8 +215,7 @@ namespace Barotrauma public Inventory Inventory; public readonly Item Item; public readonly bool IsSubSlot; - public string Tooltip { get; private set; } - public List TooltipRichTextData { get; private set;} + public RichString Tooltip { get; private set; } public int tooltipDisplayedCondition; @@ -250,23 +249,22 @@ namespace Barotrauma { itemsInSlot = ParentInventory.GetItemsAt(SlotIndex); } - TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item, itemsInSlot), out string newTooltip); - Tooltip = newTooltip; + Tooltip = GetTooltip(Item, itemsInSlot); tooltipDisplayedCondition = (int)Item.ConditionPercentage; } - private string GetTooltip(Item item, IEnumerable itemsInSlot) + private RichString GetTooltip(Item item, IEnumerable itemsInSlot) { if (item == null) { return null; } - string toolTip = ""; + LocalizedString toolTip = ""; if (GameMain.DebugDraw) { toolTip = item.ToString(); } else { - string description = item.Description; + LocalizedString description = item.Description; if (item.Prefab.Identifier == "idcard" || item.Tags.Contains("despawncontainer")) { string[] readTags = item.Tags.Split(','); @@ -288,7 +286,7 @@ namespace Barotrauma } else { - description = TextManager.GetWithVariables("IDCardNameJob", new string[2] { "[name]", "[job]" }, new string[2] { idName, idJob }, new bool[2] { false, true }); + description = TextManager.GetWithVariables("IDCardNameJob", ("[name]", idName, FormatCapitals.No), ("[job]", idJob, FormatCapitals.Yes)); } if (!string.IsNullOrEmpty(item.Description)) { @@ -297,7 +295,7 @@ namespace Barotrauma } } - string name = item.Name; + LocalizedString name = item.Name; foreach (ItemComponent component in item.Components) { component.AddTooltipInfo(ref name, ref description); @@ -314,13 +312,14 @@ namespace Barotrauma } } - string colorStr = XMLExtensions.ColorToString(item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUI.Style.Red : Color.White); + string colorStr = (item.SpawnedInCurrentOutpost && !item.AllowStealing ? GUIStyle.Red : Color.White).ToStringHex(); toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (item.GetComponent() != null) { - // substring by to get rid of the empty space at start, text file should be adjusted - toolTip += $"\n{TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "", fallBackTag: "itemname.quality3")?.Substring(1)}"; + toolTip += "\n" + TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "") + .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", "")) + .TrimStart(); } if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) @@ -329,24 +328,24 @@ namespace Barotrauma } if (!item.IsFullCondition && !item.Prefab.HideConditionInTooltip) { - string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull)); + string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull)); toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; } - if (!string.IsNullOrEmpty(description)) { toolTip += '\n' + description; } - if (item.prefab.ContentPackage != GameMain.VanillaContent && item.prefab.ContentPackage != null) + if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; } + if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null) { colorStr = XMLExtensions.ColorToString(Color.MediumPurple); - toolTip += $"\n‖color:{colorStr}‖{item.prefab.ContentPackage.Name}‖color:end‖"; + toolTip += $"\n‖color:{colorStr}‖{item.Prefab.ContentPackage.Name}‖color:end‖"; } } if (itemsInSlot.Count() > 1) { - string colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; - colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); - toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; + string colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); + toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + colorStr = XMLExtensions.ColorToString(GUIStyle.Blue); + toolTip += $"\n‖color:{colorStr}‖[{GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; } - return toolTip; + return RichString.Rich(toolTip); } } @@ -551,7 +550,7 @@ namespace Barotrauma { if (item != null) { - slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); if (!mouseDrag) { SoundPlayer.PlayUISound(GUISoundType.PickItem); @@ -1038,9 +1037,9 @@ namespace Barotrauma return CursorState.Default; } - protected static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle highlightedSlot, List richTextData = null) + protected static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle highlightedSlot) { - GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, richTextData); + GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot); } public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) @@ -1155,7 +1154,7 @@ namespace Barotrauma { foreach (int i in indices) { - inventory.visualSlots[i]?.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + inventory.visualSlots[i]?.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } break; @@ -1248,7 +1247,7 @@ namespace Barotrauma selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } } - selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } SoundPlayer.PlayUISound(GUISoundType.PickItem); } @@ -1289,7 +1288,7 @@ namespace Barotrauma } else { - if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } SoundPlayer.PlayUISound(GUISoundType.PickItemFail); } } @@ -1439,20 +1438,20 @@ namespace Barotrauma if ((GUI.MouseOn == null || mouseOnHealthInterface) && selectedSlot == null) { - var shadowSprite = GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; - string toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : + var shadowSprite = GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0]; + LocalizedString toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : Character.Controlled.FocusedItem != null ? - TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, true) : + TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, FormatCapitals.Yes) : TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); - int textWidth = (int)Math.Max(GUI.Font.MeasureString(DraggingItems.First().Name).X, GUI.SmallFont.MeasureString(toolTip).X); + int textWidth = (int)Math.Max(GUIStyle.Font.MeasureString(DraggingItems.First().Name).X, GUIStyle.SmallFont.MeasureString(toolTip).X); int textSpacing = (int)(15 * GUI.Scale); Point shadowBorders = (new Point(40, 10)).Multiply(GUI.Scale); shadowSprite.Draw(spriteBatch, new Rectangle(itemPos.ToPoint() - new Point(iconSize / 2) - shadowBorders, new Point(iconSize + textWidth + textSpacing, iconSize) + shadowBorders.Multiply(2)), Color.Black * 0.8f); GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y - iconSize / 2), DraggingItems.First().Name, Color.White); GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y), toolTip, - color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUI.Style.Red : Color.LightGreen, - font: GUI.SmallFont); + color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUIStyle.Red : Color.LightGreen, + font: GUIStyle.SmallFont); } sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black, scale: scale); sprite.Draw(spriteBatch, @@ -1464,8 +1463,8 @@ namespace Barotrauma { Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; string stackCountText = "x" + DraggingItems.Count; - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } } } @@ -1478,7 +1477,7 @@ namespace Barotrauma { selectedSlot.RefreshTooltip(); } - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipRichTextData); + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect); } } @@ -1514,11 +1513,11 @@ namespace Barotrauma /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) { - slotColor = slot.IsHighlighted ? GUI.Style.EquipmentSlotColor : GUI.Style.EquipmentSlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUIStyle.EquipmentSlotColor : GUIStyle.EquipmentSlotColor * 0.8f; } else { - slotColor = slot.IsHighlighted ? GUI.Style.InventorySlotColor : GUI.Style.InventorySlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUIStyle.InventorySlotColor : GUIStyle.InventorySlotColor * 0.8f; }*/ if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } @@ -1526,7 +1525,7 @@ namespace Barotrauma if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown() && selectedSlot?.Slot == slot) { - GUI.DrawRectangle(spriteBatch, rect, GUI.Style.Red * 0.3f, isFilled: true); + GUI.DrawRectangle(spriteBatch, rect, GUIStyle.Red * 0.3f, isFilled: true); } bool canBePut = false; @@ -1550,7 +1549,7 @@ namespace Barotrauma } if (slot.MouseOn() && canBePut && selectedSlot?.Slot == slot) { - GUI.UIGlow.Draw(spriteBatch, rect, GUI.Style.Green); + GUIStyle.UIGlow.Draw(spriteBatch, rect, GUIStyle.Green); } if (item != null && drawItem) @@ -1571,7 +1570,7 @@ namespace Barotrauma conditionIndicatorArea.Inflate(-4, 0); } - var indicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); + var indicatorStyle = GUIStyle.GetComponentStyle("ContainedStateIndicator.Default"); Sprite indicatorSprite = indicatorStyle?.GetDefaultSprite(); Sprite emptyIndicatorSprite = indicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover); DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, conditionIndicatorArea, item.Condition / item.MaxCondition); @@ -1624,14 +1623,14 @@ namespace Barotrauma if (item.Quality != 0) { - var style = GUI.Style.GetComponentStyle("InnerGlowSmall"); + var style = GUIStyle.GetComponentStyle("InnerGlowSmall"); if (style == null) { - GUI.DrawRectangle(spriteBatch, rect, GUI.Style.GetQualityColor(item.Quality) * 0.7f); + GUI.DrawRectangle(spriteBatch, rect, GUIStyle.GetQualityColor(item.Quality) * 0.7f); } else { - style.Sprites[GUIComponent.ComponentState.None].FirstOrDefault()?.Draw(spriteBatch, rect, GUI.Style.GetQualityColor(item.Quality) * 0.5f); + style.Sprites[GUIComponent.ComponentState.None].FirstOrDefault()?.Draw(spriteBatch, rect, GUIStyle.GetQualityColor(item.Quality) * 0.5f); } } } @@ -1640,7 +1639,7 @@ namespace Barotrauma var slotIcon = parentItem?.GetComponent()?.GetSlotIcon(slotIndex); if (slotIcon != null) { - slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUI.Style.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f); + slotIcon.Draw(spriteBatch, rect.Center.ToVector2(), GUIStyle.EquipmentSlotIconColor, scale: Math.Min(rect.Width / slotIcon.size.X, rect.Height / slotIcon.size.Y) * 0.8f); } } } @@ -1653,7 +1652,7 @@ namespace Barotrauma if (slot.HighlightColor != Color.Transparent) { - GUI.UIGlow.Draw(spriteBatch, rect, slot.HighlightColor); + GUIStyle.UIGlow.Draw(spriteBatch, rect, slot.HighlightColor); } if (item != null && drawItem) @@ -1693,7 +1692,7 @@ namespace Barotrauma stealIcon.Draw( spriteBatch, new Vector2(rect.X + iconSize.X * 0.2f, rect.Bottom - iconSize.Y * 1.2f), - color: GUI.Style.Red, + color: GUIStyle.Red, scale: iconSize.X / stealIcon.size.X); } int maxStackSize = item.Prefab.MaxStackSize; @@ -1708,9 +1707,9 @@ namespace Barotrauma { Vector2 stackCountPos = new Vector2(rect.Right, rect.Bottom); string stackCountText = "x" + itemCount; - stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); - GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); } } } @@ -1721,7 +1720,7 @@ namespace Barotrauma slot.InventoryKeyIndex != -1) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameMain.Config.InventoryKeyBind(slot.InventoryKeyIndex).Name, Color.Black, font: GUI.HotkeyFont); + GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameSettings.CurrentConfig.InventoryKeyMap.Bindings[slot.InventoryKeyIndex].Name, Color.Black, font: GUIStyle.HotkeyFont); } } @@ -1730,7 +1729,7 @@ namespace Barotrauma Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState, bool pulsate = false) { - Color backgroundColor = GUI.Style.ColorInventoryBackground; + Color backgroundColor = GUIStyle.ColorInventoryBackground; if (indicatorSprite == null) { @@ -1738,7 +1737,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true); GUI.DrawRectangle(spriteBatch, new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height), - ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull) * 0.8f, true); + ToolBox.GradientLerp(containedState, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull) * 0.8f, true); GUI.DrawLine(spriteBatch, new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y), new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom), @@ -1763,7 +1762,7 @@ namespace Barotrauma if (containedState > 0.0f) { - Color indicatorColor = ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull); + Color indicatorColor = ToolBox.GradientLerp(containedState, GUIStyle.ColorInventoryEmpty, GUIStyle.ColorInventoryHalf, GUIStyle.ColorInventoryFull); if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), @@ -1784,7 +1783,7 @@ namespace Barotrauma } else if (emptyIndicatorSprite != null) { - Color indicatorColor = GUI.Style.ColorInventoryEmptyOverlay; + Color indicatorColor = GUIStyle.ColorInventoryEmptyOverlay; if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } emptyIndicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 86b76aebb..011ec97e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -11,6 +11,7 @@ using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; +using System.Collections.Immutable; namespace Barotrauma { @@ -33,7 +34,7 @@ namespace Barotrauma } else { - IconStyle = GUI.Style.GetComponentStyle($"CampaignInteractionIcon.{interactionType}"); + IconStyle = GUIStyle.GetComponentStyle($"CampaignInteractionIcon.{interactionType}"); } } @@ -48,6 +49,8 @@ namespace Barotrauma private readonly Dictionary spriteAnimState = new Dictionary(); + public float DrawDepthOffset; + private bool fakeBroken; public bool FakeBroken { @@ -90,7 +93,7 @@ namespace Barotrauma if (itemInUseWarning == null) { itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", - textColor: GUI.Style.Orange, color: Color.Black, + textColor: GUIStyle.Orange, color: Color.Black, textAlignment: Alignment.Center, style: "OuterGlow"); } return itemInUseWarning; @@ -101,7 +104,7 @@ namespace Barotrauma { get { - if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(Prefab.Subcategory)) { return false; } @@ -114,7 +117,7 @@ namespace Barotrauma public float GetDrawDepth() { - return GetDrawDepth(SpriteDepth, Sprite); + return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } public Color GetSpriteColor() @@ -147,7 +150,7 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific() { - activeSprite = prefab.sprite; + activeSprite = Prefab.Sprite; activeContainedSprite = null; Holdable holdable = GetComponent(); if (holdable != null && holdable.Attached) @@ -179,7 +182,7 @@ namespace Barotrauma } float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; - for (int i = 0; i < Prefab.BrokenSprites.Count;i++) + for (int i = 0; i < Prefab.BrokenSprites.Length;i++) { if (Prefab.BrokenSprites[i].FadeIn) { continue; } float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f; @@ -193,14 +196,14 @@ namespace Barotrauma partial void InitProjSpecific() { - Prefab.sprite?.EnsureLazyLoaded(); + Prefab.Sprite?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded(); foreach (BrokenItemSprite brokenSprite in Prefab.BrokenSprites) { brokenSprite.Sprite.EnsureLazyLoaded(); } - foreach (var decorativeSprite in ((ItemPrefab)prefab).DecorativeSprites) + foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); @@ -269,7 +272,7 @@ namespace Barotrauma else if (!ShowItems) { return; } } - Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); + Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUIStyle.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -283,7 +286,7 @@ namespace Barotrauma Vector2 drawOffset = Vector2.Zero; if (displayCondition < MaxCondition) { - for (int i = 0; i < Prefab.BrokenSprites.Count; i++) + for (int i = 0; i < Prefab.BrokenSprites.Length; i++) { if (Prefab.BrokenSprites[i].FadeIn) { @@ -320,7 +323,7 @@ namespace Barotrauma if (body == null) { - if (prefab.ResizeHorizontal || prefab.ResizeVertical) + if (Prefab.ResizeHorizontal || Prefab.ResizeVertical) { Vector2 size = new Vector2(rect.Width, rect.Height); if (color.A > 0) @@ -380,9 +383,11 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); - 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; } + bool flipX = flippedX && Prefab.CanSpriteFlipX; + bool flipY = flippedY && Prefab.CanSpriteFlipY; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? rotationRad : -rotationRad) * Scale; + if (flipX) { offset.X = -offset.X; } + if (flipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); @@ -518,7 +523,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, rectWorldPos, new Vector2(transformedTrigger.Width, transformedTrigger.Height), - GUI.Style.Green, + GUIStyle.Green, false, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); @@ -529,8 +534,8 @@ namespace Barotrauma foreach (MapEntity e in linkedTo) { - bool isLinkAllowed = prefab.IsLinkAllowed(e.prefab); - Color lineColor = GUI.Style.Red * 0.5f; + bool isLinkAllowed = Prefab.IsLinkAllowed(e.Prefab); + Color lineColor = GUIStyle.Red * 0.5f; if (isLinkAllowed) { lineColor = e is Item i && (DisplaySideBySideWhenLinked || i.DisplaySideBySideWhenLinked) ? Color.Purple * 0.5f : Color.LightGreen * 0.5f; @@ -685,7 +690,7 @@ namespace Barotrauma Spacing = (int)(25 * GUI.Scale) }; - var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; + var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; activeEditors.Add(itemEditor); itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) @@ -694,21 +699,17 @@ namespace Barotrauma var itemContainer = GetComponent(); if (itemContainer != null) { - var tagsField = itemEditor.Fields["Tags"].First().Parent; + var tagsField = itemEditor.Fields["Tags".ToIdentifier()].First().Parent; //find all the items that can be put inside the container and add their PreferredContainer identifiers/tags to the available tags - HashSet availableTags = new HashSet(); - foreach (MapEntityPrefab me in MapEntityPrefab.List) - { - if (!(me is ItemPrefab ip)) { continue; } - if (!itemContainer.CanBeContained(ip)) { continue; } - foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Primary)) { availableTags.Add(tag); } - foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Secondary)) { availableTags.Add(tag); } - } - //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) - availableTags.RemoveWhere(t => MapEntityPrefab.List.Any(me => me.Identifier == t)); + ImmutableHashSet availableTags = ItemPrefab.Prefabs + .Where(ip => itemContainer.CanBeContained(ip)) + .SelectMany(ip => ip.PreferredContainers.SelectMany(pc => pc.Primary.Union(pc.Secondary))) + //remove identifiers from the available container tags + //(otherwise the list will include many irrelevant options, + //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) + .Where(t => !ItemPrefab.Prefabs.Any(ip => ip.Identifier == t)) + .ToImmutableHashSet(); new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") { OnClicked = (bt, userData) => { CreateTagPicker(tagsField.GetChild(), availableTags); return true; } @@ -717,14 +718,14 @@ namespace Barotrauma if (Linkable) { - var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUI.SmallFont); - var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUI.SmallFont); - string allowedItems = AllowedLinks.None() ? TextManager.Get("None") :string.Join(", ", AllowedLinks); + var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont); + var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUIStyle.SmallFont); + LocalizedString allowedItems = AllowedLinks.None() ? TextManager.Get("None") : string.Join(", ", AllowedLinks); itemsText.Text = TextManager.AddPunctuation(':', itemsText.Text, allowedItems); itemEditor.AddCustomContent(linkText, 1); itemEditor.AddCustomContent(itemsText, 2); - linkText.TextColor = GUI.Style.Orange; - itemsText.TextColor = GUI.Style.Orange; + linkText.TextColor = GUIStyle.Orange; + itemsText.TextColor = GUIStyle.Orange; } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) @@ -795,7 +796,7 @@ namespace Barotrauma { GUITickBox tickBox = new GUITickBox(new RectTransform(new Point(listBox.Content.Rect.Width, 10)), TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.name")) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Selected = RemoveIfLinkedOutpostDoorInUse, ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), OnSelected = (tickBox) => @@ -826,7 +827,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); - var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUI.SubHeadingFont) { UserData = ic }; + var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic }; componentEditor.Children.First().Color = Color.Black * 0.7f; activeEditors.Add(componentEditor); @@ -851,7 +852,7 @@ namespace Barotrauma { //TODO: add to localization var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), - relatedItem.Type.ToString() + " required", font: GUI.SmallFont) + relatedItem.Type.ToString() + " required", font: GUIStyle.SmallFont) { Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f) }; @@ -860,7 +861,7 @@ namespace Barotrauma GUITextBox namesBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight)) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Text = relatedItem.JoinedIdentifiers, OverflowClip = true }; @@ -890,7 +891,7 @@ namespace Barotrauma return editingHUD; } - private List GetUpgradeSprites(Upgrade upgrade) + private ImmutableArray GetUpgradeSprites(Upgrade upgrade) { var upgradeSprites = upgrade.Prefab.DecorativeSprites; @@ -908,7 +909,7 @@ namespace Barotrauma bool result = base.AddUpgrade(upgrade, createNetworkEvent); if (result && !upgrade.Disposed) { - List upgradeSprites = GetUpgradeSprites(upgrade); + var upgradeSprites = GetUpgradeSprites(upgrade); if (upgradeSprites.Any()) { @@ -923,9 +924,9 @@ namespace Barotrauma return result; } - private void CreateTagPicker(GUITextBox textBox, IEnumerable availableTags) + private void CreateTagPicker(GUITextBox textBox, IEnumerable availableTags) { - var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -940,10 +941,10 @@ namespace Barotrauma } }; - foreach (string availableTag in availableTags.ToList().OrderBy(t => t)) + foreach (var availableTag in availableTags.ToList().OrderBy(t => t)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(availableTag, GUI.Font, textList.Content.Rect.Width)) + ToolBox.LimitString(availableTag.Value, GUIStyle.Font, textList.Content.Rect.Width)) { UserData = availableTag }; @@ -998,11 +999,11 @@ namespace Barotrauma GameMain.GraphicsWidth, HUDLayoutSettings.InventoryTopY > 0 ? HUDLayoutSettings.InventoryTopY - 40 : GameMain.GraphicsHeight - 80)); + //System.Diagnostics.Debug.WriteLine("after: " + elementsToMove[0].Rect.ToString() + " " + elementsToMove[1].Rect.ToString()); foreach (ItemComponent ic in activeHUDs) { if (ic.GuiFrame == null) { continue; } - var linkUIToComponent = ic.GetLinkUIToComponent(); if (linkUIToComponent == null) { continue; } @@ -1041,7 +1042,7 @@ namespace Barotrauma foreach (MapEntity entity in linkedTo) { - if (prefab.IsLinkAllowed(entity.prefab) && entity is Item i) + if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) { if (!i.DisplaySideBySideWhenLinked) { continue; } activeComponents.AddRange(i.components); @@ -1179,17 +1180,17 @@ namespace Barotrauma nameText += $" ({idName})"; } } - texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); + texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, false, false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) { - texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameMain.Config.KeyBindText(InputType.Use)), Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, false, false)); } else { foreach (ItemComponent ic in components) { - if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } + if (ic.DisplayMsg.IsNullOrEmpty()) { continue; } if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } @@ -1205,12 +1206,12 @@ namespace Barotrauma color = Color.Cyan; } } - texts.Add(new ColoredText(ic.DisplayMsg, color, false, false)); + texts.Add(new ColoredText(ic.DisplayMsg.Value, color, false, false)); } } if (PlayerInput.IsShiftDown() && CrewManager.DoesItemHaveContextualOrders(this)) { - texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false, false)); + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, false, false)); } return texts; } @@ -1350,7 +1351,7 @@ namespace Barotrauma ReadPropertyChange(msg, false); break; case NetEntityEvent.Type.Upgrade: - string identifier = msg.ReadString(); + Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); if (UpgradePrefab.Find(identifier) is { } upgradePrefab) { @@ -1458,7 +1459,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif GameAnalyticsManager.AddErrorEventOnce("Item.ClientReadPosition:nophysicsbody", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; @@ -1490,10 +1491,10 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = false (" + body.PhysEnabled + ")", e); - if (body.UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + body.UserData.GetType().ToString(), GUI.Style.Red); - if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", GUI.Style.Red); - else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", GUI.Style.Red); - if (body.FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", GUI.Style.Red); + if (body.UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + body.UserData.GetType().ToString(), GUIStyle.Red); + if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", GUIStyle.Red); + else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", GUIStyle.Red); + if (body.FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", GUIStyle.Red); } } @@ -1579,8 +1580,8 @@ namespace Barotrauma string tags = ""; if (tagsChanged) { - string[] addedTags = msg.ReadString().Split(','); - string[] removedTags = msg.ReadString().Split(','); + HashSet addedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet(); + HashSet removedTags = msg.ReadString().Split(',').ToIdentifiers().ToHashSet(); if (itemPrefab != null) { tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags)); @@ -1600,7 +1601,7 @@ namespace Barotrauma if (itemPrefab == null) { string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")"; - errorMsg += "\n" + string.Join(", ", GameMain.Config.AllEnabledPackages.Select(cp => cp.Name)); + errorMsg += "\n" + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name)); GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:PrefabNotFound" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); @@ -1621,7 +1622,7 @@ namespace Barotrauma if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count) { string errorMsg = - $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; + $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.Prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 274eabdf9..860e59d1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -1,8 +1,11 @@ -using FarseerPhysics; +using Barotrauma.IO; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -35,58 +38,175 @@ namespace Barotrauma public readonly Sprite Sprite; public readonly bool UseWhenAttached; public readonly DecorativeSpriteBehaviorType DecorativeSpriteBehavior; - public readonly string[] AllowedContainerIdentifiers; - public readonly string[] AllowedContainerTags; + public readonly ImmutableHashSet AllowedContainerIdentifiers; + public readonly ImmutableHashSet AllowedContainerTags; - public ContainedItemSprite(XElement element, string path = "", bool lazyLoad = false) + public ContainedItemSprite(ContentXElement element, string path = "", bool lazyLoad = false) { Sprite = new Sprite(element, path, lazyLoad: lazyLoad); UseWhenAttached = element.GetAttributeBool("usewhenattached", false); Enum.TryParse(element.GetAttributeString("decorativespritebehavior", "None"), ignoreCase: true, out DecorativeSpriteBehavior); - AllowedContainerIdentifiers = element.GetAttributeStringArray("allowedcontaineridentifiers", new string[0], convertToLowerInvariant: true); - AllowedContainerTags = element.GetAttributeStringArray("allowedcontainertags", new string[0], convertToLowerInvariant: true); + AllowedContainerIdentifiers = element.GetAttributeIdentifierArray("allowedcontaineridentifiers", Array.Empty()).ToImmutableHashSet(); + AllowedContainerTags = element.GetAttributeIdentifierArray("allowedcontainertags", Array.Empty()).ToImmutableHashSet(); } public bool MatchesContainer(Item container) { if (container == null) { return false; } - return AllowedContainerIdentifiers.Contains(container.prefab.Identifier) || - AllowedContainerTags.Any(t => container.prefab.Tags.Contains(t)); + return AllowedContainerIdentifiers.Contains(container.Prefab.Identifier) || + AllowedContainerTags.Any(t => container.Prefab.Tags.Contains(t)); } } - partial class ItemPrefab : MapEntityPrefab + partial class ItemPrefab : MapEntityPrefab, IImplementsVariants { - public List BrokenSprites = new List(); - public List DecorativeSprites = new List(); - public List ContainedSprites = new List(); - public Dictionary> DecorativeSpriteGroups = new Dictionary>(); - public Sprite InventoryIcon; - public Sprite MinimapIcon; - public Sprite UpgradePreviewSprite; - public Sprite InfectedSprite; - public Sprite DamagedInfectedSprite; + public ImmutableDictionary> UpgradeOverrideSprites { get; private set; } + public ImmutableArray BrokenSprites { get; private set; } + public ImmutableArray DecorativeSprites { get; private set; } + public ImmutableArray ContainedSprites { get; private set; } + public ImmutableDictionary> DecorativeSpriteGroups { get; private set; } + public Sprite InventoryIcon { get; private set; } + public Sprite MinimapIcon { get; private set; } + public Sprite UpgradePreviewSprite { get; private set; } + public Sprite InfectedSprite { get; private set; } + public Sprite DamagedInfectedSprite { get; private set; } public float UpgradePreviewScale = 1.0f; //only used to display correct color in the sub editor, item instances have their own property that can be edited on a per-item basis - [Serialize("1.0,1.0,1.0,1.0", false)] - public Color InventoryIconColor - { - get; - protected set; - } + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] + public Color InventoryIconColor { get; protected set; } - [Serialize(true, false)] + [Serialize("", IsPropertySaveable.No)] + public string ImpactSoundTag { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] public bool ShowInStatusMonitor { get; private set; } + private void ParseSubElementsClient(ContentXElement element, ItemPrefab variantOf) + { + UpgradePreviewSprite = null; + UpgradePreviewScale = 1f; + InventoryIcon = null; + MinimapIcon = null; + InfectedSprite = null; + DamagedInfectedSprite = null; + var upgradeOverrideSprites = new Dictionary>(); + var brokenSprites = new List(); + var decorativeSprites = new List(); + var containedSprites = new List(); + var decorativeSpriteGroups = new Dictionary>(); - [Serialize("", false)] - public string ImpactSoundTag { get; private set; } + foreach (var subElement in element.Elements()) + { + switch (subElement.Name.LocalName.ToLowerInvariant()) + { + case "upgradeoverride": + { + + var sprites = new List(); + foreach (var decorSprite in subElement.Elements()) + { + if (decorSprite.NameAsIdentifier() == "decorativesprite") + { + sprites.Add(new DecorativeSprite(decorSprite)); + } + } + upgradeOverrideSprites.Add(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), sprites); + break; + } + case "upgradepreviewsprite": + { + string iconFolder = GetTexturePath(subElement, variantOf); + UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); + } + break; + case "inventoryicon": + { + string iconFolder = GetTexturePath(subElement, variantOf); + InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "minimapicon": + { + string iconFolder = GetTexturePath(subElement, variantOf); + MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "infectedsprite": + { + string iconFolder = GetTexturePath(subElement, variantOf); + + InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "damagedinfectedsprite": + { + string iconFolder = GetTexturePath(subElement, variantOf); + + DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "brokensprite": + string brokenSpriteFolder = GetTexturePath(subElement, variantOf); + + var brokenSprite = new BrokenItemSprite( + new Sprite(subElement, brokenSpriteFolder, lazyLoad: true), + subElement.GetAttributeFloat("maxcondition", 0.0f), + subElement.GetAttributeBool("fadein", false), + subElement.GetAttributePoint("offset", Point.Zero)); + + int spriteIndex = 0; + for (int i = 0; i < brokenSprites.Count && brokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) + { + spriteIndex = i; + } + brokenSprites.Insert(spriteIndex, brokenSprite); + break; + case "decorativesprite": + string decorativeSpriteFolder = GetTexturePath(subElement, variantOf); + + int groupID = 0; + DecorativeSprite decorativeSprite = null; + if (subElement.Attribute("texture") == null) + { + groupID = subElement.GetAttributeInt("randomgroupid", 0); + } + else + { + decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); + decorativeSprites.Add(decorativeSprite); + groupID = decorativeSprite.RandomGroupID; + } + if (!decorativeSpriteGroups.ContainsKey(groupID)) + { + decorativeSpriteGroups.Add(groupID, new List()); + } + decorativeSpriteGroups[groupID].Add(decorativeSprite); + + break; + case "containedsprite": + string containedSpriteFolder = GetTexturePath(subElement, variantOf); + var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true); + if (containedSprite.Sprite != null) + { + containedSprites.Add(containedSprite); + } + break; + } + } + + UpgradeOverrideSprites = upgradeOverrideSprites.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + BrokenSprites = brokenSprites.ToImmutableArray(); + DecorativeSprites = decorativeSprites.ToImmutableArray(); + ContainedSprites = containedSprites.ToImmutableArray(); + DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + } public override void UpdatePlacing(Camera cam) { @@ -94,7 +214,7 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } @@ -104,7 +224,7 @@ namespace Barotrauma { if (PlayerInput.PrimaryMouseButtonClicked()) { - var item = new Item(new Rectangle((int)position.X, (int)position.Y, (int)(sprite.size.X * Scale), (int)(sprite.size.Y * Scale)), this, Submarine.MainSub) + var item = new Item(new Rectangle((int)position.X, (int)position.Y, (int)(Sprite.size.X * Scale), (int)(Sprite.size.Y * Scale)), this, Submarine.MainSub) { Submarine = Submarine.MainSub }; @@ -128,7 +248,7 @@ namespace Barotrauma } else { - Vector2 placeSize = size * Scale; + Vector2 placeSize = Size * Scale; if (placePosition == Vector2.Zero) { @@ -137,9 +257,9 @@ namespace Barotrauma else { if (ResizeHorizontal) - placeSize.X = Math.Max(position.X - placePosition.X, size.X); + placeSize.X = Math.Max(position.X - placePosition.X, Size.X); if (ResizeVertical) - placeSize.Y = Math.Max(placePosition.Y - position.Y, size.Y); + placeSize.Y = Math.Max(placePosition.Y - position.Y, Size.Y); if (PlayerInput.PrimaryMouseButtonReleased()) { @@ -174,24 +294,24 @@ namespace Barotrauma if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } if (!ResizeHorizontal && !ResizeVertical) { - sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); + Sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + Sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); } else { - Vector2 placeSize = size * Scale; + Vector2 placeSize = Size * Scale; if (placePosition != Vector2.Zero) { if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } position = placePosition; } - sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); } } @@ -199,19 +319,19 @@ namespace Barotrauma { if (!ResizeHorizontal && !ResizeVertical) { - sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale); + Sprite.Draw(spriteBatch, new Vector2(placeRect.Center.X, -(placeRect.Y - placeRect.Height / 2)), SpriteColor * 0.8f, scale: scale); } else { Vector2 position = Submarine.MouseToWorldGrid(Screen.Selected.Cam, Submarine.MainSub); - Vector2 placeSize = size * Scale; + Vector2 placeSize = Size * Scale; if (placePosition != Vector2.Zero) { if (ResizeHorizontal) { placeSize.X = Math.Max(position.X - placePosition.X, placeSize.X); } if (ResizeVertical) { placeSize.Y = Math.Max(placePosition.Y - position.Y, placeSize.Y); } position = placePosition; } - sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + Sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 04169a6fa..80d814e5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -16,79 +16,35 @@ namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { - - // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global, UnusedAutoPropertyAccessor.Global, MemberCanBePrivate.Global - internal class DamageParticle - { - [Serialize(defaultValue: "", isSaveable: false)] - public string Identifier { get; set; } = ""; - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MinRotation { get; set; } - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MaxRotation { get; set; } - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MinVelocity { get; set; } - - [Serialize(defaultValue: 0f, isSaveable: false)] - public float MaxVelocity { get; set; } - - [Serialize(defaultValue: "255,255,255,255", isSaveable: false)] - public Color ColorMultiplier { get; set; } - - private float RandRotation() => Rand.Range(MinRotation, MaxRotation); - private float RandVelocity() => Rand.Range(MinVelocity, MaxVelocity); - - public void Emit(Vector2 pos) - { - Particle particle = GameMain.ParticleManager.CreateParticle(Identifier, pos, RandRotation(), RandVelocity()); - if (particle != null) - { - particle.ColorMultiplier = ColorMultiplier.ToVector4(); - } - } - - public DamageParticle(XElement element) - { - SerializableProperty.DeserializeProperties(this, element); - } - } - public Sprite? branchAtlas, decayAtlas; public readonly Dictionary BranchSprites = new Dictionary(); public readonly List FlowerSprites = new List(), DamagedFlowerSprites = new List(); public readonly List HiddenFlowerSprites = new List(); public readonly List LeafSprites = new List(), DamagedLeafSprites = new List(); - public readonly List DamageParticles = new List(); - public readonly List DeathParticles = new List(); + public readonly List DamageParticles = new List(); + public readonly List DeathParticles = new List(); public static bool AlwaysShowBallastFloraSprite = false; - partial void LoadPrefab(XElement element) + partial void LoadPrefab(ContentXElement element) { - string? branchAtlasPath = element.GetAttributeString("branchatlas", null); - string? decayAtlasPath = element.GetAttributeString("decayatlas", null); - - if (branchAtlasPath != null) + if (element.GetAttributeContentPath("branchatlas") is { } branchAtlasPath) { - branchAtlas = new Sprite(branchAtlasPath, Rectangle.Empty); - } - - if (decayAtlasPath != null) - { - decayAtlas = new Sprite(decayAtlasPath, Rectangle.Empty); + branchAtlas = new Sprite(branchAtlasPath.Value, Rectangle.Empty); } - foreach (XElement subElement in element.Elements()) + if (element.GetAttributeContentPath("decayatlas") is { } decayAtlasPath) + { + decayAtlas = new Sprite(decayAtlasPath.Value, Rectangle.Empty); + } + + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "branchsprite": - var tileType = subElement.GetAttributeString("type", null); - VineTileType type = Enum.Parse(tileType); + var type = subElement.GetAttributeEnum("type", VineTileType.Stem); BranchSprites.Add(type, new VineSprite(subElement)); break; case "flowersprite": @@ -107,10 +63,10 @@ namespace Barotrauma.MapCreatures.Behavior DamagedLeafSprites.Add(new Sprite(subElement)); break; case "damageparticle": - DamageParticles.Add(new DamageParticle(subElement)); + DamageParticles.Add(new ParticleEmitter(subElement)); break; case "deathparticle": - DeathParticles.Add(new DamageParticle(subElement)); + DeathParticles.Add(new ParticleEmitter(subElement)); break; case "targets": LoadTargets(subElement); @@ -131,29 +87,59 @@ namespace Barotrauma.MapCreatures.Behavior } } - private void CreateDamageParticle(BallastFloraBranch branch, float damage) - { - Vector2 pos = GetWorldPosition() + branch.Position; - int amount = (int)Math.Clamp(damage / 10f, 1, 10); - for (int i = 0; i < amount; i++) + partial void UpdateDamage(float deltaTime) + { + foreach (BallastFloraBranch branch in Branches) { - foreach (DamageParticle particle in DamageParticles) + if (branch.AccumulatedDamage > 0) { - particle.Emit(pos); + CreateDamageParticle(branch, branch.AccumulatedDamage); + + if (GameMain.DebugDraw) + { + var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position; + GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUIStyle.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1); + } + } + if (Character.Controlled != null && Character.Controlled.CurrentHull == branch.CurrentHull && + branch.IsRoot && + (branch.AccumulatedDamage > 0.0f || branch.AccumulatedDamage < -0.1f)) + { + Character.Controlled.UpdateHUDProgressBar(this, + GetWorldPosition() + branch.Position, + branch.Health / branch.MaxHealth, + emptyColor: GUIStyle.HealthBarColorLow, + fullColor: GUIStyle.HealthBarColorHigh, + textTag: Prefab.DisplayName.Value); + } + branch.AccumulatedDamage = 0f; + if (branch.DamageVisualizationTimer > 0.0f) + { + branch.DamageVisualizationTimer -= deltaTime; + float t1 = (float)Timing.TotalTime * 0.2f + branch.Position.X / 100.0f; + float t2 = (float)Timing.TotalTime * 0.5f + branch.Position.Y / 100.0f; + branch.ShakeAmount = new Vector2( + PerlinNoise.GetPerlin(t1, t2) - 0.5f, + PerlinNoise.GetPerlin(t2, t1) - 0.5f) * 10.0f * branch.DamageVisualizationTimer; } } } - private void CreateDeathParticle(BallastFloraBranch branch) + private void CreateDamageParticle(BallastFloraBranch branch, float deltaTime) { Vector2 pos = GetWorldPosition() + branch.Position; - int amount = (int)Math.Clamp(branch.MaxHealth / 10f, 1, 10); - for (int i = 0; i < amount; i++) + foreach (var particleEmitter in DamageParticles) { - foreach (DamageParticle particle in DeathParticles) - { - particle.Emit(pos); - } + particleEmitter.Emit(deltaTime, pos, branch.CurrentHull); + } + } + + private void CreateDeathParticle(BallastFloraBranch branch, float deltaTime) + { + Vector2 pos = GetWorldPosition() + branch.Position; + foreach (var particleEmitter in DeathParticles) + { + particleEmitter.Emit(deltaTime, pos, branch.CurrentHull); } } @@ -169,25 +155,25 @@ namespace Barotrauma.MapCreatures.Behavior { Vector2 pos = Parent.Submarine.DrawPosition + ConvertUnits.ToDisplayUnits(body.Position); pos.Y = -pos.Y; - GUI.DrawRectangle(spriteBatch, pos, 32f, 32f, 0f, Color.Cyan, 0.1f, thickness: 1); + GUI.DrawRectangle(spriteBatch, pos, 32f, 32f, 0f, body.UserData is BallastFloraBranch { IsRoot: true } ? Color.Magenta : Color.Cyan, 0.1f, thickness: 1); } foreach (var (key, steps) in IgnoredTargets) { string label = $"Ignored \"{key.Name}\" for {steps} steps"; - var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(label); + var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(label); Vector2 targetPos = key.WorldPosition; targetPos.Y = -targetPos.Y; - GUI.DrawString(spriteBatch, targetPos - new Vector2(sizeX / 2f, sizeY), label, GUI.Style.Red, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, targetPos - new Vector2(sizeX / 2f, sizeY), label, GUIStyle.Red, font: GUIStyle.SubHeadingFont); } } foreach (BallastFloraBranch branch in Branches) { - Vector2 pos = Parent.DrawPosition + Offset + branch.Position; + Vector2 pos = Parent.DrawPosition + Offset + branch.Position + branch.ShakeAmount; pos.Y = -pos.Y; - float depth = BranchDepth; + float depth = branch.IsRootGrowth ? 0.2f : BranchDepth; float layer1 = depth + 0.01f, layer2 = depth + 0.02f, @@ -195,7 +181,7 @@ namespace Barotrauma.MapCreatures.Behavior VineSprite branchSprite = BranchSprites[branch.Type]; - Color branchColor = Color.White; + Color branchColor = (branch.IsRoot || branch.IsRootGrowth) ? RootColor : Color.White; if (GameMain.DebugDraw) { @@ -207,7 +193,13 @@ namespace Barotrauma.MapCreatures.Behavior pos1.Y = -pos1.Y; Vector2 pos2 = basePos - to; pos2.Y = -pos2.Y; - GUI.DrawLine(spriteBatch, pos1, pos2, GUI.Style.Yellow * 0.8f, width: 4); + GUI.DrawLine(spriteBatch, pos1, pos2, GUIStyle.Yellow * 0.8f, width: 4); + } + if (branch.ParentBranch != null) + { + Vector2 pos2 = Parent.DrawPosition + Offset + branch.ParentBranch.Position; + pos2.Y = -pos2.Y; + GUI.DrawLine(spriteBatch, pos, pos2, GUIStyle.Green * 0.8f, width: 3); } #endif @@ -235,8 +227,8 @@ namespace Barotrauma.MapCreatures.Behavior } } - var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(label); - GUI.DrawString(spriteBatch, pos - new Vector2(sizeX / 2f, branch.Rect.Height + sizeY), label, Color.White, font: GUI.SubHeadingFont); + var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(label); + GUI.DrawString(spriteBatch, pos - new Vector2(sizeX / 2f, branch.Rect.Height + sizeY), label, Color.White, font: GUIStyle.SubHeadingFont); } bool isDamaged = branch.Health < branch.MaxHealth; @@ -340,7 +332,7 @@ namespace Barotrauma.MapCreatures.Behavior case NetworkHeader.BranchRemove: int removedBranchId = msg.ReadInt32(); - BallastFloraBranch removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); + BallastFloraBranch? removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); if (removedBranch != null) { RemoveBranch(removedBranch); @@ -352,14 +344,11 @@ namespace Barotrauma.MapCreatures.Behavior break; case NetworkHeader.BranchDamage: - int damageBranchId = msg.ReadInt32(); - float damage = msg.ReadSingle(); float health = msg.ReadSingle(); - BallastFloraBranch damagedBranch = Branches.FirstOrDefault(b => b.ID == damageBranchId); + BallastFloraBranch? damagedBranch = Branches.FirstOrDefault(b => b.ID == damageBranchId); if (damagedBranch != null) { - CreateDamageParticle(damagedBranch, damage); damagedBranch.Health = health; } else @@ -370,6 +359,9 @@ namespace Barotrauma.MapCreatures.Behavior case NetworkHeader.Kill: Kill(); break; + case NetworkHeader.Remove: + Remove(); + break; } PowerConsumptionTimer = msg.ReadSingle(); @@ -378,15 +370,18 @@ namespace Barotrauma.MapCreatures.Behavior private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); - byte type = (byte) msg.ReadRangedInteger(0b0000, 0b1111); - byte sides = (byte) msg.ReadRangedInteger(0b0000, 0b1111); + byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); + byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); int leafConfig = msg.ReadRangedInteger(0, 0xFFF); int maxHealth = msg.ReadUInt16(); int posX = msg.ReadInt32(), posY = msg.ReadInt32(); + int parentBranchIndex = msg.ReadInt32(); Vector2 pos = new Vector2(posX * VineTile.Size, posY * VineTile.Size); - return new BallastFloraBranch(this, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) + BallastFloraBranch? parentBranch = parentBranchIndex < 0 || parentBranchIndex >= Branches.Count ? null : Branches[parentBranchIndex]; + + return new BallastFloraBranch(this, parentBranch, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) { ID = id, MaxHealth = maxHealth, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index afc015edf..57979fdd0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -46,7 +46,7 @@ namespace Barotrauma if (!editing || !ShowGaps || !SubEditorScreen.IsLayerVisible(this)) { return; } - Color clr = (open == 0.0f) ? GUI.Style.Red : Color.Cyan; + Color clr = (open == 0.0f) ? GUIStyle.Red : Color.Cyan; if (IsHighlighted) { clr = Color.Gold; } GUI.DrawRectangle( @@ -118,7 +118,7 @@ namespace Barotrauma GUI.DrawRectangle(sb, new Vector2(WorldRect.X - 5, -WorldRect.Y - 5), new Vector2(rect.Width + 10, rect.Height + 10), - GUI.Style.Red, + GUIStyle.Red, false, depth, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 6a01d168d..b14c94720 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; namespace Barotrauma @@ -107,7 +108,7 @@ namespace Barotrauma { CanTakeKeyBoardFocus = false }; - new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont); + new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont); PositionEditingHUD(); @@ -285,7 +286,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(drawRect.X, -drawRect.Y), new Vector2(rect.Width, rect.Height), - (IsHighlighted ? Color.LightBlue * 0.8f : GUI.Style.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + (IsHighlighted ? Color.LightBlue * 0.8f : GUIStyle.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); } GUI.DrawRectangle(spriteBatch, @@ -295,34 +296,34 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), - GUI.Style.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); + GUIStyle.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); if (GameMain.DebugDraw) { - GUI.SmallFont.DrawString(spriteBatch, "Pressure: " + ((int)pressure - rect.Y).ToString() + + GUIStyle.SmallFont.DrawString(spriteBatch, "Pressure: " + ((int)pressure - rect.Y).ToString() + " - Oxygen: " + ((int)OxygenPercentage), new Vector2(drawRect.X + 5, -drawRect.Y + 5), Color.White); - GUI.SmallFont.DrawString(spriteBatch, waterVolume + " / " + Volume, new Vector2(drawRect.X + 5, -drawRect.Y + 20), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, waterVolume + " / " + Volume, new Vector2(drawRect.X + 5, -drawRect.Y + 20), Color.White); GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * Math.Min(waterVolume / Volume, 1.0f))), Color.Cyan, true); if (WaterVolume > Volume) { float maxExcessWater = Volume * MaxCompress; - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUIStyle.Red, true); } GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, 100), Color.Black); foreach (FireSource fs in FireSources) { Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); - GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); - GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUIStyle.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUIStyle.Orange, false, 0, 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } foreach (FireSource fs in FakeFireSources) { Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); - GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); - GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUIStyle.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUIStyle.Orange, false, 0, 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } @@ -358,7 +359,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(currentHullRect.X, -currentHullRect.Y), new Vector2(connectedHullRect.X, -connectedHullRect.Y), - GUI.Style.Green, width: 2); + GUIStyle.Green, width: 2); } } } @@ -376,7 +377,7 @@ namespace Barotrauma if (section.ColorStrength < 0.01f || section.Color.A < 1) { continue; } - if (GameMain.DecalManager.GrimeSprites.Count == 0) + if (DecalManager.GrimeSprites.None()) { GUI.DrawRectangle(spriteBatch, new Vector2(drawOffset.X + rect.X + section.Rect.X, -(drawOffset.Y + rect.Y + section.Rect.Y)), @@ -387,7 +388,7 @@ namespace Barotrauma { Vector2 sectionPos = new Vector2(drawPos.X + section.Rect.Location.X, -(drawPos.Y + section.Rect.Location.Y)); Vector2 randomOffset = new Vector2(section.Noise.X - 0.5f, section.Noise.Y - 0.5f) * 15.0f; - var sprite = GameMain.DecalManager.GrimeSprites[i % GameMain.DecalManager.GrimeSprites.Count]; + var sprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{i % DecalManager.GrimeSpriteCount}"].Sprite; sprite.Draw(spriteBatch, sectionPos + randomOffset, section.GetStrengthAdjustedColor(), scale: 1.25f); } } @@ -648,7 +649,7 @@ namespace Barotrauma BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) message.ReadByte(); if (header == BallastFloraBehavior.NetworkHeader.Spawn) { - string identifier = message.ReadString(); + Identifier identifier = message.ReadIdentifier(); float x = message.ReadSingle(); float y = message.ReadSingle(); BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index f20bd6797..0db354aea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - partial class ItemAssemblyPrefab + partial class ItemAssemblyPrefab : MapEntityPrefab { public void DrawIcon(SpriteBatch spriteBatch, GUICustomComponent guiComponent) { @@ -16,28 +16,28 @@ namespace Barotrauma float scale = Math.Min(drawArea.Width / (float)Bounds.Width, drawArea.Height / (float)Bounds.Height) * 0.9f; - foreach (Pair entity in DisplayEntities) + foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) { - if (entity.First is CoreEntityPrefab) { continue; } - Rectangle drawRect = entity.Second; - drawRect = new Rectangle( - (int)(drawRect.X * scale) + drawArea.Center.X, (int)((drawRect.Y) * scale) - drawArea.Center.Y, - (int)(drawRect.Width * scale), (int)(drawRect.Height * scale)); - entity.First.DrawPlacing(spriteBatch, drawRect, entity.First.Scale * scale); + var entityPrefab = MapEntityPrefab.FindByIdentifier(identifier); + if (entityPrefab is CoreEntityPrefab) { continue; } + var drawRect = new Rectangle( + (int)(rect.X * scale) + drawArea.Center.X, (int)((rect.Y) * scale) - drawArea.Center.Y, + (int)(rect.Width * scale), (int)(rect.Height * scale)); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale * scale); } } - public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) { base.DrawPlacing(spriteBatch, cam); - foreach (Pair entity in DisplayEntities) + foreach ((Identifier identifier, Rectangle rect) in DisplayEntities) { - Rectangle drawRect = entity.Second; + var entityPrefab = MapEntityPrefab.Find(p => p.Identifier == identifier); + Rectangle drawRect = rect; drawRect.Location += placePosition != Vector2.Zero ? placePosition.ToPoint() : Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); - entity.First.DrawPlacing(spriteBatch, drawRect, entity.First.Scale); + entityPrefab.DrawPlacing(spriteBatch, drawRect, entityPrefab.Scale); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs index ad29039b0..b7aae9022 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs @@ -90,7 +90,7 @@ namespace Barotrauma checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval, Rand.RandSync.ClientOnly); - foreach (XElement subElement in prefab.Config.Elements()) + foreach (var subElement in prefab.Config.Elements()) { List deformationList = null; switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index b8ef5800c..7dc0b6ad4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -18,45 +18,46 @@ namespace Barotrauma private readonly List prefabs = new List(); private readonly List creatures = new List(); - public BackgroundCreatureManager(string configPath) - { - LoadConfig(new ContentFile(configPath, ContentType.BackgroundCreaturePrefabs)); - } - - public BackgroundCreatureManager(IEnumerable files) + public BackgroundCreatureManager(IEnumerable files) { foreach(var file in files) { - LoadConfig(file); + LoadConfig(file.Path); } } - private void LoadConfig(ContentFile config) + public BackgroundCreatureManager(string path) + { + DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}"); + LoadConfig(ContentPath.FromRaw(null, path)); + } + + private void LoadConfig(ContentPath configPath) { try { - XDocument doc = XMLExtensions.TryLoadXml(config.Path); + XDocument doc = XMLExtensions.TryLoadXml(configPath); if (doc == null) { return; } - var mainElement = doc.Root; + var mainElement = doc.Root.FromPackage(configPath.ContentPackage); if (mainElement.IsOverride()) { - mainElement = doc.Root.FirstElement(); + mainElement = mainElement.FirstElement(); prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all background creatures with '{config.Path}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.Yellow); } else if (prefabs.Any()) { - DebugConsole.NewMessage($"Loading additional background creatures from file '{config.Path}'"); + DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'"); } - foreach (XElement element in mainElement.Elements()) + foreach (var element in mainElement.Elements()) { prefabs.Add(new BackgroundCreaturePrefab(element)); }; } catch (Exception e) { - DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", config.Path), e); + DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs index 30939a39a..7c9ef97f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs @@ -12,52 +12,52 @@ namespace Barotrauma public readonly XElement Config; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Speed { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float WanderAmount { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float WanderZAmount { get; private set; } - [Serialize(1, true)] + [Serialize(1, IsPropertySaveable.Yes)] public int SwarmMin { get; private set; } - [Serialize(1, true)] + [Serialize(1, IsPropertySaveable.Yes)] public int SwarmMax { get; private set; } - [Serialize(200.0f, true)] + [Serialize(200.0f, IsPropertySaveable.Yes)] public float SwarmRadius { get; private set; } - [Serialize(0.2f, true)] + [Serialize(0.2f, IsPropertySaveable.Yes)] public float SwarmCohesion { get; private set; } - [Serialize(10.0f, true)] + [Serialize(10.0f, IsPropertySaveable.Yes)] public float MinDepth { get; private set; } - [Serialize(1000.0f, true)] + [Serialize(1000.0f, IsPropertySaveable.Yes)] public float MaxDepth { get; private set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DisableRotation { get; private set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DisableFlipping { get; private set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Scale { get; private set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Commonness { get; private set; } - [Serialize(1000, true)] + [Serialize(1000, IsPropertySaveable.Yes)] public int MaxCount { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float FlashInterval { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float FlashDuration { get; private set; } @@ -65,9 +65,9 @@ namespace Barotrauma /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// - public Dictionary OverrideCommonness = new Dictionary(); + public Dictionary OverrideCommonness = new Dictionary(); - public BackgroundCreaturePrefab(XElement element) + public BackgroundCreaturePrefab(ContentXElement element) { Name = element.Name.ToString(); @@ -75,7 +75,7 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -92,7 +92,7 @@ namespace Barotrauma DeformableLightSprite = new DeformableSprite(subElement, lazyLoad: true); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -104,9 +104,10 @@ namespace Barotrauma public float GetCommonness(LevelGenerationParams generationParams) { - if (generationParams?.Identifier != null && + if (generationParams != null && + !generationParams.Identifier.IsEmpty && (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || - (generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) { return commonness; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index 7396a16b5..ce1348882 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -130,12 +130,12 @@ namespace Barotrauma SoundTriggers = new LevelTrigger[Prefab.Sounds.Count]; for (int i = 0; i < Prefab.Sounds.Count; i++) { - Sounds[i] = Submarine.LoadRoundSound(Prefab.Sounds[i].SoundElement, false); + Sounds[i] = RoundSound.Load(Prefab.Sounds[i].SoundElement, false); SoundTriggers[i] = Prefab.Sounds[i].TriggerIndex > -1 ? Triggers[Prefab.Sounds[i].TriggerIndex] : null; } int j = 0; - foreach (XElement subElement in Prefab.Config.Elements()) + foreach (var subElement in Prefab.Config.Elements()) { if (!subElement.Name.ToString().Equals("deformablesprite", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (XElement animationElement in subElement.Elements()) @@ -151,7 +151,7 @@ namespace Barotrauma } VisibleOnSonar = Prefab.SonarDisruption > 0.0f || Prefab.OverrideProperties.Any(p => p != null && p.SonarDisruption > 0.0f) || - (Triggers != null && Triggers.Any(t => !MathUtils.NearlyEqual(t.Force, Vector2.Zero) && t.ForceMode != LevelTrigger.TriggerForceMode.LimitVelocity || !string.IsNullOrWhiteSpace(t.InfectIdentifier))); + (Triggers != null && Triggers.Any(t => !MathUtils.NearlyEqual(t.Force, Vector2.Zero) && t.ForceMode != LevelTrigger.TriggerForceMode.LimitVelocity || !t.InfectIdentifier.IsEmpty)); if (VisibleOnSonar && Triggers.Any()) { SonarRadius = Triggers.Select(t => t.ColliderRadius * 1.5f).Max(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index f4ecea93d..76bf3c231 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -206,7 +206,7 @@ namespace Barotrauma if (GameMain.DebugDraw) { - GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true); if (obj.Triggers == null) { continue; } foreach (LevelTrigger trigger in obj.Triggers) @@ -218,7 +218,7 @@ namespace Barotrauma if (flowForce.LengthSquared() > 1) { flowForce.Y = -flowForce.Y; - GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUI.Style.Orange, 0, 5); + GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5); } trigger.PhysicsBody.UpdateDrawPosition(); trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index ebf0a6c6c..19b490b62 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -9,15 +9,15 @@ using System.Xml.Linq; namespace Barotrauma { - partial class LevelObjectPrefab + partial class LevelObjectPrefab : PrefabWithUintIdentifier, ISerializableEntity { public class SoundConfig { - public readonly XElement SoundElement; + public readonly ContentXElement SoundElement; public readonly Vector2 Position; public readonly int TriggerIndex; - public SoundConfig(XElement element, int triggerIndex) + public SoundConfig(ContentXElement element, int triggerIndex) { SoundElement = element; Position = element.GetAttributeVector2("position", Vector2.Zero); @@ -67,14 +67,14 @@ namespace Barotrauma private set; } = new List(); - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific(ContentXElement element) { LoadElementsProjSpecific(element, -1); } - private void LoadElementsProjSpecific(XElement element, int parentTriggerIndex) + private void LoadElementsProjSpecific(ContentXElement element, int parentTriggerIndex) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -121,7 +121,7 @@ namespace Barotrauma SerializableProperty.SerializeProperties(this, element); - foreach (XElement subElement in element.Elements().ToList()) + foreach (var subElement in element.Elements().ToList()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -144,7 +144,7 @@ namespace Barotrauma { int elementIndex = 0; bool wasSaved = false; - foreach (XElement subElement in element.Elements().ToList()) + foreach (var subElement in element.Elements().ToList()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -175,13 +175,13 @@ namespace Barotrauma new XAttribute("maxcount", childObj.MaxCount))); } - foreach (KeyValuePair overrideCommonness in OverrideCommonness) + foreach (KeyValuePair overrideCommonness in OverrideCommonness) { bool elementFound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("overridecommonness", System.StringComparison.OrdinalIgnoreCase) - && subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, System.StringComparison.OrdinalIgnoreCase)) + && subElement.GetAttributeIdentifier("leveltype", Identifier.Empty) == overrideCommonness.Key) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); elementFound = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 8028f686a..475553ddf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -326,7 +326,7 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(nodeList[i - 1].X, -nodeList[i - 1].Y), new Vector2(nodeList[i].X, -nodeList[i].Y), - Color.Lerp(Color.Yellow, GUI.Style.Red, i / (float)nodeList.Count), 0, 10); + Color.Lerp(Color.Yellow, GUIStyle.Red, i / (float)nodeList.Count), 0, 10); } }*/ diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index eccf97ae5..0483137ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -388,7 +388,7 @@ namespace Barotrauma.Lights if (!BoundingBox.Contains(point)) { return false; } Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f; - for (int i=0;i<4;i++) + for (int i = 0; i < 4; i++) { Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos; Vector2 centerToVertex = center - vertices[i].Pos; @@ -695,7 +695,7 @@ namespace Barotrauma.Lights } ShadowVertexCount = 0; - for (int i=0;i<4;i++) + for (int i = 0; i < 4; i++) { if (!backFacing[i]) { continue; } int currentIndex = i; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index f7826794f..420a52d66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Lights { var pp = graphics.PresentationParameters; - currLightMapScale = GameMain.Config.LightMapScale; + currLightMapScale = GameSettings.CurrentConfig.Graphics.LightMapScale; LightMap?.Dispose(); LightMap = CreateRenderTarget(); @@ -120,15 +120,15 @@ namespace Barotrauma.Lights RenderTarget2D CreateRenderTarget() { return new RenderTarget2D(graphics, - (int)(GameMain.GraphicsWidth * GameMain.Config.LightMapScale), (int)(GameMain.GraphicsHeight * GameMain.Config.LightMapScale), false, + (int)(GameMain.GraphicsWidth * GameSettings.CurrentConfig.Graphics.LightMapScale), (int)(GameMain.GraphicsHeight * GameSettings.CurrentConfig.Graphics.LightMapScale), false, pp.BackBufferFormat, pp.DepthStencilFormat, pp.MultiSampleCount, RenderTargetUsage.DiscardContents); } LosTexture?.Dispose(); LosTexture = new RenderTarget2D(graphics, - (int)(GameMain.GraphicsWidth * GameMain.Config.LightMapScale), - (int)(GameMain.GraphicsHeight * GameMain.Config.LightMapScale), false, SurfaceFormat.Color, DepthFormat.None); + (int)(GameMain.GraphicsWidth * GameSettings.CurrentConfig.Graphics.LightMapScale), + (int)(GameMain.GraphicsHeight * GameSettings.CurrentConfig.Graphics.LightMapScale), false, SurfaceFormat.Color, DepthFormat.None); } public void AddLight(LightSource light) @@ -165,13 +165,13 @@ namespace Barotrauma.Lights { if (!LightingEnabled) { return; } - if (Math.Abs(currLightMapScale - GameMain.Config.LightMapScale) > 0.01f) + if (Math.Abs(currLightMapScale - GameSettings.CurrentConfig.Graphics.LightMapScale) > 0.01f) { //lightmap scale has changed -> recreate render targets CreateRenderTargets(graphics); } - Matrix spriteBatchTransform = cam.Transform * Matrix.CreateScale(new Vector3(GameMain.Config.LightMapScale, GameMain.Config.LightMapScale, 1.0f)); + Matrix spriteBatchTransform = cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f)); Matrix transform = cam.ShaderTransform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; @@ -187,6 +187,7 @@ namespace Barotrauma.Lights if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } if (light.ParentBody != null) { + light.ParentBody.UpdateDrawPosition(); light.Position = light.ParentBody.DrawPosition; if (light.ParentSub != null) { light.Position -= light.ParentSub.DrawPosition; } } @@ -501,7 +502,7 @@ namespace Barotrauma.Lights private Dictionary GetVisibleHulls(Camera cam) { visibleHulls.Clear(); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.HiddenInGame) { continue; } var drawRect = @@ -537,7 +538,7 @@ namespace Barotrauma.Lights Vector2 scale = new Vector2( MathHelper.Clamp(losOffset.Length() / 256.0f, 4.0f, 5.0f), 3.0f); - spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameMain.Config.LightMapScale, GameMain.Config.LightMapScale, 1.0f))); + spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform * Matrix.CreateScale(new Vector3(GameSettings.CurrentConfig.Graphics.LightMapScale, GameSettings.CurrentConfig.Graphics.LightMapScale, 1.0f))); spriteBatch.Draw(visionCircle, new Vector2(ViewTarget.WorldPosition.X, -ViewTarget.WorldPosition.Y), null, Color.White, rotation, new Vector2(visionCircle.Width * 0.2f, visionCircle.Height / 2), scale, SpriteEffects.None, 0.0f); spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 151b148dd..0eacc74f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -15,9 +15,9 @@ namespace Barotrauma.Lights public bool Persistent; - public Dictionary SerializableProperties { get; private set; } = new Dictionary(); + public Dictionary SerializableProperties { get; private set; } = new Dictionary(); - [Serialize("1.0,1.0,1.0,1.0", true, alwaysUseInstanceValues: true), Editable] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable] public Color Color { get; @@ -26,7 +26,7 @@ namespace Barotrauma.Lights private float range; - [Serialize(100.0f, true, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] + [Serialize(100.0f, IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { get { return range; } @@ -43,19 +43,19 @@ namespace Barotrauma.Lights } } - [Serialize(1f, true), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(minValue: 0.01f, maxValue: 100f, ValueStep = 0.1f, DecimalCount = 2)] public float Scale { get; set; } - [Serialize("0, 0", true), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] + [Serialize("0, 0", IsPropertySaveable.Yes), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] public Vector2 Offset { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] public float Rotation { get; set; } public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(Rotation)); private float flicker; - [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] public float Flicker { get { return flicker; } @@ -65,7 +65,7 @@ namespace Barotrauma.Lights } } - [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + [Editable, Serialize(1.0f, IsPropertySaveable.No, description: "How fast the light flickers.")] public float FlickerSpeed { get; @@ -73,7 +73,7 @@ namespace Barotrauma.Lights } private float pulseFrequency; - [Editable, Serialize(0.0f, true, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] public float PulseFrequency { get { return pulseFrequency; } @@ -84,7 +84,7 @@ namespace Barotrauma.Lights } private float pulseAmount; - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, true, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] public float PulseAmount { get { return pulseAmount; } @@ -95,7 +95,7 @@ namespace Barotrauma.Lights } private float blinkFrequency; - [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { get { return blinkFrequency; } @@ -124,7 +124,7 @@ namespace Barotrauma.Lights private set; } - public XElement DeformableLightSpriteElement + public ContentXElement DeformableLightSpriteElement { get; private set; @@ -134,11 +134,11 @@ namespace Barotrauma.Lights //Can be used to make lamp sprites glow at full brightness even if the light itself is dim. public float? OverrideLightSpriteAlpha; - public LightSourceParams(XElement element) + public LightSourceParams(ContentXElement element) { Deserialize(element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -427,7 +427,7 @@ namespace Barotrauma.Lights private readonly PropertyConditional.Comparison comparison; private readonly List conditionals = new List(); - public LightSource (XElement element, ISerializableEntity conditionalTarget = null) + public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) : this(Vector2.Zero, 100.0f, Color.White, null) { lightSourceParams = new LightSourceParams(element); @@ -444,7 +444,7 @@ namespace Barotrauma.Lights } this.conditionalTarget = conditionalTarget; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1211,7 +1211,7 @@ namespace Barotrauma.Lights new SegmentPoint(new Vector2(drawPos.X - bounds, drawPos.Y + bounds), null) }; - for (int i=0;i<4;i++) + for (int i = 0; i < 4; i++) { GUI.DrawLine(spriteBatch, boundaryCorners[i].Pos, boundaryCorners[(i + 1) % 4].Pos, Color.White, 0, 3); } @@ -1283,16 +1283,16 @@ namespace Barotrauma.Lights if (CastShadows && Screen.Selected == GameMain.SubEditorScreen) { - GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, GUI.Style.Orange, isFilled: false); - GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, GUI.Style.Orange); - GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, GUI.Style.Orange); + GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 20, Vector2.One * 40, GUIStyle.Orange, isFilled: false); + GUI.DrawLine(spriteBatch, drawPos - Vector2.One * 20, drawPos + Vector2.One * 20, GUIStyle.Orange); + GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * 20, drawPos + new Vector2(1.0f, -1.0f) * 20, GUIStyle.Orange); } //visualize light recalculations float timeSinceRecalculation = (float)Timing.TotalTime - lastRecalculationTime; if (timeSinceRecalculation < 0.1f) { - GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUI.Style.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true); + GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUIStyle.Red * (1.0f - timeSinceRecalculation * 10.0f), isFilled: true); GUI.DrawLine(spriteBatch, drawPos - Vector2.One * Range, drawPos + Vector2.One * Range, Color); GUI.DrawLine(spriteBatch, drawPos - new Vector2(1.0f, -1.0f) * Range, drawPos + new Vector2(1.0f, -1.0f) * Range, Color); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 2dc444032..897e86a23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -1,8 +1,8 @@ -using Barotrauma.Items.Components; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; -using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -25,14 +25,14 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(WorldPosition.X, -WorldPosition.Y), new Vector2(e.WorldPosition.X, -e.WorldPosition.Y), - isLinkAllowed ? GUI.Style.Green * 0.5f : GUI.Style.Red * 0.5f, width: 3); + isLinkAllowed ? GUIStyle.Green * 0.5f : GUIStyle.Red * 0.5f, width: 3); } } public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, float alpha = 1.0f) { - Color color = (IsHighlighted) ? GUI.Style.Orange : GUI.Style.Green; - if (IsSelected) { color = GUI.Style.Red; } + Color color = (IsHighlighted) ? GUIStyle.Orange : GUIStyle.Green; + if (IsSelected) { color = GUIStyle.Red; } Vector2 pos = drawPos; @@ -98,16 +98,16 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("LinkedSub"), font: GUI.LargeFont); + TextManager.Get("LinkedSub"), font: GUIStyle.LargeFont); if (!inGame) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("LinkLinkedSub"), textColor: GUI.Style.Orange, font: GUI.SmallFont); + TextManager.Get("LinkLinkedSub"), textColor: GUIStyle.Orange, font: GUIStyle.SmallFont); } var pathContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true); - var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUI.SmallFont); + var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUIStyle.SmallFont); var reloadButton = new GUIButton(new RectTransform(new Vector2(0.25f / pathBox.RectTransform.RelativeSize.X, 1.0f), pathBox.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") { @@ -132,20 +132,21 @@ namespace Barotrauma if (!File.Exists(pathBox.Text)) { new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariable("ReloadLinkedSubError", "[file]", pathBox.Text)); - pathBox.Flash(GUI.Style.Red); + pathBox.Flash(GUIStyle.Red); pathBox.Text = filePath; return false; } XDocument doc = SubmarineInfo.OpenFile(pathBox.Text); - if (doc == null || doc.Root == null) return false; + if (doc == null || doc.Root == null) { return false; } doc.Root.SetAttributeValue("filepath", pathBox.Text); - pathBox.Flash(GUI.Style.Green); + pathBox.Flash(GUIStyle.Green); GenerateWallVertices(doc.Root); saveElement = doc.Root; saveElement.Name = "LinkedSubmarine"; + CargoCapacity = doc.Root.GetAttributeInt("cargocapacity", 0); filePath = pathBox.Text; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 2c34707c6..0f85e7fda 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.Xna.Framework.Input; @@ -65,10 +66,7 @@ namespace Barotrauma private float connectionHighlightState; - private (Rectangle targetArea, string tip)? tooltip; - private string sanitizedTooltip; - private List tooltipRichTextData; - private string prevTooltip; + private (Rectangle targetArea, RichString tip)? tooltip; private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; @@ -136,7 +134,7 @@ namespace Barotrauma for (int y = 0; y < tilesY; y++) { var biome = GetBiome(x * tileSize.X); - List tileList = null; + ImmutableArray tileList; if (generationParams.MapTiles.ContainsKey(biome.Identifier)) { tileList = generationParams.MapTiles[biome.Identifier]; @@ -146,7 +144,7 @@ namespace Barotrauma tileList = generationParams.MapTiles.Values.First(); missingBiomes.Add(biome); } - mapTiles[x, y] = tileList[x % tileList.Count]; + mapTiles[x, y] = tileList[x % tileList.Length]; } } @@ -208,7 +206,7 @@ namespace Barotrauma { if (location == null) { return; } - var mapTile = generationParams.MapTiles.Values.FirstOrDefault()?.FirstOrDefault(); + var mapTile = generationParams.MapTiles.Values.FirstOrDefault().FirstOrDefault(); if (mapTile == null) { return; } Vector2 mapTileSize = mapTile.size * generationParams.MapTileScale; @@ -253,12 +251,12 @@ namespace Barotrauma location.LastTypeChangeMessage = msg; if (GameMain.Client != null) { - GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName")); + GameMain.Client.AddChatMessage(msg, Networking.ChatMessageType.Default, TextManager.Get("RadioAnnouncerName").Value); } else { GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage( - TextManager.Get("RadioAnnouncerName"), + TextManager.Get("RadioAnnouncerName").Value, msg, Networking.ChatMessageType.Default, sender: null); @@ -617,7 +615,7 @@ namespace Barotrauma { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; - generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUI.Style.Red, scale: typeChangeIconScale * zoom); + generationParams.TypeChangeIcon.Draw(spriteBatch, typeChangeIconPos, GUIStyle.Red, scale: typeChangeIconScale * zoom); if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { @@ -646,8 +644,8 @@ namespace Barotrauma Vector2 dPos = pos; dPos.Y += 48; string name = $"Reputation: {location.Name}"; - Vector2 nameSize = GUI.SmallFont.MeasureString(name); - GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUI.SmallFont); + Vector2 nameSize = GUIStyle.SmallFont.MeasureString(name); + GUI.DrawString(spriteBatch, dPos, name, Color.White, Color.Black * 0.8f, 4, font: GUIStyle.SmallFont); dPos.Y += nameSize.Y + 16; Rectangle bgRect = new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32); @@ -656,8 +654,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.8f, isFilled: true); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, (int)(location.Reputation.NormalizedValue * 255), 32), barColor, isFilled: true); string reputationValue = ((int)location.Reputation.Value).ToString(); - Vector2 repValueSize = GUI.SubHeadingFont.MeasureString(reputationValue); - GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUI.SubHeadingFont); + Vector2 repValueSize = GUIStyle.SubHeadingFont.MeasureString(reputationValue); + GUI.DrawString(spriteBatch, dPos + (new Vector2(256, 32) / 2) - (repValueSize / 2), reputationValue, Color.White, Color.Black, font: GUIStyle.SubHeadingFont); GUI.DrawRectangle(spriteBatch, new Rectangle((int)dPos.X, (int)dPos.Y, 256, 32), Color.White); } } @@ -670,12 +668,7 @@ namespace Barotrauma if (tooltip != null) { - if (tooltip.Value.tip != prevTooltip) - { - prevTooltip = tooltip.Value.tip; - tooltipRichTextData = RichTextData.GetRichTextData(tooltip.Value.tip, out sanitizedTooltip); - } - GUIComponent.DrawToolTip(spriteBatch, sanitizedTooltip, tooltip.Value.targetArea, tooltipRichTextData); + GUIComponent.DrawToolTip(spriteBatch, tooltip.Value.tip, tooltip.Value.targetArea); drawRadiationTooltip = false; } else if (HighlightedLocation != null) @@ -685,35 +678,35 @@ namespace Barotrauma pos.X += 50 * zoom; pos.X = (int)pos.X; pos.Y = (int)pos.Y; - Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = string.IsNullOrEmpty(HighlightedLocation.Type.Name) ? Vector2.Zero : GUI.Font.MeasureString(HighlightedLocation.Type.Name); + Vector2 nameSize = GUIStyle.LargeFont.MeasureString(HighlightedLocation.Name); + Vector2 typeSize = HighlightedLocation.Type.Name.IsNullOrEmpty() ? Vector2.Zero : GUIStyle.Font.MeasureString(HighlightedLocation.Type.Name); Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; - string repLabelText = null, repValueText = null; + LocalizedString repLabelText = null, repValueText = null; Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; if (showReputation) { repLabelText = TextManager.Get("reputation"); - repLabelSize = GUI.Font.MeasureString(repLabelText); + repLabelSize = GUIStyle.Font.MeasureString(repLabelText); repBarSize = new Vector2(GUI.IntScale(200), repLabelSize.Y); size.Y += 2 * repLabelSize.Y + GUI.IntScale(5) + repBarSize.Y; repValueText = HighlightedLocation.Reputation.GetFormattedReputationText(addColorTags: false); - size.X = Math.Max(size.X, repBarSize.X + GUI.Font.MeasureString(repValueText).X + GUI.IntScale(10)); + size.X = Math.Max(size.X, repBarSize.X + GUIStyle.Font.MeasureString(repValueText).X + GUI.IntScale(10)); } - GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( + GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( spriteBatch, new Rectangle((int)(pos.X - 60 * GUI.Scale), (int)(pos.Y - size.Y), (int)(size.X + 120 * GUI.Scale), (int)(size.Y * 2.2f)), Color.Black * hudVisibility); var topLeftPos = pos - new Vector2(0.0f, size.Y / 2); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUI.Style.TextColor * hudVisibility * 1.5f, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f, font: GUIStyle.LargeFont); topLeftPos += new Vector2(0.0f, nameSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUI.Style.TextColor * hudVisibility * 1.5f); + GUI.DrawString(spriteBatch, topLeftPos, HighlightedLocation.Type.Name, GUIStyle.TextColorNormal * hudVisibility * 1.5f); if (showReputation) { topLeftPos += new Vector2(0.0f, typeSize.Y + repLabelSize.Y); - GUI.DrawString(spriteBatch, topLeftPos, repLabelText, GUI.Style.TextColor * hudVisibility * 1.5f); + GUI.DrawString(spriteBatch, topLeftPos, repLabelText.Value, GUIStyle.TextColorNormal * hudVisibility * 1.5f); topLeftPos += new Vector2(0.0f, repLabelSize.Y + GUI.IntScale(10)); Rectangle repBarRect = new Rectangle(new Point((int)topLeftPos.X, (int)topLeftPos.Y), new Point((int)repBarSize.X, (int)repBarSize.Y)); RoundSummary.DrawReputationBar(spriteBatch, repBarRect, HighlightedLocation.Reputation.NormalizedValue); - GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); + GUI.DrawString(spriteBatch, new Vector2(repBarRect.Right + GUI.IntScale(5), repBarRect.Top), repValueText.Value, Reputation.GetReputationColor(HighlightedLocation.Reputation.NormalizedValue)); } } @@ -763,7 +756,7 @@ namespace Barotrauma generationParams.SmallLevelConnectionLength, generationParams.LargeLevelConnectionLength, connection.Length); - connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUI.Style.Orange, GUI.Style.Red); + connectionColor = ToolBox.GradientLerp(sizeFactor, Color.LightGreen, GUIStyle.Orange, GUIStyle.Red); } else if (overrideColor.HasValue) { @@ -894,14 +887,14 @@ namespace Barotrauma } if (GameMain.GameSession?.Campaign?.UpgradeManager != null) { - var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth"); + var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth".ToIdentifier()); if (hullUpgradePrefab != null) { int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); int currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); if (pendingLevel > currentLevel) { - string updateValueStr = hullUpgradePrefab.SourceElement?.Element("Structure")?.GetAttributeString("crushdepth", null); + string updateValueStr = hullUpgradePrefab.SourceElement?.GetChildElement("Structure")?.GetAttributeString("crushdepth", null); if (!string.IsNullOrEmpty(updateValueStr)) { subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel - currentLevel, updateValueStr); @@ -934,8 +927,8 @@ namespace Barotrauma { var gateLocation = connection.Locations[0].IsGateBetweenBiomes ? connection.Locations[0] : connection.Locations[1]; var unlockEvent = - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? - EventSet.PrefabList.Find(ep => ep.UnlockPathEvent && string.IsNullOrEmpty(ep.BiomeIdentifier)); + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == gateLocation.LevelData.Biome.Identifier) ?? + EventPrefab.Prefabs.FirstOrDefault(ep => ep.UnlockPathEvent && ep.BiomeIdentifier == Identifier.Empty); if (unlockEvent != null) { @@ -943,15 +936,15 @@ namespace Barotrauma Faction unlockFaction = null; if (!string.IsNullOrEmpty(unlockEvent.UnlockPathFaction)) { - unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier.Equals(unlockEvent.UnlockPathFaction, StringComparison.OrdinalIgnoreCase)); + unlockFaction = GameMain.GameSession.Campaign.Factions.Find(f => f.Prefab.Identifier == unlockEvent.UnlockPathFaction); unlockReputation = unlockFaction?.Reputation; } DrawIcon( "LockedLocationConnection", (int)(28 * zoom), TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", - new string[] { "[requiredreputation]", "[currentreputation]" }, - new string[] { Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true), unlockReputation.GetFormattedReputationText(addColorTags: true) })); + ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), + ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true)))); } else { @@ -968,9 +961,9 @@ namespace Barotrauma if (crushDepthWarningIconStyle != null) { DrawIcon(crushDepthWarningIconStyle, (int)(32 * zoom), - TextManager.Get(tooltip) - .Replace("[initialdepth]", $"‖color:gui.orange‖{(int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)}‖end‖") - .Replace("[submarinecrushdepth]", $"‖color:gui.orange‖{(int)subCrushDepth}‖end‖")); + RichString.Rich(TextManager.GetWithVariables(tooltip, + ("[initialdepth]", $"‖color:gui.orange‖{(int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)}‖end‖"), + ("[submarinecrushdepth]", $"‖color:gui.orange‖{(int)subCrushDepth}‖end‖")))); } } @@ -983,14 +976,14 @@ namespace Barotrauma } } - void DrawIcon(string iconStyle, int iconSize, string tooltipText) + void DrawIcon(string iconStyle, int iconSize, LocalizedString tooltipText) { Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize; iconPos += (iconDiff * -(iconCount - 1) / 2.0f) + iconDiff * iconIndex; - var style = GUI.Style.GetComponentStyle(iconStyle); + var style = GUIStyle.GetComponentStyle(iconStyle); bool mouseOn = Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize && IsPreferredTooltip(iconPos); Sprite iconSprite = style.GetDefaultSprite(); iconSprite.Draw(spriteBatch, iconPos, (mouseOn ? style.HoverColor : style.Color) * 0.7f, @@ -1018,10 +1011,10 @@ namespace Barotrauma GUI.DrawString(spriteBatch, new Vector2(rect.Right - GUI.IntScale(170), rect.Y + GUI.IntScale(5)), - "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUI.SmallFont); + "JOVIAN FLUX " + ((cameraNoiseStrength + Rand.Range(-0.02f, 0.02f)) * 500), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, new Vector2(rect.X + GUI.IntScale(15), rect.Bottom - GUI.IntScale(25)), - "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUI.SmallFont); + "LAT " + (-DrawOffset.Y / 100.0f) + " LON " + (-DrawOffset.X / 100.0f), generationParams.IndicatorColor * hudVisibility, font: GUIStyle.SmallFont); } private void UpdateMapAnim(MapAnim anim, float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs index 990d91775..2ed19962f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -7,9 +7,9 @@ namespace Barotrauma { internal partial class Radiation { - private static readonly string radiationTooltip = TextManager.Get("RadiationTooltip"); + private static readonly LocalizedString radiationTooltip = TextManager.Get("RadiationTooltip"); private static float spriteIndex; - private readonly SpriteSheet sheet = GUI.Style.RadiationAnimSpriteSheet; + private readonly SpriteSheet sheet = GUIStyle.RadiationAnimSpriteSheet; private int maxFrames => sheet.FrameCount + 1; private bool isHovingOver; @@ -18,7 +18,7 @@ namespace Barotrauma { if (!Enabled) { return; } - UISprite uiSprite = GUI.Style.RadiationSprite; + UISprite uiSprite = GUIStyle.Radiation; var (offsetX, offsetY) = Map.DrawOffset * zoom; var (centerX, centerY) = container.Center.ToVector2(); var (halfSizeX, halfSizeY) = new Vector2(container.Width / 2f, container.Height / 2f) * zoom; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 1c9c15096..0b0bec78b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -610,7 +610,7 @@ namespace Barotrauma foreach (MapEntity entity in highlightedEntities) { - var tooltip = string.Empty; + LocalizedString tooltip = string.Empty; if (wiringMode && entity is Item item) { @@ -622,9 +622,9 @@ namespace Barotrauma var conn = wire.Connections[i]; if (conn != null) { - string[] tags = { "[item]", "[pin]" }; - string[] values = { conn.Item?.Name, conn.Name }; - tooltip += TextManager.GetWithVariables("wirelistformat",tags , values); + tooltip += TextManager.GetWithVariables("wirelistformat", + ("[item]", conn.Item?.Name), + ("[pin]", conn.Name)); } if (i != wire.Connections.Length - 1) { tooltip += '\n'; } } @@ -632,7 +632,7 @@ namespace Barotrauma } var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform), - ToolBox.LimitString(entity.Name, GUI.SmallFont, 140), font: GUI.SmallFont) + ToolBox.LimitString(entity.Name, GUIStyle.SmallFont, 140), font: GUIStyle.SmallFont) { ToolTip = tooltip, UserData = entity @@ -803,7 +803,7 @@ namespace Barotrauma break; } } - e.prefab?.DrawPlacing(spriteBatch, + e.Prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); GUI.DrawRectangle(spriteBatch, new Vector2(e.WorldRect.X, -e.WorldRect.Y) + moveAmount, @@ -830,7 +830,7 @@ namespace Barotrauma new Vector2(posX, posY + sizeY) }; - Color selectionColor = GUI.Style.Blue; + Color selectionColor = GUIStyle.Blue; float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom); GUI.DrawFilledRectangle(spriteBatch, corners[0], selectionSize, selectionColor * 0.1f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index e7adb8e8d..daf65126a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -5,15 +5,13 @@ using System.Collections.Generic; namespace Barotrauma { - abstract partial class MapEntityPrefab : IPrefab, IDisposable + abstract partial class MapEntityPrefab : PrefabWithUintIdentifier { - public readonly Dictionary> UpgradeOverrideSprites = new Dictionary>(); - public virtual void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } @@ -47,7 +45,7 @@ namespace Barotrauma placePosition = Vector2.Zero; if (!PlayerInput.IsShiftDown()) { - selected = null; + Selected = null; } } @@ -97,7 +95,7 @@ namespace Barotrauma } public void DrawListLine(SpriteBatch spriteBatch, Vector2 pos, Color color) { - GUI.Font.DrawString(spriteBatch, originalName, pos, color); + GUIStyle.Font.DrawString(spriteBatch, OriginalName, pos, color); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs new file mode 100644 index 000000000..5f4645cac --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/RoundSound.cs @@ -0,0 +1,137 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Xml.Linq; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class RoundSound + { + public Sound? Sound; + public readonly float Volume; + public readonly float Range; + public readonly Vector2 FrequencyMultiplierRange; + public readonly bool Stream; + public readonly bool IgnoreMuffling; + + public readonly string? Filename; + + private RoundSound(ContentXElement element, Sound sound) + { + Filename = sound.Filename; + Sound = sound; + Stream = sound.Stream; + Range = element.GetAttributeFloat("range", 1000.0f); + Volume = element.GetAttributeFloat("volume", 1.0f); + FrequencyMultiplierRange = new Vector2(1.0f); + string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0"))!; + if (!freqMultAttr.Contains(',')) + { + if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) + { + FrequencyMultiplierRange = new Vector2(freqMult); + } + } + else + { + var freqMult = XMLExtensions.ParseVector2(freqMultAttr, false); + if (freqMult.Y >= 0.25f) + { + FrequencyMultiplierRange = freqMult; + } + } + if (FrequencyMultiplierRange.Y > 4.0f) + { + DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); + } + IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); + } + + public float GetRandomFrequencyMultiplier() + { + return Rand.Range(FrequencyMultiplierRange.X, FrequencyMultiplierRange.Y); + } + + private static readonly List roundSounds = new List(); + public static RoundSound? Load(ContentXElement element, bool stream = false) + { + if (GameMain.SoundManager?.Disabled ?? true) { return null; } + + var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound"); + + if (filename is null) + { + string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return null; + } + + Sound? existingSound = roundSounds.Find(s => s.Filename == filename?.FullPath && s.Stream == stream && s.Sound is { Disposed: false })?.Sound; + + if (existingSound is null) + { + try + { + existingSound = GameMain.SoundManager.LoadSound(filename?.FullPath, stream); + if (existingSound == null) { return null; } + } + catch (System.IO.FileNotFoundException e) + { + string errorMsg = "Failed to load sound file \"" + filename + "\"."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return null; + } + } + + RoundSound newSound = new RoundSound(element, existingSound); + + roundSounds.Add(newSound); + return newSound; + } + + public static void Reload(RoundSound roundSound) + { + Sound? existingSound = roundSounds.Find(s => s.Filename == roundSound.Filename && s.Stream == roundSound.Stream && s.Sound is { Disposed: false })?.Sound; + if (existingSound == null) + { + try + { + existingSound = GameMain.SoundManager.LoadSound(roundSound.Filename, roundSound.Stream); + } + catch (System.IO.FileNotFoundException e) + { + string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; + DebugConsole.ThrowError(errorMsg, e); + GameAnalyticsManager.AddErrorEventOnce("RoundSound.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); + return; + } + } + roundSound.Sound = existingSound; + } + + private static void Remove(RoundSound roundSound) + { + #warning TODO: what is going on here???? + roundSound.Sound?.Dispose(); + + if (roundSounds.Contains(roundSound)) { roundSounds.Remove(roundSound); } + foreach (RoundSound otherSound in roundSounds) + { + if (otherSound.Sound == roundSound.Sound) { otherSound.Sound = null; } + } + } + + public static void RemoveAllRoundSounds() + { + for (int i = roundSounds.Count - 1; i >= 0; i--) + { + Remove(roundSounds[i]); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 55bba4cda..339d904b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -26,7 +26,7 @@ namespace Barotrauma { get { - if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(Prefab.Subcategory)) { return false; } @@ -38,9 +38,9 @@ namespace Barotrauma } #if DEBUG - [Editable, Serialize("", true)] + [Editable, Serialize("", IsPropertySaveable.Yes)] #else - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] #endif public string SpecialTag { @@ -50,7 +50,7 @@ namespace Barotrauma partial void InitProjSpecific() { - Prefab.sprite?.EnsureLazyLoaded(); + Prefab.Sprite?.EnsureLazyLoaded(); Prefab.BackgroundSprite?.EnsureLazyLoaded(); foreach (var decorativeSprite in Prefab.DecorativeSprites) @@ -120,13 +120,13 @@ namespace Barotrauma { CanTakeKeyBoardFocus = false }; - var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUI.LargeFont) { UserData = this }; + var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule) { GUITickBox tickBox = new GUITickBox(new RectTransform(new Point(listBox.Content.Rect.Width, 10)), TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.name")) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Selected = RemoveIfLinkedOutpostDoorInUse, ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), OnSelected = (tickBox) => @@ -246,7 +246,7 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (prefab.sprite == null) { return; } + if (Prefab.Sprite == null) { return; } if (editing) { @@ -265,17 +265,17 @@ namespace Barotrauma private float GetRealDepth() { - return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; + return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : Prefab.Sprite.Depth; } public float GetDrawDepth() { - return GetDrawDepth(GetRealDepth(), prefab.sprite); + return GetDrawDepth(GetRealDepth(), Prefab.Sprite); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) { - if (prefab.sprite == null) { return; } + if (Prefab.Sprite == null) { return; } if (editing) { if (!SubEditorScreen.IsLayerVisible(this)) { return; } @@ -284,7 +284,7 @@ namespace Barotrauma } else if (HiddenInGame) { return; } - Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted ? GUI.Style.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; + Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted ? GUIStyle.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; if (IsSelected && editing) { @@ -371,8 +371,8 @@ namespace Barotrauma if (back == GetRealDepth() > 0.5f) { - SpriteEffects oldEffects = prefab.sprite.effects; - prefab.sprite.effects ^= SpriteEffects; + SpriteEffects oldEffects = Prefab.Sprite.effects; + Prefab.Sprite.effects ^= SpriteEffects; for (int i = 0; i < Sections.Length; i++) { @@ -410,10 +410,10 @@ namespace Barotrauma if (FlippedX && IsHorizontal) { sectionOffset.X = drawSection.Right - rect.Right; } if (FlippedY && !IsHorizontal) { sectionOffset.Y = (rect.Y - rect.Height) - (drawSection.Y - drawSection.Height); } - sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width); - sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height); + sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, Prefab.Sprite.SourceRect.Width); + sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, Prefab.Sprite.SourceRect.Height); - prefab.sprite.DrawTiled( + Prefab.Sprite.DrawTiled( spriteBatch, new Vector2(drawSection.X + drawOffset.X, -(drawSection.Y + drawOffset.Y)), new Vector2(drawSection.Width, drawSection.Height), @@ -429,10 +429,10 @@ namespace Barotrauma float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, prefab.sprite.effects, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, Prefab.Sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - Prefab.Sprite.Depth), 0.999f)); } - prefab.sprite.effects = oldEffects; + Prefab.Sprite.effects = oldEffects; } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) @@ -473,13 +473,13 @@ namespace Barotrauma DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) { - for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) + for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Length; i++) { var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } if (spriteGroup > 0) { - int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Count; + int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Length; if (i != activeSpriteIndex) { spriteAnimState[decorativeSprite].IsActive = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 20e6c3b99..ee708d2ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -2,25 +2,22 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class StructurePrefab : MapEntityPrefab { - public Color BackgroundSpriteColor - { - get; - private set; - } + public readonly Color BackgroundSpriteColor; - public List DecorativeSprites = new List(); - public Dictionary> DecorativeSpriteGroups = new Dictionary>(); + public readonly ImmutableArray DecorativeSprites; + public readonly ImmutableDictionary> DecorativeSpriteGroups; public override void UpdatePlacing(Camera cam) { if (PlayerInput.SecondaryMouseButtonClicked()) { - selected = null; + Selected = null; return; } @@ -65,7 +62,7 @@ namespace Barotrauma placePosition = Vector2.Zero; if (!PlayerInput.IsShiftDown()) { - selected = null; + Selected = null; } return; } @@ -91,24 +88,24 @@ namespace Barotrauma newRect = Submarine.AbsRect(placePosition, placeSize); } - sprite.DrawTiled(spriteBatch, new Vector2(newRect.X, -newRect.Y), new Vector2(newRect.Width, newRect.Height), textureScale: TextureScale * Scale); + Sprite.DrawTiled(spriteBatch, new Vector2(newRect.X, -newRect.Y), new Vector2(newRect.Width, newRect.Height), textureScale: TextureScale * Scale); GUI.DrawRectangle(spriteBatch, new Rectangle(newRect.X - GameMain.GraphicsWidth, -newRect.Y, newRect.Width + GameMain.GraphicsWidth * 2, newRect.Height), Color.White); GUI.DrawRectangle(spriteBatch, new Rectangle(newRect.X, -newRect.Y - GameMain.GraphicsHeight, newRect.Width, newRect.Height + GameMain.GraphicsHeight * 2), Color.White); } public override void DrawPlacing(SpriteBatch spriteBatch, Rectangle placeRect, float scale = 1.0f, SpriteEffects spriteEffects = SpriteEffects.None) { - SpriteEffects oldEffects = sprite.effects; - sprite.effects ^= spriteEffects; + SpriteEffects oldEffects = Sprite.effects; + Sprite.effects ^= spriteEffects; - sprite.DrawTiled( + Sprite.DrawTiled( spriteBatch, new Vector2(placeRect.X, -placeRect.Y), new Vector2(placeRect.Width, placeRect.Height), color: Color.White * 0.8f, textureScale: TextureScale * scale); - sprite.effects = oldEffects; + Sprite.effects = oldEffects; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 9f35f13be..90fb358ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -12,58 +12,9 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; -using System.Globalization; namespace Barotrauma { - class RoundSound - { - public Sound Sound; - public readonly float Volume; - public readonly float Range; - public readonly Vector2 FrequencyMultiplierRange; - public readonly bool Stream; - public readonly bool IgnoreMuffling; - - public readonly string Filename; - - public RoundSound(XElement element, Sound sound) - { - Filename = sound?.Filename; - Sound = sound; - Stream = sound.Stream; - Range = element.GetAttributeFloat("range", 1000.0f); - Volume = element.GetAttributeFloat("volume", 1.0f); - FrequencyMultiplierRange = new Vector2(1.0f); - string freqMultAttr = element.GetAttributeString("frequencymultiplier", element.GetAttributeString("frequency", "1.0")); - if (!freqMultAttr.Contains(',')) - { - if (float.TryParse(freqMultAttr, NumberStyles.Any, CultureInfo.InvariantCulture, out float freqMult)) - { - FrequencyMultiplierRange = new Vector2(freqMult); - } - } - else - { - var freqMult = XMLExtensions.ParseVector2(freqMultAttr, false); - if (freqMult.Y >= 0.25f) - { - FrequencyMultiplierRange = freqMult; - } - } - if (FrequencyMultiplierRange.Y > 4.0f) - { - DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); - } - IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); - } - - public float GetRandomFrequencyMultiplier() - { - return Rand.Range(FrequencyMultiplierRange.X, FrequencyMultiplierRange.Y); - } - } - partial class Submarine : Entity, IServerSerializable { public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) @@ -82,97 +33,6 @@ namespace Barotrauma return worldGridPos; } - - private static List roundSounds = null; - public static RoundSound LoadRoundSound(XElement element, bool stream = false) - { - if (GameMain.SoundManager?.Disabled ?? true) { return null; } - - string filename = element.GetAttributeString("file", ""); - if (string.IsNullOrEmpty(filename)) filename = element.GetAttributeString("sound", ""); - - if (string.IsNullOrEmpty(filename)) - { - string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FilePathEmpty" + element.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); - return null; - } - - filename = Path.GetFullPath(filename.CleanUpPath()).CleanUpPath(); - Sound existingSound = null; - if (roundSounds == null) - { - roundSounds = new List(); - } - else - { - existingSound = roundSounds.Find(s => s.Filename == filename && s.Stream == stream && !s.Sound.Disposed)?.Sound; - } - - if (existingSound == null) - { - try - { - existingSound = GameMain.SoundManager.LoadSound(filename, stream); - if (existingSound == null) { return null; } - } - catch (System.IO.FileNotFoundException e) - { - string errorMsg = "Failed to load sound file \"" + filename + "\"."; - DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); - return null; - } - } - - RoundSound newSound = new RoundSound(element, existingSound); - - roundSounds.Add(newSound); - return newSound; - } - - public static void ReloadRoundSound(RoundSound roundSound) - { - Sound existingSound = roundSounds?.Find(s => s.Filename == roundSound.Filename && s.Stream == roundSound.Stream && !s.Sound.Disposed)?.Sound; - if (existingSound == null) - { - try - { - existingSound = GameMain.SoundManager.LoadSound(roundSound.Filename, roundSound.Stream); - } - catch (System.IO.FileNotFoundException e) - { - string errorMsg = "Failed to load sound file \"" + roundSound.Filename + "\"."; - DebugConsole.ThrowError(errorMsg, e); - GameAnalyticsManager.AddErrorEventOnce("Submarine.LoadRoundSound:FileNotFound" + roundSound.Filename, GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } - } - roundSound.Sound = existingSound; - } - - private static void RemoveRoundSound(RoundSound roundSound) - { - roundSound.Sound?.Dispose(); - if (roundSounds == null) return; - - if (roundSounds.Contains(roundSound)) roundSounds.Remove(roundSound); - foreach (RoundSound otherSound in roundSounds) - { - if (otherSound.Sound == roundSound.Sound) otherSound.Sound = null; - } - } - - public static void RemoveAllRoundSounds() - { - if (roundSounds == null) return; - for (int i = roundSounds.Count - 1; i >= 0; i--) - { - RemoveRoundSound(roundSounds[i]); - } - } - //drawing ---------------------------------------------------- private static readonly HashSet visibleSubs = new HashSet(); public static void CullEntities(Camera cam) @@ -402,7 +262,7 @@ namespace Barotrauma var connectedSubs = GetConnectedSubs(); - HashSet hullList = Hull.hullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet(); + HashSet hullList = Hull.HullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet(); Dictionary> combinedHulls = new Dictionary>(); @@ -592,23 +452,23 @@ namespace Barotrauma List errorMsgs = new List(); List warnings = new List(); - if (!Hull.hullList.Any()) + if (!Hull.HullList.Any()) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints)) { - errorMsgs.Add(TextManager.Get("NoHullsWarning")); + errorMsgs.Add(TextManager.Get("NoHullsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoHulls); } } - if (Info.Type != SubmarineType.OutpostModule || - (Info.OutpostModuleInfo?.ModuleFlags.Any(f => !f.Equals("hallwayvertical", StringComparison.OrdinalIgnoreCase) && !f.Equals("hallwayhorizontal", StringComparison.OrdinalIgnoreCase)) ?? true)) + if (Info.Type != SubmarineType.OutpostModule || + (Info.OutpostModuleInfo?.ModuleFlags.Any(f => f != "hallwayvertical" && f != "hallwayhorizontal") ?? true)) { if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints)) { - errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); + errorMsgs.Add(TextManager.Get("NoWaypointsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoWaypoints); } } @@ -623,7 +483,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.DisconnectedVents)) { - errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning")); + errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning").Value); warnings.Add(SubEditorScreen.WarningType.DisconnectedVents); } break; @@ -634,7 +494,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHumanSpawnpoints)) { - errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoHumanSpawnpoints); } } @@ -642,7 +502,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoCargoSpawnpoints)) { - errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning")); + errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoCargoSpawnpoints); } } @@ -650,7 +510,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoBallastTag)) { - errorMsgs.Add(TextManager.Get("NoBallastTagsWarning")); + errorMsgs.Add(TextManager.Get("NoBallastTagsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NoBallastTag); } } @@ -670,8 +530,8 @@ namespace Barotrauma if (doorLinks + wireCount > item.Connections[i].MaxWires) { errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", - new string[] { "[doorcount]", "[freeconnectioncount]" }, - new string[] { doorLinks.ToString(), (item.Connections[i].MaxWires - wireCount).ToString() })); + ("[doorcount]", doorLinks.ToString()), + ("[freeconnectioncount]", (item.Connections[i].MaxWires - wireCount).ToString())).Value); break; } } @@ -682,7 +542,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.NonLinkedGaps)) { - errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning")); + errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning").Value); warnings.Add(SubEditorScreen.WarningType.NonLinkedGaps); } } @@ -698,7 +558,7 @@ namespace Barotrauma { if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooManyLights)) { - errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning")); + errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning").Value); warnings.Add(SubEditorScreen.WarningType.TooManyLights); } } @@ -746,7 +606,7 @@ namespace Barotrauma var msgBox = new GUIMessageBox( TextManager.Get("Warning"), TextManager.Get("FarAwayEntitiesWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked += (btn, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index 2977533a7..c8e5cedeb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -83,31 +83,31 @@ namespace Barotrauma Spacing = 5 }; - ScalableFont font = parent.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; + GUIFont font = parent.Rect.Width < 350 ? GUIStyle.SmallFont : GUIStyle.Font; CreateSpecsWindow(descriptionBox, font, includeDescription: true); } - public void CreateSpecsWindow(GUIListBox parent, ScalableFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) + public void CreateSpecsWindow(GUIListBox parent, GUIFont font, bool includeTitle = true, bool includeClass = true, bool includeDescription = false) { float leftPanelWidth = 0.6f; float rightPanelWidth = 0.4f / leftPanelWidth; - string className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); + LocalizedString className = !HasTag(SubmarineTag.Shuttle) ? TextManager.Get($"submarineclass.{SubmarineClass}") : TextManager.Get("shuttle"); - int classHeight = (int)GUI.SubHeadingFont.MeasureString(className).Y; + int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); GUITextBlock submarineNameText = null; GUITextBlock submarineClassText = null; if (includeTitle) { - int nameHeight = (int)GUI.LargeFont.MeasureString(DisplayName, true).Y; - submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) { CanBeFocused = false }; + int nameHeight = (int)GUIStyle.LargeFont.MeasureString(DisplayName, true).Y; + submarineNameText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, nameHeight + HUDLayoutSettings.Padding / 2), parent.Content.RectTransform), DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false }; submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y); } if (includeClass) { - submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { CanBeFocused = false }; + submarineClassText = new GUITextBlock(new RectTransform(new Point(leftPanelWidthInt, classHeight), parent.Content.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y); } @@ -124,7 +124,7 @@ namespace Barotrauma Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; if (realWorldDimensions != Vector2.Zero) { - string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); + LocalizedString dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", ("[width]", ((int)realWorldDimensions.X).ToString()), ("[height]", ((int)realWorldDimensions.Y).ToString())); var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; @@ -134,7 +134,7 @@ namespace Barotrauma dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); } - string cargoCapacityStr = CargoCapacity < 0 ? TextManager.Get("unknown") : TextManager.GetWithVariables("cargocapacityformat", new string[1] { "[cratecount]" }, new string[1] {CargoCapacity.ToString() }); + var cargoCapacityStr = CargoCapacity < 0 ? TextManager.Get("unknown") : TextManager.GetWithVariable("cargocapacityformat", "[cratecount]", CargoCapacity.ToString()); var cargoCapacityText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), parent.Content.RectTransform), TextManager.Get("cargocapacity"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; @@ -200,11 +200,11 @@ namespace Barotrauma //space new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), parent.Content.RectTransform), style: null); - if (!string.IsNullOrEmpty(Description)) + if (!Description.IsNullOrEmpty()) { var wsItemDesc = new GUITextBlock(new RectTransform(new Vector2(1, 0), parent.Content.RectTransform), - TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) - { CanBeFocused = false, ForceUpperCase = true }; + TextManager.Get("SaveSubDialogDescription", "WorkshopItemDescription"), font: GUIStyle.Font, wrap: true) + { CanBeFocused = false, ForceUpperCase = ForceUpperCase.Yes }; descBlock = new GUITextBlock(new RectTransform(new Vector2(1, 0), parent.Content.RectTransform), Description, font: font, wrap: true) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 900a6d355..5aa7c9197 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -26,12 +26,12 @@ namespace Barotrauma private class HullCollection { public readonly List Rects; - public readonly string Name; + public readonly LocalizedString Name; - public HullCollection(string identifier) + public HullCollection(Identifier identifier) { Rects = new List(); - Name = TextManager.Get(identifier, returnNull: true) ?? identifier; + Name = TextManager.Get(identifier).Fallback(identifier.Value); } public void AddRect(XElement element) @@ -53,10 +53,9 @@ namespace Barotrauma } } - private readonly Dictionary hullCollections; + private readonly Dictionary hullCollections; private readonly List doors; - private static SubmarinePreview instance = null; public static void Create(SubmarineInfo submarineInfo) @@ -78,7 +77,7 @@ namespace Barotrauma isDisposed = false; loadTask = null; - hullCollections = new Dictionary(); + hullCollections = new Dictionary(); doors = new List(); previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); @@ -133,7 +132,7 @@ namespace Barotrauma }; var topLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 5f / 7f), topContainer.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); - titleText = new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUI.LargeFont); + titleText = new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUIStyle.LargeFont); new GUIButton(new RectTransform(new Vector2(0.05f, 1f), topLayout.RectTransform), TextManager.Get("Close")) { OnClicked = (btn, obj) => { Dispose(); return false; } @@ -146,7 +145,7 @@ namespace Barotrauma ScrollBarVisible = false, Spacing = 5 }; - subInfo.CreateSpecsWindow(specsContainer, GUI.Font, includeTitle: false, includeDescription: true); + subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); int width = specsContainer.Rect.Width; void recalculateSpecsContainerHeight() { @@ -242,8 +241,8 @@ namespace Barotrauma BakeMapEntity(subElement); break; case "hull": - string identifier = subElement.GetAttributeString("roomname", "").ToLowerInvariant(); - if (!string.IsNullOrEmpty(identifier)) + Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); + if (!identifier.IsEmpty) { if (!hullCollections.TryGetValue(identifier, out HullCollection hullCollection)) { @@ -309,11 +308,11 @@ namespace Barotrauma float rotation = element.GetAttributeFloat("rotation", 0f); - MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + MapEntityPrefab prefab = MapEntityPrefab.List.FirstOrDefault(p => p.Identifier == identifier); if (prefab == null) { return; } - var texture = prefab.sprite.Texture; - var srcRect = prefab.sprite.SourceRect; + var texture = prefab.Sprite.Texture; + var srcRect = prefab.Sprite.SourceRect; SpriteEffects spriteEffects = SpriteEffects.None; if (flippedX && ((prefab as ItemPrefab)?.CanSpriteFlipX ?? true)) @@ -325,8 +324,8 @@ namespace Barotrauma spriteEffects |= SpriteEffects.FlipVertically; } - var prevEffects = prefab.sprite.effects; - prefab.sprite.effects ^= spriteEffects; + var prevEffects = prefab.Sprite.effects; + prefab.Sprite.effects ^= spriteEffects; bool overrideSprite = false; ItemPrefab itemPrefab = prefab as ItemPrefab; @@ -359,10 +358,10 @@ namespace Barotrauma if (flippedY) { textureOffset.Y = -textureOffset.Y; } backGroundOffset = new Vector2( - MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width), - MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height)); + MathUtils.PositiveModulo((int)-textureOffset.X, prefab.Sprite.SourceRect.Width), + MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.Sprite.SourceRect.Height)); - prefab.sprite.DrawTiled( + prefab.Sprite.DrawTiled( spriteRecorder, rect.Location.ToVector2() * new Vector2(1f, -1f), rect.Size.ToVector2(), @@ -385,17 +384,17 @@ namespace Barotrauma { if (!prefab.ResizeHorizontal) { - rect.Width = (int)(prefab.sprite.size.X * scale); + rect.Width = (int)(prefab.Sprite.size.X * scale); } if (!prefab.ResizeVertical) { - rect.Height = (int)(prefab.sprite.size.Y * scale); + rect.Height = (int)(prefab.Sprite.size.Y * scale); } var spritePos = rect.Center.ToVector2(); //spritePos.Y = rect.Height - spritePos.Y; - prefab.sprite.DrawTiled( + prefab.Sprite.DrawTiled( spriteRecorder, rect.Location.ToVector2() * new Vector2(1f, -1f), rect.Size.ToVector2(), @@ -413,7 +412,7 @@ namespace Barotrauma new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), rect.Size.ToVector2(), color: color, textureScale: Vector2.One * scale, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } else @@ -425,14 +424,14 @@ namespace Barotrauma spritePos.Y -= rect.Height; //spritePos.Y = rect.Height - spritePos.Y; - prefab.sprite.Draw( + prefab.Sprite.Draw( spriteRecorder, spritePos * new Vector2(1f, -1f), color, - prefab.sprite.Origin, + prefab.Sprite.Origin, rotation, scale, - prefab.sprite.effects, depth); + prefab.Sprite.effects, depth); foreach (var decorativeSprite in itemPrefab.DecorativeSprites) { @@ -442,14 +441,14 @@ namespace Barotrauma if (flippedX && itemPrefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && itemPrefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, - MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.sprite.effects, - depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + MathHelper.ToRadians(rotation) + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } } } - prefab.sprite.effects = prevEffects; + prefab.Sprite.effects = prevEffects; } private void BakeItemComponents( @@ -467,7 +466,7 @@ namespace Barotrauma case "turret": Sprite barrelSprite = null; Sprite railSprite = null; - foreach (XElement turretSubElem in subElement.Elements()) + foreach (var turretSubElem in subElement.Elements()) { switch (turretSubElem.Name.ToString().ToLowerInvariant()) { @@ -494,13 +493,13 @@ namespace Barotrauma drawPos, color, rotation + MathHelper.PiOver2, scale, - SpriteEffects.None, depth + (railSprite.Depth - prefab.sprite.Depth)); + SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth)); barrelSprite?.Draw(spriteRecorder, drawPos, color, rotation + MathHelper.PiOver2, scale, - SpriteEffects.None, depth + (barrelSprite.Depth - prefab.sprite.Depth)); + SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth)); break; case "door": @@ -578,13 +577,13 @@ namespace Barotrauma if (!spriteRecorder.ReadyToRender) { - string waitText = !loadTask.IsCompleted ? - TextManager.Get("generatingsubmarinepreview", fallBackTag: "loading") : + LocalizedString waitText = !loadTask.IsCompleted ? + TextManager.Get("generatingsubmarinepreview", "loading") : (loadTask.Exception?.ToString() ?? "Task completed without marking as ready to render"); - Vector2 origin = (GUI.Font.MeasureString(waitText) * 0.5f); + Vector2 origin = (GUIStyle.Font.MeasureString(waitText) * 0.5f); origin.X = MathF.Round(origin.X); origin.Y = MathF.Round(origin.Y); - GUI.Font.DrawString( + GUIStyle.Font.DrawString( spriteBatch, waitText, scissorRectangle.Center.ToVector2(), @@ -629,18 +628,18 @@ namespace Barotrauma if (mouseOver) { - string str = hullCollection.Name; - Vector2 strSize = GUI.Font.MeasureString(str) / camera.Zoom; + LocalizedString str = hullCollection.Name; + Vector2 strSize = GUIStyle.Font.MeasureString(str) / camera.Zoom; Vector2 padding = new Vector2(30, 30) / camera.Zoom; Vector2 shift = new Vector2(10, 0) / camera.Zoom; GUI.DrawRectangle(spriteBatch, mousePos + shift, strSize + padding, Color.Black, isFilled: true, depth: 0.25f); - GUI.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); + GUIStyle.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); } } foreach (var door in doors) { - GUI.DrawRectangle(spriteBatch, door.Rect, GUI.Style.Green * 0.5f, isFilled: true, depth: 0.4f); + GUI.DrawRectangle(spriteBatch, door.Rect, GUIStyle.Green * 0.5f, isFilled: true, depth: 0.4f); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index b2cbce8ff..8cd2acaaf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -37,7 +37,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Vector2 drawPos) { - Color clr = CurrentHull == null ? Color.DodgerBlue : GUI.Style.Green; + Color clr = CurrentHull == null ? Color.DodgerBlue : GUIStyle.Green; if (spawnType != SpawnType.Path) { clr = Color.Gray; } if (isObstructed) { @@ -54,7 +54,7 @@ namespace Barotrauma if (IsSelected || IsHighlighted) { int glowSize = (int)(iconSize * 1.5f); - GUI.Style.UIGlowCircular.Draw(spriteBatch, + GUIStyle.UIGlowCircular.Draw(spriteBatch, new Rectangle((int)(drawPos.X - glowSize / 2), (int)(drawPos.Y - glowSize / 2), glowSize, glowSize), Color.White); } @@ -84,21 +84,21 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - (isObstructed ? Color.Gray : GUI.Style.Green) * 0.7f, width: 5, depth: 0.002f); + (isObstructed ? Color.Gray : GUIStyle.Green) * 0.7f, width: 5, depth: 0.002f); } if (ConnectedGap != null) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(ConnectedGap.DrawPosition.X, -ConnectedGap.DrawPosition.Y), - GUI.Style.Green * 0.5f, width: 1); + GUIStyle.Green * 0.5f, width: 1); } if (Ladders != null) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(Ladders.Item.DrawPosition.X, -Ladders.Item.DrawPosition.Y), - GUI.Style.Green * 0.5f, width: 1); + GUIStyle.Green * 0.5f, width: 1); } var color = Color.WhiteSmoke; @@ -123,13 +123,13 @@ namespace Barotrauma } } } - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, ID.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 30), color); if (Tunnel?.Type != null) { - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, Tunnel.Type.ToString(), new Vector2(DrawPosition.X - 10, -DrawPosition.Y - 45), color); @@ -289,13 +289,13 @@ namespace Barotrauma if (spawnType == SpawnType.Path) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Waypoint"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Waypoint"), font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("LinkWaypoint")); } else { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUI.LargeFont); - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUIStyle.LargeFont); + var spawnTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true) { Stretch = true, @@ -318,8 +318,8 @@ namespace Barotrauma OnClicked = ChangeSpawnType }; - var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardDescription"), font: GUI.SmallFont) + var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), + TextManager.Get("IDCardDescription"), font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("IDCardDescriptionTooltip") }; @@ -336,17 +336,17 @@ namespace Barotrauma propertyBox.OnEnterPressed += (textBox, text) => { IdCardDesc = text; - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; }; propertyBox.OnDeselected += (textBox, keys) => { IdCardDesc = textBox.Text; - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); }; var idCardTagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardTags"), font: GUI.SmallFont) + TextManager.Get("IDCardTags"), font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("IDCardTagsTooltip") }; @@ -363,17 +363,17 @@ namespace Barotrauma propertyBox.OnEnterPressed += (textBox, text) => { textBox.Text = string.Join(",", IdCardTags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; }; propertyBox.OnDeselected += (textBox, keys) => { textBox.Text = string.Join(",", IdCardTags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); }; var jobsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("SpawnpointJobs"), font: GUI.SmallFont) + TextManager.Get("SpawnpointJobs"), font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("SpawnpointJobsTooltip") }; @@ -394,7 +394,7 @@ namespace Barotrauma jobDropDown.SelectItem(AssignedJob); var tagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("spawnpointtags"), font: GUI.SmallFont); + TextManager.Get("spawnpointtags"), font: GUIStyle.SmallFont); propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), tagsText.RectTransform, Anchor.CenterRight), string.Join(", ", tags)) { MaxTextLength = 60, @@ -402,19 +402,19 @@ namespace Barotrauma }; propertyBox.OnTextChanged += (textBox, text) => { - tags = text.Split(',').ToList(); + tags = text.Split(',').ToIdentifiers().ToHashSet(); return true; }; propertyBox.OnEnterPressed += (textBox, text) => { textBox.Text = string.Join(",", tags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; }; propertyBox.OnDeselected += (textBox, keys) => { textBox.Text = string.Join(",", tags); - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index 898d39d8c..1e112d1df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -97,12 +97,12 @@ namespace Barotrauma.Networking new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), bannedPlayer.ExpirationTime == null ? TextManager.Get("BanPermanent") : TextManager.GetWithVariable("BanExpires", "[time]", bannedPlayer.ExpirationTime.Value.ToString()), - font: GUI.SmallFont); + font: GUIStyle.SmallFont); var reasonText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedPlayerFrame.RectTransform), TextManager.Get("BanReason") + " " + (string.IsNullOrEmpty(bannedPlayer.Reason) ? TextManager.Get("None") : bannedPlayer.Reason), - font: GUI.SmallFont, wrap: true) + font: GUIStyle.SmallFont, wrap: true) { ToolTip = bannedPlayer.Reason }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 0e853822f..3a61415c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -61,25 +61,27 @@ namespace Barotrauma.Networking break; case ChatMessageType.Order: var orderMessageInfo = OrderChatMessage.ReadOrder(msg); - if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + if (orderMessageInfo.OrderIdentifier == Identifier.Empty) { DebugConsole.ThrowError("Invalid order message - order index out of bounds."); if (NetIdUtils.IdMoreRecent(id, LastID)) { LastID = id; } return; } - var orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; - string orderOption = orderMessageInfo.OrderOption; - orderOption ??= orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length ? - orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] : ""; + var orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; + Identifier orderOption = orderMessageInfo.OrderOption; + orderOption = orderOption.IfEmpty( + orderMessageInfo.OrderOptionIndex.HasValue && orderMessageInfo.OrderOptionIndex >= 0 && orderMessageInfo.OrderOptionIndex < orderPrefab.Options.Length + ? orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value] + : Identifier.Empty); string targetRoom; if (orderMessageInfo.TargetEntity is Hull targetHull) { - targetRoom = targetHull.DisplayName; + targetRoom = targetHull.DisplayName.Value; } else { - targetRoom = senderCharacter?.CurrentHull?.DisplayName; + targetRoom = senderCharacter?.CurrentHull?.DisplayName?.Value; } txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom, @@ -93,18 +95,19 @@ namespace Barotrauma.Networking switch (orderMessageInfo.TargetType) { case Order.OrderTargetType.Entity: - order = new Order(orderPrefab, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter); break; case Order.OrderTargetType.Position: - order = new Order(orderPrefab, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); break; case Order.OrderTargetType.WallSection: - order = new Order(orderPrefab, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter); break; } if (order != null) { + order = order.WithManualPriority(orderMessageInfo.Priority); if (order.TargetAllCharacters) { var fadeOutTime = !orderPrefab.IsIgnoreOrder ? (float?)orderPrefab.FadeOutTime : null; @@ -112,24 +115,39 @@ namespace Barotrauma.Networking } else { - orderMessageInfo.TargetCharacter?.SetOrder(order, orderOption, orderMessageInfo.Priority, senderCharacter); + orderMessageInfo.TargetCharacter?.SetOrder(order); } } } if (NetIdUtils.IdMoreRecent(id, LastID)) { + Order order = null; + if (orderMessageInfo.TargetPosition != null) + { + order = new Order(orderPrefab, orderOption, orderMessageInfo.Priority, Order.OrderType.Current, null, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); + } + else if (orderMessageInfo.WallSectionIndex != null) + { + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: senderCharacter) + .WithManualPriority(orderMessageInfo.Priority); + } + else + { + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: senderCharacter) + .WithManualPriority(orderMessageInfo.Priority); + } GameMain.Client.AddChatMessage( - new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, txt, orderMessageInfo.TargetPosition ?? orderMessageInfo.TargetEntity as ISpatialEntity, orderMessageInfo.TargetCharacter, senderCharacter)); + new OrderChatMessage(order, txt, orderMessageInfo.TargetCharacter, senderCharacter)); LastID = id; } return; case ChatMessageType.ServerMessageBox: - txt = TextManager.GetServerMessage(txt); + txt = TextManager.GetServerMessage(txt).Value; break; case ChatMessageType.ServerMessageBoxInGame: styleSetting = msg.ReadString(); - txt = TextManager.GetServerMessage(txt); + txt = TextManager.GetServerMessage(txt).Value; break; } @@ -148,7 +166,7 @@ namespace Barotrauma.Networking break; case ChatMessageType.ServerMessageBoxInGame: { - GUIMessageBox messageBox = new GUIMessageBox("", txt, new string[0], type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); + GUIMessageBox messageBox = new GUIMessageBox("", txt, Array.Empty(), type: GUIMessageBox.Type.InGame, iconStyle: styleSetting); if (textColor != null) { messageBox.Text.TextColor = textColor.Value; } } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index bd3111221..41ab884b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Networking struct TempClient { public string Name; - public string PreferredJob; + public Identifier PreferredJob; public CharacterTeamType PreferredTeam; public UInt16 NameID; public UInt64 SteamID; @@ -66,7 +66,7 @@ namespace Barotrauma.Networking if (character != null) { - if (GameMain.Config.UseDirectionalVoiceChat) + if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) { VoipSound.SetPosition(new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 86897d268..5fb9aac87 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -19,7 +19,7 @@ namespace Barotrauma DebugConsole.Log($"Received entity removal message for \"{entity}\"."); if (entity is Item item && item.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); } entity.Remove(); } @@ -36,7 +36,7 @@ namespace Barotrauma var newItem = Item.ReadSpawnData(message, true); if (newItem is Item item && item.Container?.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none".ToIdentifier()) + ":" + item.Prefab.Identifier); } break; case (byte)SpawnableType.Character: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 7fd0f8bb1..4c2721291 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Threading; @@ -35,6 +36,8 @@ namespace Barotrauma.Networking get; private set; } + + public int LastSeen { get; set; } public FileTransferType FileType { @@ -119,7 +122,7 @@ namespace Barotrauma.Networking int passed = Environment.TickCount - TimeStarted; float psec = passed / 1000.0f; - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log($"Received {all.Length} bytes of the file {FileName} ({Received / 1000}/{FileSize / 1000} kB received)"); } @@ -162,16 +165,15 @@ namespace Barotrauma.Networking private readonly List activeTransfers; private readonly List<(int transferId, double finishedTime)> finishedTransfers; - private readonly Dictionary downloadFolders = new Dictionary() + private readonly ImmutableDictionary downloadFolders = new Dictionary() { { FileTransferType.Submarine, SaveUtil.SubmarineDownloadFolder }, - { FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder } - }; + { FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder }, + { FileTransferType.Mod, ModReceiver.DownloadFolder } + }.ToImmutableDictionary(); - public List ActiveTransfers - { - get { return activeTransfers; } - } + public IReadOnlyList ActiveTransfers => activeTransfers; + public bool HasActiveTransfers => ActiveTransfers.Any(); public FileReceiver() { @@ -211,7 +213,7 @@ namespace Barotrauma.Networking } else //resend acknowledgement packet { - GameMain.Client.UpdateFileTransfer(transferId, existingTransfer.Received); + GameMain.Client.UpdateFileTransfer(transferId, existingTransfer.Received, existingTransfer.LastSeen); } return; } @@ -223,7 +225,7 @@ namespace Barotrauma.Networking return; } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log("Received file transfer initiation message: "); DebugConsole.Log(" File: " + fileName); @@ -278,7 +280,7 @@ namespace Barotrauma.Networking } activeTransfers.Add(newTransfer); - GameMain.Client.UpdateFileTransfer(transferId, 0); //send acknowledgement packet + GameMain.Client.UpdateFileTransfer(transferId, 0, 0); //send acknowledgement packet } break; case (byte)FileTransferMessageType.TransferOnSameMachine: @@ -287,7 +289,7 @@ namespace Barotrauma.Networking byte fileType = inc.ReadByte(); string filePath = inc.ReadString(); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log("Received file transfer message on the same machine: "); DebugConsole.Log(" File: " + filePath); @@ -308,7 +310,7 @@ namespace Barotrauma.Networking FileSize = 0 }; - Md5Hash.RemoveFromCache(directTransfer.FilePath); + Md5Hash.Cache.Remove(directTransfer.FilePath); OnFinished(directTransfer); } break; @@ -335,10 +337,12 @@ namespace Barotrauma.Networking int bytesToRead = inc.ReadUInt16(); if (offset != activeTransfer.Received) { + activeTransfer.LastSeen = Math.Max(offset, activeTransfer.LastSeen); DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})"); - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received); + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, activeTransfer.LastSeen); return; } + activeTransfer.LastSeen = offset; if (activeTransfer.Received + bytesToRead > activeTransfer.FileSize) { @@ -366,7 +370,7 @@ namespace Barotrauma.Networking return; } - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, reliable: activeTransfer.Status == FileTransferStatus.Finished); + GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, activeTransfer.LastSeen, reliable: activeTransfer.Status == FileTransferStatus.Finished); if (activeTransfer.Status == FileTransferStatus.Finished) { activeTransfer.Dispose(); @@ -375,7 +379,7 @@ namespace Barotrauma.Networking { finishedTransfers.Add((transferId, Timing.TotalTime)); StopTransfer(activeTransfer); - Md5Hash.RemoveFromCache(activeTransfer.FilePath); + Md5Hash.Cache.Remove(activeTransfer.FilePath); OnFinished(activeTransfer); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs new file mode 100644 index 000000000..257c9b950 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/ModReceiver.cs @@ -0,0 +1,8 @@ +namespace Barotrauma.Networking +{ + static class ModReceiver + { + public const string DownloadFolder = "TempMods_Download"; + public const string Extension = ".barodir.gz"; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index ecc352288..23dd76fe6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -50,8 +51,9 @@ namespace Barotrauma.Networking private GUIMessageBox reconnectBox, waitInServerQueueBox; //TODO: move these to NetLobbyScreen + public LocalizedString endRoundVoteText; public GUITickBox EndVoteTickBox; - private GUIComponent buttonContainer; + private readonly GUIComponent buttonContainer; public readonly NetStats NetStats; @@ -121,7 +123,7 @@ namespace Barotrauma.Networking public bool HasSpawned; public bool SpawnAsTraitor; - public string TraitorFirstObjective; + public LocalizedString TraitorFirstObjective; public TraitorMissionPrefab TraitorMission = null; public byte ID @@ -221,13 +223,14 @@ namespace Barotrauma.Networking CanBeFocused = false }; + endRoundVoteText = TextManager.Get("EndRound"); EndVoteTickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.4f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) }, - TextManager.Get("EndRound")) + endRoundVoteText) { - UserData = TextManager.Get("EndRound"), OnSelected = ToggleEndRoundVote, Visible = false }; + EndVoteTickBox.TextBlock.Wrap = true; ShowLogButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.6f), buttonContainer.RectTransform) { MinSize = new Point(150, 0) }, TextManager.Get("ServerLog")) @@ -282,8 +285,7 @@ namespace Barotrauma.Networking //ServerLog = new ServerLog(""); ChatMessage.LastID = 0; - GameMain.NetLobbyScreen?.Release(); - GameMain.NetLobbyScreen = new NetLobbyScreen(); + GameMain.ResetNetLobbyScreen(); } private void ConnectToServer(object endpoint, string hostName) @@ -340,7 +342,7 @@ namespace Barotrauma.Networking catch { new GUIMessageBox(TextManager.Get("CouldNotConnectToServer"), - TextManager.GetWithVariables("InvalidIPAddress", new string[2] { "[serverip]", "[port]" }, new string[2] { serverIP, port.ToString() })); + TextManager.GetWithVariables("InvalidIPAddress", ("[serverip]", serverIP), ("[port]", port.ToString()))); return; } @@ -361,39 +363,7 @@ namespace Barotrauma.Networking } clientPeer.OnDisconnect = OnDisconnect; clientPeer.OnDisconnectMessageReceived = HandleDisconnectMessage; - clientPeer.OnInitializationComplete = () => - { - if (SteamManager.IsInitialized) - { - Steamworks.SteamFriends.ClearRichPresence(); - Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + serverName); - Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); - } - - canStart = true; - connected = true; - - VoipClient = new VoipClient(this, clientPeer); - - if (Screen.Selected != GameMain.GameScreen) - { - GameMain.NetLobbyScreen.Select(); - } - else - { - entityEventManager.ClearSelf(); - foreach (Character c in Character.CharacterList) - { - c.ResetNetState(); - } - } - - chatBox.InputBox.Enabled = true; - if (GameMain.NetLobbyScreen?.ChatInput != null) - { - GameMain.NetLobbyScreen.ChatInput.Enabled = true; - } - }; + clientPeer.OnInitializationComplete = OnConnectionInitializationComplete; clientPeer.OnRequestPassword = (int salt, int retries) => { if (pwRetries != retries) @@ -471,10 +441,10 @@ namespace Barotrauma.Networking canStart = false; DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 40); - DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); + DateTime reqAuthTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, 200); // Loop until we are approved - string connectingText = TextManager.Get("Connecting"); + LocalizedString connectingText = TextManager.Get("Connecting"); while (!canStart && !connectCancelled) { if (reconnectBox == null && waitInServerQueueBox == null) @@ -493,12 +463,12 @@ namespace Barotrauma.Networking } } } - if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown"); } + if (string.IsNullOrEmpty(serverDisplayName)) { serverDisplayName = TextManager.Get("Unknown").Value; } reconnectBox = new GUIMessageBox( connectingText, TextManager.GetWithVariable("ConnectingTo", "[serverip]", serverDisplayName), - new string[] { TextManager.Get("Cancel") }); + new LocalizedString[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; reconnectBox.Buttons[0].OnClicked += reconnectBox.Close; } @@ -524,9 +494,9 @@ namespace Barotrauma.Networking GUI.ClearCursorWait(); reconnectBox?.Close(); reconnectBox = null; - string pwMsg = TextManager.Get("PasswordRequired"); + LocalizedString pwMsg = TextManager.Get("PasswordRequired"); - var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + var msgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170))); var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) @@ -537,7 +507,7 @@ namespace Barotrauma.Networking if (wrongPassword) { - var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUI.Style.Red, GUI.Font, textAlignment: Alignment.Center); + var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center); incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y); passwordHolder.Recalculate(); } @@ -651,7 +621,7 @@ namespace Barotrauma.Networking } GameAnalyticsManager.AddErrorEventOnce("GameClient.Update:CheckServerMessagesException" + e.TargetSite.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError("Error while reading a message from server.", e); - new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", new string[2] { "[message]", "[targetsite]" }, new string[2] { e.Message, e.TargetSite.ToString() })); + new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", e.Message), ("[targetsite]", e.TargetSite.ToString()))); Disconnect(); GameMain.ServerListScreen.Select(); return; @@ -734,6 +704,22 @@ namespace Barotrauma.Networking MultiPlayerCampaign campaign = GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; + if (Screen.Selected is ModDownloadScreen) + { + switch (header) + { + case ServerPacketHeader.UPDATE_LOBBY: + case ServerPacketHeader.PING_REQUEST: + case ServerPacketHeader.FILE_TRANSFER: + case ServerPacketHeader.PERMISSIONS: + case ServerPacketHeader.CHEATS_ENABLED: + //allow interpreting this packet + break; + default: + return; //ignore any other packets + } + } + switch (header) { case ServerPacketHeader.PING_REQUEST: @@ -980,9 +966,12 @@ namespace Barotrauma.Networking List contentToPreload = new List(); for (int i = 0; i < contentToPreloadCount; i++) { - ContentType contentType = (ContentType)inc.ReadByte(); string filePath = inc.ReadString(); - contentToPreload.Add(new ContentFile(filePath, contentType)); + ContentFile file = ContentPackageManager.EnabledPackages.All + .Select(p => + p.Files.FirstOrDefault(f => f.Path == filePath)) + .FirstOrDefault(f => !(f is null)); + contentToPreload.AddIfNotNull(file); } GameMain.GameSession.EventManager.PreloadContent(contentToPreload); @@ -1002,10 +991,10 @@ namespace Barotrauma.Networking string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; throw new Exception(errorMsg); } - List serverMissionIdentifiers = new List(); + List serverMissionIdentifiers = new List(); for (int i = 0; i < missionCount; i++) { - serverMissionIdentifiers.Add(inc.ReadString() ?? ""); + serverMissionIdentifiers.Add(inc.ReadIdentifier()); } if (missionCount > 0) @@ -1032,7 +1021,7 @@ namespace Barotrauma.Networking " (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.ShortHash + ")" + + ", 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); @@ -1048,7 +1037,7 @@ namespace Barotrauma.Networking ", 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.ShortHash + ")" + + ", 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,7 +1065,7 @@ namespace Barotrauma.Networking reconnectBox?.Close(); reconnectBox = null; - GameMain.Config.RestoreBackupPackages(); + ContentPackageManager.EnabledPackages.Restore(); GUI.ClearCursorWait(); @@ -1138,7 +1127,7 @@ namespace Barotrauma.Networking var queueBox = new GUIMessageBox( TextManager.Get("DisconnectReason.ServerFull"), - TextManager.Get("ServerFullQuestionPrompt"), new string[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); + TextManager.Get("ServerFullQuestionPrompt"), new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("ServerQueue") }); queueBox.Buttons[0].OnClicked += queueBox.Close; queueBox.Buttons[1].OnClicked += queueBox.Close; @@ -1178,15 +1167,15 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("Attempting to reconnect..."); //if the first part of the message is the disconnect reason Enum, don't include it in the popup message - string msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); - msg = string.IsNullOrWhiteSpace(msg) ? + LocalizedString msg = TextManager.GetServerMessage(disconnectReasonIncluded ? string.Join('/', splitMsg.Skip(1)) : disconnectMsg); + msg = msg.IsNullOrWhiteSpace() ? TextManager.Get("ConnectionLostReconnecting") : msg + '\n' + TextManager.Get("ConnectionLostReconnecting"); reconnectBox?.Close(); reconnectBox = new GUIMessageBox( TextManager.Get("ConnectionLost"), msg, - new string[] { TextManager.Get("Cancel") }); + new LocalizedString[] { TextManager.Get("Cancel") }); reconnectBox.Buttons[0].OnClicked += (btn, userdata) => { CancelConnect(); return true; }; connected = false; ConnectToServer(serverEndpoint, serverName); @@ -1196,7 +1185,7 @@ namespace Barotrauma.Networking connected = false; connectCancelled = true; - string msg = ""; + LocalizedString msg = ""; if (disconnectReason == DisconnectReason.Unknown) { DebugConsole.NewMessage("Not attempting to reconnect (unknown disconnect reason)."); @@ -1241,11 +1230,45 @@ namespace Barotrauma.Networking } } + private void OnConnectionInitializationComplete() + { + if (SteamManager.IsInitialized) + { + Steamworks.SteamFriends.ClearRichPresence(); + Steamworks.SteamFriends.SetRichPresence("status", "Playing on " + serverName); + Steamworks.SteamFriends.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); + } + + canStart = true; + connected = true; + + VoipClient = new VoipClient(this, clientPeer); + + if (Screen.Selected != GameMain.GameScreen) + { + GameMain.ModDownloadScreen.Select(); + } + else + { + entityEventManager.ClearSelf(); + foreach (Character c in Character.CharacterList) + { + c.ResetNetState(); + } + } + + chatBox.InputBox.Enabled = true; + if (GameMain.NetLobbyScreen?.ChatInput != null) + { + GameMain.NetLobbyScreen.ChatInput.Enabled = true; + } + } + private IEnumerable WaitInServerQueue() { waitInServerQueueBox = new GUIMessageBox( TextManager.Get("ServerQueuePleaseWait"), - TextManager.Get("WaitingInServerQueue"), new string[] { TextManager.Get("Cancel") }); + TextManager.Get("WaitingInServerQueue"), new LocalizedString[] { TextManager.Get("Cancel") }); waitInServerQueueBox.Buttons[0].OnClicked += (btn, userdata) => { CoroutineManager.StopCoroutines("WaitInServerQueue"); @@ -1273,7 +1296,7 @@ namespace Barotrauma.Networking private void ReadAchievement(IReadMessage inc) { - string achievementIdentifier = inc.ReadString(); + Identifier achievementIdentifier = inc.ReadIdentifier(); int amount = inc.ReadInt32(); if (amount == 0) { @@ -1289,16 +1312,16 @@ namespace Barotrauma.Networking { TraitorMessageType messageType = (TraitorMessageType)inc.ReadByte(); string missionIdentifier = inc.ReadString(); - string message = inc.ReadString(); - message = TextManager.GetServerMessage(message); + string messageFmt = inc.ReadString(); + LocalizedString message = TextManager.GetServerMessage(messageFmt); - var missionPrefab = TraitorMissionPrefab.List.Find(t => t.Identifier == missionIdentifier); + var missionPrefab = TraitorMissionPrefab.Prefabs.Find(t => t.Identifier == missionIdentifier); Sprite icon = missionPrefab?.Icon; switch (messageType) { case TraitorMessageType.Objective: - var isTraitor = !string.IsNullOrEmpty(message); + var isTraitor = !message.IsNullOrEmpty(); SpawnAsTraitor = isTraitor; TraitorFirstObjective = message; TraitorMission = missionPrefab; @@ -1309,11 +1332,11 @@ namespace Barotrauma.Networking } break; case TraitorMessageType.Console: - GameMain.Client.AddChatMessage(ChatMessage.Create("", message, ChatMessageType.Console, null)); + GameMain.Client.AddChatMessage(ChatMessage.Create("", message.Value, ChatMessageType.Console, null)); DebugConsole.NewMessage(message); break; case TraitorMessageType.ServerMessageBox: - var msgBox = new GUIMessageBox("", message, new string[0], type: GUIMessageBox.Type.InGame, icon: icon); + var msgBox = new GUIMessageBox("", message, Array.Empty(), type: GUIMessageBox.Type.InGame, icon: icon); if (msgBox.Icon != null) { msgBox.IconColor = missionPrefab.IconColor; @@ -1321,7 +1344,7 @@ namespace Barotrauma.Networking break; case TraitorMessageType.Server: default: - GameMain.Client.AddChatMessage(message, ChatMessageType.Server); + GameMain.Client.AddChatMessage(message.Value, ChatMessageType.Server); break; } } @@ -1369,7 +1392,7 @@ namespace Barotrauma.Networking msgBox.Content.ClearChildren(); msgBox.Content.RectTransform.RelativeSize = new Vector2(0.95f, 0.9f); - var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get("PermissionsChanged"), textAlignment: Alignment.Center, font: GUI.LargeFont); + var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgBox.Content.RectTransform), TextManager.Get("PermissionsChanged"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont); header.RectTransform.IsFixedSize = true; var permissionArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), msgBox.Content.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -1378,12 +1401,12 @@ namespace Barotrauma.Networking var permissionsLabel = new GUITextBlock(new RectTransform(new Vector2(newPermissions == ClientPermissions.None ? 2.0f : 1.0f, 0.0f), leftColumn.RectTransform), TextManager.Get(newPermissions == ClientPermissions.None ? "PermissionsRemoved" : "CurrentPermissions"), - wrap: true, font: (newPermissions == ClientPermissions.None ? GUI.Font : GUI.SubHeadingFont)); + wrap: true, font: (newPermissions == ClientPermissions.None ? GUIStyle.Font : GUIStyle.SubHeadingFont)); permissionsLabel.RectTransform.NonScaledSize = new Point(permissionsLabel.Rect.Width, permissionsLabel.Rect.Height); permissionsLabel.RectTransform.IsFixedSize = true; if (newPermissions != ClientPermissions.None) { - string permissionList = ""; + LocalizedString permissionList = ""; foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { if (!newPermissions.HasFlag(permission) || permission == ClientPermissions.None) { continue; } @@ -1396,12 +1419,12 @@ 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: GUI.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) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), commandList.Content.RectTransform, minSize: new Point(0, 15)), - permittedCommand, font: GUI.SmallFont) + permittedCommand, font: GUIStyle.SmallFont) { CanBeFocused = false }; @@ -1504,11 +1527,11 @@ namespace Barotrauma.Networking string subHash = inc.ReadString(); string shuttleName = inc.ReadString(); string shuttleHash = inc.ReadString(); - List missionIndices = new List(); + List missionHashes = new List(); int missionCount = inc.ReadByte(); for (int i = 0; i < missionCount; i++) { - missionIndices.Add(inc.ReadInt16()); + missionHashes.Add(inc.ReadUInt32()); } if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) { @@ -1525,7 +1548,7 @@ namespace Barotrauma.Networking //this shouldn't happen, TrySelectSub should stop the coroutine if the correct sub/shuttle cannot be found if (GameMain.NetLobbyScreen.SelectedSub == null || GameMain.NetLobbyScreen.SelectedSub.Name != subName || - GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash != subHash) + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash) { string errorMsg = "Failed to select submarine \"" + subName + "\" (hash: " + subHash + ")."; if (GameMain.NetLobbyScreen.SelectedSub == null) @@ -1538,9 +1561,9 @@ namespace Barotrauma.Networking { errorMsg += "\n" + "Name mismatch: " + GameMain.NetLobbyScreen.SelectedSub.Name + " != " + subName; } - if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash != subHash) + if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation != subHash) { - errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash + " != " + subHash; + errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.StringRepresentation + " != " + subHash; } } gameStarted = true; @@ -1552,7 +1575,7 @@ namespace Barotrauma.Networking } if (GameMain.NetLobbyScreen.SelectedShuttle == null || GameMain.NetLobbyScreen.SelectedShuttle.Name != shuttleName || - GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.Hash != shuttleHash) + GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash?.StringRepresentation != shuttleHash) { gameStarted = true; GameMain.NetLobbyScreen.Select(); @@ -1563,7 +1586,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - var selectedMissions = missionIndices.Select(i => MissionPrefab.List[i]); + var selectedMissions = missionHashes.Select(i => MissionPrefab.Prefabs.Find(p => p.UintIdentifier == i)); GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); @@ -1767,7 +1790,7 @@ namespace Barotrauma.Networking GameMain.GameScreen.Select(); - AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameMain.Config.KeyBindText(InputType.Chat)}~[radiobutton]={GameMain.Config.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); + AddChatMessage($"ServerMessage.HowToCommunicate~[chatbutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Chat)}~[radiobutton]={GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.RadioChat)}", ChatMessageType.Server); yield return CoroutineStatus.Success; } @@ -1850,7 +1873,7 @@ namespace Barotrauma.Networking byte subClass = inc.ReadByte(); bool requiredContentPackagesInstalled = inc.ReadBoolean(); - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); if (matchingSub == null) { matchingSub = new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false) @@ -1913,7 +1936,7 @@ namespace Barotrauma.Networking if (Screen.Selected != GameMain.GameScreen) { new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); - GameMain.NetLobbyScreen.Select(); + if (!(Screen.Selected is ModDownloadScreen)) { GameMain.NetLobbyScreen.Select(); } } } } @@ -1930,7 +1953,7 @@ namespace Barotrauma.Networking UInt64 steamId = inc.ReadUInt64(); UInt16 nameId = inc.ReadUInt16(); string name = inc.ReadString(); - string preferredJob = inc.ReadString(); + Identifier preferredJob = inc.ReadIdentifier(); byte preferredTeam = inc.ReadByte(); UInt16 characterID = inc.ReadUInt16(); float karma = inc.ReadSingle(); @@ -2088,7 +2111,7 @@ namespace Barotrauma.Networking bool isInitialUpdate = inc.ReadBoolean(); if (isInitialUpdate) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Received initial lobby update, ID: " + updateID + ", last ID: " + GameMain.NetLobbyScreen.LastUpdateID, Color.Gray); } @@ -2253,7 +2276,10 @@ namespace Barotrauma.Networking } break; case ServerNetObject.ENTITY_POSITION: - bool isItem = inc.ReadBoolean(); + inc.ReadPadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly + + bool isItem = inc.ReadBoolean(); inc.ReadPadBits(); + UInt32 incomingUintIdentifier = inc.ReadUInt32(); UInt16 id = inc.ReadUInt16(); uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); @@ -2272,11 +2298,18 @@ namespace Barotrauma.Networking { DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); } + else if (entity is MapEntity { Prefab: { UintIdentifier: { } uintIdentifier } } me && + uintIdentifier != incomingUintIdentifier) + { + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message." + +$"Entity identifier does not match (server entity is {MapEntityPrefab.List.FirstOrDefault(p => p.UintIdentifier == incomingUintIdentifier)?.Identifier.Value ?? "[not found]"}, " + +$"client entity is {me.Prefab.Identifier}). Ignoring the message..."); + } else { entity.ClientRead(objHeader.Value, inc, sendingTime); } - } + } //force to the correct position in case the entity doesn't exist //or the message wasn't read correctly for whatever reason @@ -2382,13 +2415,13 @@ namespace Barotrauma.Networking var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; if (jobPreferences.Count > 0) { - outmsg.Write(jobPreferences[0].First.Identifier); + outmsg.Write(jobPreferences[0].Prefab.Identifier); } else { outmsg.Write(""); } - outmsg.Write((byte)GameMain.Config.TeamPreference); + outmsg.Write((byte)MultiplayerPreferences.Instance.TeamPreference); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { @@ -2512,8 +2545,8 @@ namespace Barotrauma.Networking msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Initiate); msg.Write((byte)fileType); - if (file != null) msg.Write(file); - if (fileHash != null) msg.Write(fileHash); + msg.Write(file ?? throw new ArgumentNullException(nameof(file))); + msg.Write(fileHash ?? throw new ArgumentNullException(nameof(fileHash))); clientPeer.Send(msg, DeliveryMethod.Reliable); } @@ -2522,13 +2555,14 @@ namespace Barotrauma.Networking CancelFileTransfer(transfer.ID); } - public void UpdateFileTransfer(int id, int offset, bool reliable = false) + public void UpdateFileTransfer(int id, int expecting, int lastSeen, bool reliable = false) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Data); msg.Write((byte)id); - msg.Write(offset); + msg.Write(expecting); + msg.Write(lastSeen); clientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); } @@ -2550,7 +2584,7 @@ namespace Barotrauma.Networking var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.Hash == newSub.MD5Hash.Hash).ToList(); + var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation).ToList(); foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); @@ -2565,7 +2599,7 @@ namespace Barotrauma.Networking var subElement = subListChildren.FirstOrDefault(c => ((SubmarineInfo)c.UserData).Name == newSub.Name && - ((SubmarineInfo)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); + ((SubmarineInfo)c.UserData).MD5Hash.StringRepresentation == newSub.MD5Hash.StringRepresentation); if (subElement == null) continue; Color newSubTextColor = new Color(subElement.GetChild().TextColor, 1.0f); @@ -2585,25 +2619,25 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.FailedSelectedSub.HasValue && GameMain.NetLobbyScreen.FailedSelectedSub.Value.Name == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.Hash) + GameMain.NetLobbyScreen.FailedSelectedSub.Value.Hash == newSub.MD5Hash.StringRepresentation) { - GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.SubList); + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.SubList); } if (GameMain.NetLobbyScreen.FailedSelectedShuttle.HasValue && GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.Name && - GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Hash == newSub.MD5Hash.Hash) + GameMain.NetLobbyScreen.FailedSelectedShuttle.Value.Name == newSub.MD5Hash.StringRepresentation) { - GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.Hash, GameMain.NetLobbyScreen.ShuttleList.ListBox); + GameMain.NetLobbyScreen.TrySelectSub(newSub.Name, newSub.MD5Hash.StringRepresentation, GameMain.NetLobbyScreen.ShuttleList.ListBox); } - NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); + NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation); if (failedCampaignSub != default) { GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub); } - NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); + NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation); if (failedOwnedSub != default) { GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(newSub); @@ -2611,7 +2645,7 @@ namespace Barotrauma.Networking } // Replace a submarine dud with the downloaded version - SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.Hash == newSub.MD5Hash?.Hash); + SubmarineInfo existingServerSub = ServerSubmarines.Find(s => s.Name == newSub.Name && s.MD5Hash?.StringRepresentation == newSub.MD5Hash?.StringRepresentation); if (existingServerSub != null) { int existingIndex = ServerSubmarines.IndexOf(existingServerSub); @@ -2661,6 +2695,11 @@ namespace Barotrauma.Networking //(as there may have been campaign updates after the save file was created) campaign.LastUpdateID--; break; + case FileTransferType.Mod: + if (!(Screen.Selected is ModDownloadScreen)) { return; } + + GameMain.ModDownloadScreen.CurrentDownloadFinished(transfer); + break; } } @@ -2756,24 +2795,26 @@ namespace Barotrauma.Networking msg.Write(characterInfo == null); if (characterInfo == null) return; - msg.Write((byte)characterInfo.Gender); - msg.Write((byte)characterInfo.Race); - msg.Write((byte)characterInfo.HeadSpriteId); - msg.Write((byte)characterInfo.HairIndex); - msg.Write((byte)characterInfo.BeardIndex); - msg.Write((byte)characterInfo.MoustacheIndex); - msg.Write((byte)characterInfo.FaceAttachmentIndex); - msg.WriteColorR8G8B8(characterInfo.SkinColor); - msg.WriteColorR8G8B8(characterInfo.HairColor); - msg.WriteColorR8G8B8(characterInfo.FacialHairColor); + msg.Write((byte)characterInfo.Head.Preset.TagSet.Count); + foreach (Identifier tag in characterInfo.Head.Preset.TagSet) + { + msg.Write(tag); + } + msg.Write((byte)characterInfo.Head.HairIndex); + msg.Write((byte)characterInfo.Head.BeardIndex); + msg.Write((byte)characterInfo.Head.MoustacheIndex); + msg.Write((byte)characterInfo.Head.FaceAttachmentIndex); + msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); + msg.WriteColorR8G8B8(characterInfo.Head.HairColor); + msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; int count = Math.Min(jobPreferences.Count, 3); msg.Write((byte)count); for (int i = 0; i < count; i++) { - msg.Write(jobPreferences[i].First.Identifier); - msg.Write((byte)jobPreferences[i].Second); + msg.Write(jobPreferences[i].Prefab.Identifier); + msg.Write((byte)jobPreferences[i].Variant); } } @@ -2976,7 +3017,7 @@ namespace Barotrauma.Networking msg.Write(saveName); msg.Write(mapSeed); msg.Write(sub.Name); - msg.Write(sub.MD5Hash.Hash); + msg.Write(sub.MD5Hash.StringRepresentation); settings.Serialize(msg); clientPeer.Send(msg, DeliveryMethod.Reliable); @@ -3259,7 +3300,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.FileTransferFrame.UserData = transfer; GameMain.NetLobbyScreen.FileTransferTitle.Text = ToolBox.LimitString( - TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName), + TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName).Value, GameMain.NetLobbyScreen.FileTransferTitle.Font, GameMain.NetLobbyScreen.FileTransferTitle.Rect.Width); GameMain.NetLobbyScreen.FileTransferProgressBar.BarSize = transfer.Progress; @@ -3279,26 +3320,25 @@ namespace Barotrauma.Networking { if (EndVoteTickBox.Visible) { - EndVoteTickBox.Text = - (EndVoteTickBox.UserData as string) + " " + EndVoteCount + "/" + EndVoteMax; + EndVoteTickBox.Text = $"{endRoundVoteText} {EndVoteCount}/{EndVoteMax}"; } else { - string endVoteText = TextManager.GetWithVariables("EndRoundVotes", new string[2] { "[votes]", "[max]" }, new string[2] { EndVoteCount.ToString(), EndVoteMax.ToString() }); - GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUI.SmallFont.MeasureString(endVoteText) / 2, - endVoteText, + LocalizedString endVoteText = TextManager.GetWithVariables("EndRoundVotes", ("[votes]", EndVoteCount.ToString()), ("[max]", EndVoteMax.ToString())); + GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUIStyle.SmallFont.MeasureString(endVoteText) / 2, + endVoteText.Value, Color.White, - font: GUI.SmallFont); + font: GUIStyle.SmallFont); } } else { - EndVoteTickBox.Text = EndVoteTickBox.UserData as string; + EndVoteTickBox.Text = endRoundVoteText; } if (respawnManager != null) { - string respawnText = string.Empty; + LocalizedString respawnText = string.Empty; Color textColor = Color.White; bool canChooseRespawn = GameMain.GameSession.GameMode is CampaignMode && @@ -3316,9 +3356,9 @@ namespace Barotrauma.Networking } else if (respawnManager.PendingRespawnCount > 0) { - respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", - new string[] { "[deadplayers]", "[requireddeadplayers]" }, - new string[] { respawnManager.PendingRespawnCount.ToString(), respawnManager.RequiredRespawnCount.ToString() }); + respawnText = TextManager.GetWithVariables("RespawnWaitingForMoreDeadPlayers", + ("[deadplayers]", respawnManager.PendingRespawnCount.ToString()), + ("[requireddeadplayers]", respawnManager.RequiredRespawnCount.ToString())); } } else if (respawnManager.CurrentState == RespawnManager.State.Transporting && @@ -3333,13 +3373,13 @@ namespace Barotrauma.Networking //oscillate between 0-1 float phase = (float)(Math.Sin(timeLeft * MathHelper.Pi) + 1.0f) * 0.5f; //textScale = 1.0f + phase * 0.5f; - textColor = Color.Lerp(GUI.Style.Red, Color.White, 1.0f - phase); + textColor = Color.Lerp(GUIStyle.Red, Color.White, 1.0f - phase); } canChooseRespawn = false; } GameMain.GameSession?.SetRespawnInfo( - visible: !string.IsNullOrEmpty(respawnText) || canChooseRespawn, text: respawnText, textColor: textColor, + visible: !respawnText.IsNullOrEmpty() || canChooseRespawn, text: respawnText.Value, textColor: textColor, buttonsVisible: canChooseRespawn, waitForNextRoundRespawn: (WaitForNextRoundRespawn ?? true)); } @@ -3352,23 +3392,23 @@ namespace Barotrauma.Networking int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight * 0.3f); GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true); - GUI.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); + GUIStyle.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); if (client.ServerConnection != null) { - GUI.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); + GUIStyle.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); y += 15; - GUI.SmallFont.DrawString(spriteBatch, "Received bytes: " + client.Statistics.ReceivedBytes, new Vector2(x + 10, y + 45), Color.White); - GUI.SmallFont.DrawString(spriteBatch, "Received packets: " + client.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Received bytes: " + client.Statistics.ReceivedBytes, new Vector2(x + 10, y + 45), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Received packets: " + client.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White); - GUI.SmallFont.DrawString(spriteBatch, "Sent bytes: " + client.Statistics.SentBytes, new Vector2(x + 10, y + 75), Color.White); - GUI.SmallFont.DrawString(spriteBatch, "Sent packets: " + client.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Sent bytes: " + client.Statistics.SentBytes, new Vector2(x + 10, y + 75), Color.White); + GUIStyle.SmallFont.DrawString(spriteBatch, "Sent packets: " + client.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White); } else { - GUI.Font.DrawString(spriteBatch, "Disconnected", new Vector2(x + 10, y + 25), Color.White); + GUIStyle.Font.DrawString(spriteBatch, "Disconnected", new Vector2(x + 10, y + 25), Color.White); }*/ } @@ -3471,7 +3511,7 @@ namespace Barotrauma.Networking { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), - "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260)); + "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260)); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)) { @@ -3489,7 +3529,7 @@ namespace Barotrauma.Networking if (ban) { var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false); - new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true); permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent")) { @@ -3599,7 +3639,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode != null) { - errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name); + errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name.Value); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { errorLines.Add("Campaign ID: " + campaign.CampaignID); @@ -3623,19 +3663,18 @@ namespace Barotrauma.Networking errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); - foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate) + foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) { - errorLines.Add(" " + e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate); } errorLines.Add("Entity IDs:"); - List sortedEntities = Entity.GetEntities().ToList(); - sortedEntities.Sort((e1, e2) => e1.ID.CompareTo(e2.ID)); + Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray(); foreach (Entity e in sortedEntities) { - errorLines.Add(e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add(""); @@ -3645,7 +3684,7 @@ namespace Barotrauma.Networking errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text); } - string filePath = "event_error_log_client_" + Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + string filePath = $"event_error_log_client_{Name}_{DateTime.UtcNow.ToShortTimeString()}.log"; filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (!Directory.Exists(ServerLog.SavePath)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index e53215e38..e26337d66 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -28,7 +28,7 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 50.0f, 1.0f, nameof(KarmaIncreaseThreshold)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.12f), parent.RectTransform), TextManager.Get("Karma.PositiveActions"), - textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; @@ -41,7 +41,7 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(BallastFloraKarmaIncrease)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.12f), parent.RectTransform), TextManager.Get("Karma.NegativeActions"), - textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; @@ -86,9 +86,9 @@ namespace Barotrauma ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; - string labelText = TextManager.Get("Karma." + propertyName); + LocalizedString labelText = TextManager.Get("Karma." + propertyName); var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - labelText, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) + labelText, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; @@ -120,8 +120,8 @@ namespace Barotrauma ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; - string labelText = TextManager.Get("Karma." + propertyName); - new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), labelText, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) + LocalizedString labelText = TextManager.Get("Karma." + propertyName); + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), labelText, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip") }; @@ -140,7 +140,7 @@ namespace Barotrauma { var tickBox = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.1f), parent.RectTransform), TextManager.Get("Karma." + propertyName)) { - ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip", returnNull: true) ?? "" + ToolTip = TextManager.Get("Karma." + propertyName + "ToolTip").Fallback("") }; GameMain.NetworkMember.ServerSettings.AssignGUIComponent(propertyName, tickBox); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 893cfe2e2..0d67a7e1a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -120,7 +120,7 @@ namespace Barotrauma.Networking unreceivedEntityEventCount = msg.ReadUInt16(); firstNewID = msg.ReadUInt16(); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage( "received midround syncing msg, unreceived: " + unreceivedEntityEventCount + @@ -132,7 +132,7 @@ namespace Barotrauma.Networking MidRoundSyncingDone = true; if (firstNewID != null) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("midround syncing complete, switching to ID " + (UInt16) (firstNewID - 1), Microsoft.Xna.Framework.Color.Yellow); @@ -167,7 +167,7 @@ namespace Barotrauma.Networking if (entityID == Entity.NullEntityID) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", Microsoft.Xna.Framework.Color.Orange); @@ -188,12 +188,12 @@ namespace Barotrauma.Networking { if (thisEventID != (UInt16) (lastReceivedID + 1)) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage( "Received msg " + thisEventID + " (waiting for " + (lastReceivedID + 1) + ")", NetIdUtils.IdMoreRecent(thisEventID, (UInt16)(lastReceivedID + 1)) - ? GUI.Style.Red + ? GUIStyle.Red : Microsoft.Xna.Framework.Color.Yellow); } } @@ -201,7 +201,7 @@ namespace Barotrauma.Networking { DebugConsole.NewMessage( "Received msg " + thisEventID + ", entity " + entityID + " not found", - GUI.Style.Red); + GUIStyle.Red); GameMain.Client.ReportError(ClientNetError.MISSING_ENTITY, eventID: thisEventID, entityID: entityID); return false; } @@ -212,7 +212,7 @@ namespace Barotrauma.Networking else { int msgPosition = msg.BitPosition; - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (" + entity.ToString() + ")", Microsoft.Xna.Framework.Color.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs index e77762455..8604fcf9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs @@ -61,29 +61,29 @@ namespace Barotrauma.Networking GUI.DrawRectangle(spriteBatch, rect, Color.Black * 0.4f, true); graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, color: Color.Cyan); - graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, color: GUI.Style.Orange); + graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, color: GUIStyle.Orange); if (graphs[(int)NetStatType.ResentMessages].Average() > 0) { - graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, color: GUI.Style.Red); - GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", - new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); + graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, color: GUIStyle.Red); + GUIStyle.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", + new Vector2(rect.Right + 10, rect.Y + 50), GUIStyle.Red); } - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, "Peak received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].LargestValue()) + "/s " + "Avg received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].Average()) + "/s", new Vector2(rect.Right + 10, rect.Y + 10), Color.Cyan); - GUI.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " + + GUIStyle.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " + "Avg sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].Average()) + "/s", - new Vector2(rect.Right + 10, rect.Y + 30), GUI.Style.Orange); + new Vector2(rect.Right + 10, rect.Y + 30), GUIStyle.Orange); #if DEBUG /*int y = 10; foreach (KeyValuePair msgBytesSent in server.messageCount.OrderBy(key => -key.Value)) { - GUI.SmallFont.DrawString(spriteBatch, msgBytesSent.Key + ": " + MathUtils.GetBytesReadable(msgBytesSent.Value), - new Vector2(rect.Right - 200, rect.Y + y), GUI.Style.Red); + GUIStyle.SmallFont.DrawString(spriteBatch, msgBytesSent.Key + ": " + MathUtils.GetBytesReadable(msgBytesSent.Value), + new Vector2(rect.Right - 200, rect.Y + y), GUIStyle.Red); y += 15; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index cadf2a25b..3134ee4f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -2,6 +2,7 @@ using Barotrauma.Steam; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Text; @@ -10,30 +11,37 @@ namespace Barotrauma.Networking { abstract class ClientPeer { - protected class ServerContentPackage + public class ServerContentPackage { public readonly string Name; - public readonly string Hash; + public readonly Md5Hash Hash; public readonly UInt64 WorkshopId; public readonly DateTime InstallTime; - public ContentPackage RegularPackage + public RegularPackage RegularPackage { get { - return ContentPackage.RegularPackages.Find(p => p.MD5hash.Hash.Equals(Hash)); + return ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash)); } } - public ContentPackage CorePackage + public CorePackage CorePackage { get { - return ContentPackage.CorePackages.Find(p => p.MD5hash.Hash.Equals(Hash)); + return ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash)); } } - public ServerContentPackage(string name, string hash, UInt64 workshopId, DateTime installTime) + public ContentPackage ContentPackage + => (ContentPackage)RegularPackage ?? CorePackage; + + + public string GetPackageStr() + => $"\"{Name}\" (hash {Hash.ShortRepresentation})"; + + public ServerContentPackage(string name, Md5Hash hash, UInt64 workshopId, DateTime installTime) { Name = name; Hash = hash; @@ -42,14 +50,8 @@ namespace Barotrauma.Networking } } - protected string GetPackageStr(ContentPackage contentPackage) - { - return $"\"{contentPackage.Name}\" (hash {contentPackage.MD5hash.ShortHash})"; - } - protected string GetPackageStr(ServerContentPackage contentPackage) - { - return $"\"{contentPackage.Name}\" (hash {Md5Hash.GetShortHash(contentPackage.Hash)})"; - } + public ImmutableArray ServerContentPackages { get; private set; } = + ImmutableArray.Empty; public delegate void MessageCallback(IReadMessage message); public delegate void DisconnectCallback(bool disableReconnect); @@ -72,7 +74,7 @@ namespace Barotrauma.Networking public abstract void Start(object endPoint, int ownerKey); public abstract void Close(string msg = null, bool disableReconnect = false); public abstract void Update(float deltaTime); - public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod); + public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void SendPassword(string password); protected abstract void SendMsgInternal(DeliveryMethod deliveryMethod, IWriteMessage msg); @@ -108,7 +110,7 @@ namespace Barotrauma.Networking outMsg.Write(steamAuthTicket.Data, 0, steamAuthTicket.Data.Length); } outMsg.Write(GameMain.Version.ToString()); - outMsg.Write(GameMain.Config.Language); + outMsg.Write(GameSettings.CurrentConfig.Language.Value); SendMsgInternal(DeliveryMethod.Reliable, outMsg); break; @@ -122,121 +124,26 @@ namespace Barotrauma.Networking string serverName = inc.ReadString(); - UInt32 cpCount = inc.ReadVariableUInt32(); - ServerContentPackage corePackage = null; - List regularPackages = new List(); - List missingPackages = new List(); - for (int i = 0; i < cpCount; i++) + UInt32 packageCount = inc.ReadVariableUInt32(); + List serverPackages = new List(); + for (int i = 0; i < packageCount; i++) { string name = inc.ReadString(); - string hash = inc.ReadString(); + UInt32 hashByteCount = inc.ReadVariableUInt32(); + byte[] hashBytes = inc.ReadBytes((int)hashByteCount); UInt64 workshopId = inc.ReadUInt64(); UInt32 installTimeDiffSeconds = inc.ReadUInt32(); DateTime installTime = DateTime.UtcNow + TimeSpan.FromSeconds(installTimeDiffSeconds); - var pkg = new ServerContentPackage(name, hash, workshopId, installTime); - if (pkg.CorePackage != null) - { - corePackage = pkg; - } - else if (pkg.RegularPackage != null) - { - regularPackages.Add(pkg); - } - else - { - missingPackages.Add(pkg); - } - } - - if (missingPackages.Count > 0) - { - var nonDownloadable = missingPackages.Where(p => p.WorkshopId == 0); - var mismatchedButDownloaded = missingPackages.Where(remote => - { - return ContentPackage.AllPackages.Any(local => - local.SteamWorkshopId != 0 && /* is a Workshop item */ - remote.WorkshopId == local.SteamWorkshopId && /* ids match */ - remote.InstallTime < local.InstallTime/* remote is older than local */); - }); - - if (mismatchedButDownloaded.Any()) - { - string disconnectMsg; - if (mismatchedButDownloaded.Count() == 1) - { - disconnectMsg = $"DisconnectMessage.MismatchedWorkshopMod~[incompatiblecontentpackage]={GetPackageStr(mismatchedButDownloaded.First())}"; - } - else - { - List packageStrs = new List(); - mismatchedButDownloaded.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - disconnectMsg = $"DisconnectMessage.MismatchedWorkshopMods~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"; - } - Close(disconnectMsg, disableReconnect: true); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.MissingContentPackage + "/" + disconnectMsg); - } - else if (nonDownloadable.Any()) - { - string disconnectMsg; - if (nonDownloadable.Count() == 1) - { - disconnectMsg = $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(nonDownloadable.First())}"; - } - else - { - List packageStrs = new List(); - nonDownloadable.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); - disconnectMsg = $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"; - } - Close(disconnectMsg, disableReconnect: true); - OnDisconnectMessageReceived?.Invoke(DisconnectReason.MissingContentPackage + "/" + disconnectMsg); - } - else - { - Close(disableReconnect: true); - - string missingModNames = "\n"; - int displayedModCount = 0; - foreach (ServerContentPackage missingPackage in missingPackages) - { - missingModNames += "\n- " + GetPackageStr(missingPackage); - displayedModCount++; - if (GUI.Font.MeasureString(missingModNames).Y > GameMain.GraphicsHeight * 0.5f) - { - missingModNames += "\n\n" + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (missingPackages.Count - displayedModCount).ToString()); - break; - } - } - missingModNames += "\n\n"; - - var msgBox = new GUIMessageBox( - TextManager.Get("WorkshopItemDownloadTitle"), - TextManager.GetWithVariable("WorkshopItemDownloadPrompt", "[items]", missingModNames), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => - { - GameMain.ServerListScreen.Select(); - IEnumerable downloads = - missingPackages.Select(p => new ServerListScreen.PendingWorkshopDownload(p.Hash, p.WorkshopId)); - GameMain.ServerListScreen.DownloadWorkshopItems(downloads, serverName, ServerConnection.EndPointString); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - } - - return; + var pkg = new ServerContentPackage(name, Md5Hash.BytesAsHash(hashBytes), workshopId, installTime); + serverPackages.Add(pkg); } if (!contentPackageOrderReceived) { - GameMain.Config.BackUpModOrder(); - GameMain.Config.SwapPackages(corePackage.CorePackage, regularPackages.Select(p => p.RegularPackage).ToList()); - contentPackageOrderReceived = true; + ServerContentPackages = serverPackages.ToImmutableArray(); + SendMsgInternal(DeliveryMethod.Reliable, outMsg); } - - SendMsgInternal(DeliveryMethod.Reliable, outMsg); break; case ConnectionInitialization.Password: if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 0aacf802e..38b49f398 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Networking netPeerConfiguration = new NetPeerConfiguration("barotrauma") { - UseDualModeSockets = GameMain.Config.UseDualModeSockets + UseDualModeSockets = GameSettings.CurrentConfig.UseDualModeSockets }; netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt @@ -175,13 +175,13 @@ namespace Barotrauma.Networking isActive = false; - netClient.Shutdown(msg ?? TextManager.Get("Disconnecting")); + netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value); netClient = null; steamAuthTicket?.Cancel(); steamAuthTicket = null; OnDisconnect?.Invoke(disableReconnect); } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } @@ -208,7 +208,7 @@ namespace Barotrauma.Networking NetOutgoingMessage lidgrenMsg = netClient.CreateMessage(); byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); lidgrenMsg.Write((UInt16)length); lidgrenMsg.Write(msgData, 0, length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 4100bb358..d6a96d556 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -242,7 +242,7 @@ namespace Barotrauma.Networking incomingDataMessages.Clear(); } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } @@ -250,7 +250,7 @@ namespace Barotrauma.Networking buf[0] = (byte)deliveryMethod; byte[] bufAux = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref bufAux, out bool isCompressed, out int length); + msg.PrepareForSending(ref bufAux, compressPastThreshold, out bool isCompressed, out int length); buf[1] = (byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index 9f4bc841b..d96fb7c5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -429,13 +429,13 @@ namespace Barotrauma.Networking Steamworks.SteamUser.OnValidateAuthTicketResponse -= OnAuthChange; } - public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!isActive) { return; } IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); msgToSend.Write(selfSteamID); msgToSend.Write(selfSteamID); msgToSend.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 596f5c8d0..4057e960c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -52,7 +52,7 @@ namespace Barotrauma.Networking if (Character.Controlled != null || (!(GameMain.GameSession?.IsRunning ?? false))) { return; } var respawnPrompt = new GUIMessageBox( TextManager.Get("tutorial.tryagainheader"), TextManager.Get("respawnquestionprompt"), - new string[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) + new LocalizedString[] { TextManager.Get("respawnquestionpromptrespawn"), TextManager.Get("respawnquestionpromptwait") }) { UserData = "respawnquestionprompt" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index f0abd892a..44e51dfdd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking public bool? FriendlyFireEnabled; public bool? AllowRespawn; public YesNoMaybe? TraitorsEnabled; - public string GameMode; + public Identifier GameMode; public PlayStyle? PlayStyle; public bool Recent; @@ -103,7 +103,7 @@ namespace Barotrauma.Networking frame.ClearChildren(); - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUI.LargeFont) + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) { ToolTip = ServerName }; @@ -143,7 +143,7 @@ namespace Barotrauma.Networking var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.06f) }, TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, - font: GUI.SmallFont, textAlignment: Alignment.Center, + font: GUIStyle.SmallFont, textAlignment: Alignment.Center, color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); playStyleName.RectTransform.IsFixedSize = true; @@ -188,7 +188,7 @@ namespace Barotrauma.Networking new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null); var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true }; - var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUI.SmallFont, wrap: true) + var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false }; @@ -197,7 +197,7 @@ namespace Barotrauma.Networking var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), - TextManager.Get(string.IsNullOrEmpty(GameMode) ? "Unknown" : "GameMode." + GameMode, returnNull: true) ?? GameMode, + TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value), textAlignment: Alignment.Right); GUITextBlock playStyleText = null; @@ -218,11 +218,11 @@ namespace Barotrauma.Networking subSelection.TextSize.X + subSelection.GetChild().TextSize.X > subSelection.Rect.Width || modeSelection.TextSize.X + modeSelection.GetChild().TextSize.X > modeSelection.Rect.Width) { - gameMode.Font = subSelection.Font = modeSelection.Font = GUI.SmallFont; - gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUI.SmallFont; + gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont; + gameMode.GetChild().Font = subSelection.GetChild().Font = modeSelection.GetChild().Font = GUIStyle.SmallFont; if (playStyleText != null) { - playStyleText.Font = playStyleText.GetChild().Font = GUI.SmallFont; + playStyleText.Font = playStyleText.GetChild().Font = GUIStyle.SmallFont; } } @@ -268,7 +268,7 @@ namespace Barotrauma.Networking }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), - TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUI.SubHeadingFont); + TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) { ScrollBarVisible = true }; if (ContentPackageNames.Count == 0) @@ -289,7 +289,7 @@ namespace Barotrauma.Networking }; if (i < ContentPackageHashes.Count) { - if (ContentPackage.AllPackages.Any(cp => cp.MD5hash.Hash == ContentPackageHashes[i])) + if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i])) { packageText.Selected = true; continue; @@ -298,14 +298,14 @@ namespace Barotrauma.Networking //workshop download link found if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) { - packageText.TextColor = GUI.Style.Yellow; + packageText.TextColor = GUIStyle.Yellow; packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); } else //no package or workshop download link found, tough luck { - packageText.TextColor = GUI.Style.Red; + packageText.TextColor = GUIStyle.Red; packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", - new string[2] { "[contentpackage]", "[hash]" }, new string[2] { ContentPackageNames[i], ContentPackageHashes[i] }); + ("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i])); } } } @@ -361,7 +361,7 @@ namespace Barotrauma.Networking info.RespondedToSteamQuery = null; - info.GameMode = element.GetAttributeString("GameMode", ""); + info.GameMode = element.GetAttributeIdentifier("GameMode", Identifier.Empty); info.GameVersion = element.GetAttributeString("GameVersion", ""); int maxPlayersElement = element.GetAttributeInt("MaxPlayers", 0); @@ -515,7 +515,7 @@ namespace Barotrauma.Networking element.SetAttributeValue("OwnerID", SteamManager.SteamIDUInt64ToString(OwnerID)); } - element.SetAttributeValue("GameMode", GameMode ?? ""); + element.SetAttributeValue("GameMode", GameMode); element.SetAttributeValue("GameVersion", GameVersion ?? ""); element.SetAttributeValue("MaxPlayers", MaxPlayers); if (PlayStyle.HasValue) { element.SetAttributeValue("PlayStyle", PlayStyle.Value.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 8e18c1c82..19a9ba612 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -49,7 +49,7 @@ namespace Barotrauma.Networking List tickBoxes = new List(); foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) { - var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, 30), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUI.SmallFont) + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, 30), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUIStyle.SmallFont) { Selected = true, TextColor = messageColor[msgType], @@ -84,8 +84,8 @@ namespace Barotrauma.Networking isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), filterArea.RectTransform), TextManager.Get("ServerLog.Filter"), - font: GUI.SubHeadingFont); - GUITextBox searchBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.SmallFont, createClearButton: true); + font: GUIStyle.SubHeadingFont); + GUITextBox searchBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.SmallFont, createClearButton: true); searchBox.OnTextChanged += (textBox, text) => { msgFilter = text; @@ -146,7 +146,7 @@ namespace Barotrauma.Networking List tickBoxes = new List(); foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) { - var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUI.SmallFont) + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, (int)(25 * GUI.Scale)), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[msgType]), font: GUIStyle.SmallFont) { Selected = true, TextColor = messageColor[msgType], @@ -191,9 +191,10 @@ namespace Barotrauma.Networking Anchor anchor = Anchor.TopLeft; Pivot pivot = Pivot.TopLeft; - if (line.RichData != null) + RichString richString = line.Text as RichString; + if (richString != null && richString.RichTextData.HasValue) { - foreach (var data in line.RichData) + foreach (var data in richString.RichTextData.Value) { if (!UInt64.TryParse(data.Metadata, out ulong id)) { return; } Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) @@ -215,7 +216,7 @@ namespace Barotrauma.Networking } var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), (textContainer ?? listBox.Content).RectTransform, anchor, pivot), - line.RichData, line.SanitizedText, wrap: true, font: GUI.SmallFont) + line.Text, wrap: true, font: GUIStyle.SmallFont) { TextColor = messageColor[line.Type], Visible = !msgTypeHidden[(int)line.Type], @@ -235,9 +236,9 @@ namespace Barotrauma.Networking textBlock.RectTransform.SetAsFirstChild(); } - if (line.RichData != null) + if (richString != null && richString.RichTextData.HasValue) { - foreach (var data in line.RichData) + foreach (var data in richString.RichTextData.Value) { textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 783d52326..430df186b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -77,18 +77,18 @@ namespace Barotrauma.Networking } } } - private Dictionary tempMonsterEnabled; + private Dictionary tempMonsterEnabled; partial void InitProjSpecific() { var properties = TypeDescriptor.GetProperties(GetType()).Cast(); - SerializableProperties = new Dictionary(); + SerializableProperties = new Dictionary(); foreach (var property in properties) { SerializableProperty objProperty = new SerializableProperty(property); - SerializableProperties.Add(property.Name.ToLowerInvariant(), objProperty); + SerializableProperties.Add(property.Name.ToIdentifier(), objProperty); } } @@ -168,7 +168,16 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) + public void ClientAdminWrite( + NetFlags dataToSend, + int? missionTypeOr = null, + int? missionTypeAnd = null, + float? levelDifficulty = null, + bool? autoRestart = null, + int traitorSetting = 0, + int botCount = 0, + int botSpawnMode = 0, + bool? useRespawnShuttle = null) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -208,7 +217,7 @@ namespace Barotrauma.Networking outMsg.Write(count); foreach (KeyValuePair prop in changedProperties) { - DebugConsole.NewMessage(prop.Value.Name, Color.Lime); + DebugConsole.NewMessage(prop.Value.Name.Value, Color.Lime); outMsg.Write(prop.Key); prop.Value.Write(outMsg, prop.Value.GUIComponentValue); } @@ -315,7 +324,7 @@ namespace Barotrauma.Networking RelativeSpacing = 0.02f }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("Settings"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), TextManager.Get("Settings"), font: GUIStyle.LargeFont); var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedFrame.RectTransform), isHorizontal: true) { @@ -326,12 +335,9 @@ namespace Barotrauma.Networking var tabContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform), style: "InnerFrame"); //tabs - var tabValues = Enum.GetValues(typeof(SettingsTab)).Cast().ToArray(); - string[] tabNames = new string[tabValues.Count()]; - for (int i = 0; i < tabNames.Length; i++) - { - tabNames[i] = TextManager.Get("ServerSettings" + tabValues[i] + "Tab"); - } + LocalizedString[] tabNames = + Enum.GetValues(typeof(SettingsTab)).Cast() + .Select(tv => TextManager.Get("ServerSettings" + tv + "Tab")).ToArray(); settingsTabs = new GUIFrame[tabNames.Length]; tabButtons = new GUIButton[tabNames.Length]; for (int i = 0; i < tabNames.Length; i++) @@ -367,7 +373,7 @@ namespace Barotrauma.Networking //*********************************************** // Sub Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection"), font: GUIStyle.SubHeadingFont); var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) { Stretch = true, @@ -377,7 +383,7 @@ namespace Barotrauma.Networking GUIRadioButtonGroup selectionMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont, style: "GUIRadioButton"); + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); selectionMode.AddRadioButton(i, selectionTick); } selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); @@ -386,7 +392,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(SubSelectionMode)).AssignGUIComponent(selectionMode); // Mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection"), font: GUIStyle.SubHeadingFont); selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), serverTab.RectTransform), isHorizontal: true) { Stretch = true, @@ -396,7 +402,7 @@ namespace Barotrauma.Networking selectionMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont, style: "GUIRadioButton"); + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUIStyle.SmallFont, style: "GUIRadioButton"); selectionMode.AddRadioButton(i, selectionTick); } selectionFrame.RectTransform.NonScaledSize = new Point(selectionFrame.Rect.Width, selectionFrame.Children.First().Rect.Height); @@ -413,7 +419,7 @@ namespace Barotrauma.Networking //*********************************************** - string autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; + LocalizedString autoRestartDelayLabel = TextManager.Get("ServerSettingsAutoRestartDelay") + " "; var startIntervalText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverTab.RectTransform), autoRestartDelayLabel); var startIntervalSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), barSize: 0.1f, style: "GUISlider") { @@ -437,7 +443,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(StartWhenClientsReady)).AssignGUIComponent(startWhenClientsReady); CreateLabeledSlider(serverTab, "ServerSettingsStartWhenClientsReadyRatio", out GUIScrollBar slider, out GUITextBlock sliderLabel); - string clientsReadyRequiredLabel = sliderLabel.Text; + LocalizedString clientsReadyRequiredLabel = sliderLabel.Text; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -481,7 +487,7 @@ namespace Barotrauma.Networking }; // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont); + 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)) { AutoHideScrollBar = true, @@ -493,7 +499,7 @@ namespace Barotrauma.Networking GUIRadioButtonGroup selectionPlayStyle = new GUIRadioButtonGroup(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playstyleList.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUI.SmallFont, style: "GUIRadioButton") + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playstyleList.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUIStyle.SmallFont, style: "GUIRadioButton") { ToolTip = TextManager.Get("servertagdescription." + playStyle) }; @@ -510,7 +516,7 @@ namespace Barotrauma.Networking CreateLabeledSlider(roundsTab, "ServerSettingsEndRoundVotesRequired", out slider, out sliderLabel); - string endRoundLabel = sliderLabel.Text; + LocalizedString endRoundLabel = sliderLabel.Text; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); GetPropertyData(nameof(EndVoteRequiredRatio)).AssignGUIComponent(slider); @@ -526,7 +532,7 @@ namespace Barotrauma.Networking GetPropertyData(nameof(AllowRespawn)).AssignGUIComponent(respawnBox); CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); - string intervalLabel = sliderLabel.Text; + LocalizedString intervalLabel = sliderLabel.Text; slider.Range = new Vector2(10.0f, 600.0f); slider.StepValue = 10.0f; GetPropertyData(nameof(RespawnInterval)).AssignGUIComponent(slider); @@ -549,11 +555,11 @@ namespace Barotrauma.Networking ToolTip = TextManager.Get("ServerSettingsMinRespawnToolTip") }; - string minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; + LocalizedString minRespawnLabel = TextManager.Get("ServerSettingsMinRespawn") + " "; CreateLabeledSlider(minRespawnLayout, "", out slider, out sliderLabel); sliderLabel.RectTransform.RelativeSize = Vector2.Zero; slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = minRespawnText.RawToolTip; + slider.ToolTip = minRespawnText.ToolTip; slider.UserData = minRespawnText; slider.Step = 0.1f; slider.Range = new Vector2(0.0f, 1.0f); @@ -573,11 +579,11 @@ namespace Barotrauma.Networking ToolTip = TextManager.Get("ServerSettingsRespawnDurationToolTip") }; - string respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; + LocalizedString respawnDurationLabel = TextManager.Get("ServerSettingsRespawnDuration") + " "; CreateLabeledSlider(respawnDurationLayout, "", out slider, out sliderLabel); sliderLabel.RectTransform.RelativeSize = Vector2.Zero; slider.RectTransform.RelativeSize = new Vector2(1.0f, 0.5f); - slider.ToolTip = respawnDurationText.RawToolTip; + slider.ToolTip = respawnDurationText.ToolTip; slider.UserData = respawnDurationText; slider.Step = 0.1f; slider.Range = new Vector2(60.0f, 660.0f); @@ -620,7 +626,7 @@ namespace Barotrauma.Networking LosMode[] losModes = (LosMode[])Enum.GetValues(typeof(LosMode)); for (int i = 0; i < losModes.Length; i++) { - var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUI.SmallFont, style: "GUIRadioButton"); + var losTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), losModeRadioButtonLayout.RectTransform), TextManager.Get($"LosMode{losModes[i]}"), font: GUIStyle.SmallFont, style: "GUIRadioButton"); losModeRadioButtonGroup.AddRadioButton(i, losTick); } GetPropertyData(nameof(LosMode)).AssignGUIComponent(losModeRadioButtonGroup); @@ -663,13 +669,13 @@ namespace Barotrauma.Networking }; InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys.ToList(); - tempMonsterEnabled = new Dictionary(MonsterEnabled); - foreach (string s in monsterNames) + List monsterNames = MonsterEnabled.Keys.ToList(); + tempMonsterEnabled = new Dictionary(MonsterEnabled); + foreach (Identifier s in monsterNames) { - string translatedLabel = TextManager.Get($"Character.{s}", true); + LocalizedString translatedLabel = TextManager.Get($"Character.{s}").Fallback(s.Value); var monsterEnabledBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), monsterFrame.Content.RectTransform) { MinSize = new Point(0, 25) }, - label: translatedLabel ?? s) + label: translatedLabel) { Selected = tempMonsterEnabled[s], OnSelected = (GUITickBox tb) => @@ -722,10 +728,10 @@ namespace Barotrauma.Networking RelativeSpacing = 0.05f }; - if (ip.InventoryIcon != null || ip.sprite != null) + if (ip.InventoryIcon != null || ip.Sprite != null) { GUIImage img = new GUIImage(new RectTransform(new Point(itemFrame.Rect.Height), itemFrame.RectTransform), - ip.InventoryIcon ?? ip.sprite, scaleToFit: true) + ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) { CanBeFocused = false }; @@ -733,7 +739,7 @@ namespace Barotrauma.Networking } new GUITextBlock(new RectTransform(new Vector2(0.75f, 1.0f), itemFrame.RectTransform), - ip.Name, font: GUI.SmallFont) + ip.Name, font: GUIStyle.SmallFont) { Wrap = true, CanBeFocused = false @@ -826,7 +832,7 @@ namespace Barotrauma.Networking tickBoxContainer.RectTransform.MinSize = new Point(0, (int)(tickBoxContainer.Content.Children.First().Rect.Height * 2.0f + tickBoxContainer.Padding.Y + tickBoxContainer.Padding.W)); CreateLabeledSlider(antigriefingTab, "ServerSettingsKickVotesRequired", out slider, out sliderLabel); - string votesRequiredLabel = sliderLabel.Text + " "; + LocalizedString votesRequiredLabel = sliderLabel.Text + " "; slider.Step = 0.2f; slider.Range = new Vector2(0.5f, 1.0f); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -838,7 +844,7 @@ namespace Barotrauma.Networking slider.OnMoved(slider, slider.BarScroll); CreateLabeledSlider(antigriefingTab, "ServerSettingsAutobanTime", out slider, out sliderLabel); - string autobanLabel = sliderLabel.Text + " "; + LocalizedString autobanLabel = sliderLabel.Text + " "; slider.Step = 0.01f; slider.Range = new Vector2(0.0f, MaxAutoBanTime); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => @@ -946,7 +952,7 @@ namespace Barotrauma.Networking slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), - string.IsNullOrEmpty(labelTag) ? "" : TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + string.IsNullOrEmpty(labelTag) ? "" : TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); @@ -965,7 +971,7 @@ namespace Barotrauma.Networking }; var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), - TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont) + TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { AutoScaleHorizontal = true }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs deleted file mode 100644 index a72a282f0..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ /dev/null @@ -1,1740 +0,0 @@ -using Barotrauma.Extensions; -using Barotrauma.IO; -using Barotrauma.Networking; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using System.Xml.Linq; -using Color = Microsoft.Xna.Framework.Color; - -namespace Barotrauma.Steam -{ - static partial class SteamManager - { - private static readonly Dictionary modCopiesInProgress = new Dictionary(); - - private static void InitializeProjectSpecific() - { - if (isInitialized) { return; } - - try - { - Steamworks.SteamClient.Init(AppID, false); - isInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; - - if (isInitialized) - { - DebugConsole.NewMessage("Logged in as " + GetUsername() + " (SteamID " + SteamIDUInt64ToString(GetSteamID()) + ")"); - - popularTags.Clear(); - int i = 0; - foreach (KeyValuePair commonness in tagCommonness) - { - popularTags.Insert(i, commonness.Key); - i++; - } - } - - Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; - } - catch (DllNotFoundException) - { - isInitialized = false; - initializationErrors.Add("SteamDllNotFound"); - } - catch (Exception e) - { -#if !DEBUG - DebugConsole.ThrowError("SteamManager initialization threw an exception", e); -#endif - isInitialized = false; - initializationErrors.Add("SteamClientInitFailed"); - } - - if (!isInitialized) - { - try - { - if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } - } - catch (Exception e) - { - if (GameSettings.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e); - } - } - } - - public static bool NetworkingDebugLog = false; - - private static void LogSteamworksNetworking(Steamworks.NetDebugOutput nType, string pszMsg) - { - DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); - } - - public static void SetSteamworksNetworkingDebugLog(bool enabled) - { - if (enabled) - { - Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.Everything; - } - else - { - Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.None; - } - } - - public static async Task InitRelayNetworkAccess() - { - if (!IsInitialized) { return; } - - await Task.Yield(); - Steamworks.SteamNetworkingUtils.InitRelayNetworkAccess(); - - SetSteamworksNetworkingDebugLog(true); - var status = Steamworks.SteamNetworkingUtils.Status; - while (status.Avail != Steamworks.SteamNetworkingAvailability.Current) - { - if (status.Avail == Steamworks.SteamNetworkingAvailability.CannotTry || - status.Avail == Steamworks.SteamNetworkingAvailability.Previously || - status.Avail == Steamworks.SteamNetworkingAvailability.Failed) - { - DebugConsole.ThrowError($"Failed to initialize Steamworks network relay: " + - $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + - $"{Steamworks.SteamNetworkingUtils.Status.AvailNetConfig}, " + - $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + - $"{Steamworks.SteamNetworkingUtils.Status.Msg}"); - break; - } - await Task.Delay(25); - status = Steamworks.SteamNetworkingUtils.Status; - } - SetSteamworksNetworkingDebugLog(false); - } - - private enum LobbyState - { - NotConnected, - Creating, - Owner, - Joining, - Joined - } - private static UInt64 lobbyID = 0; - private static LobbyState lobbyState = LobbyState.NotConnected; - private static Steamworks.Data.Lobby? currentLobby; - public static UInt64 CurrentLobbyID - { - get { return currentLobby?.Id ?? 0; } - } - - public static void CreateLobby(ServerSettings serverSettings) - { - if (lobbyState != LobbyState.NotConnected) { return; } - lobbyState = LobbyState.Creating; - TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), - (lobby) => - { - if (lobbyState != LobbyState.Creating) - { - LeaveLobby(); - return; - } - - lobby.TryGetResult(out currentLobby); - if (currentLobby == null) - { - DebugConsole.ThrowError("Failed to create Steam lobby: returned lobby was null"); - lobbyState = LobbyState.NotConnected; - return; - } - - if (currentLobby.Value.Result != Steamworks.Result.OK) - { - DebugConsole.ThrowError($"Failed to create Steam lobby: result was {currentLobby.Value.Result}"); - lobbyState = LobbyState.NotConnected; - return; - } - - DebugConsole.NewMessage("Lobby created!", Microsoft.Xna.Framework.Color.Lime); - - lobbyState = LobbyState.Owner; - lobbyID = (currentLobby?.Id).Value; - - if (serverSettings.IsPublic) - { - currentLobby?.SetPublic(); - } - else - { - currentLobby?.SetFriendsOnly(); - } - currentLobby?.SetJoinable(true); - - UpdateLobby(serverSettings); - }); - } - - public static void UpdateLobby(ServerSettings serverSettings) - { - if (GameMain.Client == null) - { - LeaveLobby(); - } - - if (lobbyState == LobbyState.NotConnected) - { - CreateLobby(serverSettings); - } - - if (lobbyState != LobbyState.Owner) - { - return; - } - - var contentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); - - currentLobby?.SetData("name", serverSettings.ServerName); - currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); - currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); - //currentLobby?.SetData("hostipaddress", lobbyIP); - string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); - currentLobby?.SetData("pinglocation", pingLocation ?? ""); - currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); - currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); - - currentLobby?.SetData("message", serverSettings.ServerMessageText); - currentLobby?.SetData("version", GameMain.Version.ToString()); - - currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); - currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); - currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); - currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); - currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); - currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); - currentLobby?.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); - currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); - currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); - currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); - currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); - currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); - currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); - currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier ?? ""); - - DebugConsole.Log("Lobby updated!"); - } - - public static void LeaveLobby() - { - if (lobbyState != LobbyState.NotConnected) - { - currentLobby?.Leave(); currentLobby = null; - lobbyState = LobbyState.NotConnected; - - lobbyID = 0; - - Steamworks.SteamMatchmaking.ResetActions(); - } - } - public static void JoinLobby(UInt64 id, bool joinServer) - { - if (currentLobby.HasValue && currentLobby.Value.Id == id) { return; } - if (lobbyID == id) { return; } - lobbyState = LobbyState.Joining; - lobbyID = id; - - TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), - (lobby) => - { - lobby.TryGetResult(out currentLobby); - lobbyState = LobbyState.Joined; - lobbyID = (currentLobby?.Id).Value; - if (joinServer) - { - GameMain.Instance.ConnectLobby = 0; - GameMain.Instance.ConnectName = currentLobby?.GetData("servername"); - GameMain.Instance.ConnectEndpoint = SteamIDUInt64ToString((currentLobby?.Owner.Id).Value); - } - }); - } - - public static bool GetServers(Action addToServerList, Action serverQueryFinished) - { - if (!isInitialized) { return false; } - - int doneTasks = 0; - void taskDone() - { - doneTasks++; - if (doneTasks >= 2) - { - serverQueryFinished?.Invoke(); - serverQueryFinished = null; - } - } - - - Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => - { - DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); - }; - - TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), - (t) => - { - Steamworks.Dispatch.OnDebugCallback = null; - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); - taskDone(); - return; - } - t.TryGetResult(out List lobbies); - IEnumerable lobbyAddCoroutine() - { - int i = 0; - foreach (var lobby in lobbies ?? Enumerable.Empty()) - { - if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } - - ServerInfo serverInfo = new ServerInfo(); - serverInfo.ServerName = lobby.GetData("name"); - serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); - serverInfo.LobbyID = lobby.Id; - bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); - serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; - serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; - serverInfo.RespondedToSteamQuery = true; - - AssignLobbyDataToServerInfo(lobby, serverInfo); - - addToServerList(serverInfo); - i++; - if (i >= 16) { yield return CoroutineStatus.Running; i = 0; } - } - taskDone(); - yield return CoroutineStatus.Success; - } - CoroutineManager.StartCoroutine(lobbyAddCoroutine()); - }); - - Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); - void onServer(Steamworks.Data.ServerInfo info, bool responsive) - { - if (string.IsNullOrEmpty(info.Name)) { return; } - - ServerInfo serverInfo = new ServerInfo - { - ServerName = info.Name, - HasPassword = info.Passworded, - IP = info.Address.ToString(), - Port = info.ConnectionPort.ToString(), - PlayerCount = info.Players, - MaxPlayers = info.MaxPlayers, - RespondedToSteamQuery = responsive - }; - - if (responsive) - { - TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), - (t) => - { - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); - return; - } - - t.TryGetResult(out Dictionary rules); - AssignServerRulesToServerInfo(rules, serverInfo); - - addToServerList(serverInfo); - }); - } - else - { - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); - } - - } - serverQuery.OnResponsiveServer += (info) => onServer(info, true); - serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); - - TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), - (t) => - { - serverQuery.Dispose(); - taskDone(); - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); - return; - } - }); - - return true; - } - - public static async Task> LobbyQueryRequest() - { - List allLobbies = new List(); - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() - .FilterDistanceWorldwide() - .WithMaxResults(50); - //steamworks seems to unable to retrieve more than 50 - //lobbies per request; to work around this, we'll make - //up to 10 requests, asking to ignore all previous results - //in each subsequent request - for (int i = 0; i < 10; i++) - { - Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); - if (lobbies == null) { break; } - foreach (var l in lobbies) - { - lobbyQuery = lobbyQuery - .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); - } - allLobbies.AddRange(lobbies); - } - - //make sure all returned lobbies are distinct, don't want any duplicates here - return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); - } - - public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - serverInfo.ServerMessage = lobby.GetData("message"); - serverInfo.GameVersion = lobby.GetData("version"); - - serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); - serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); - - string workshopIdData = lobby.GetData("contentpackageid"); - if (!string.IsNullOrEmpty(workshopIdData)) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); - } - else - { - string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); - if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } - if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } - - serverInfo.AllowSpectating = getLobbyBool("allowspectating"); - serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); - serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); - serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); - serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); - if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } - - serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; - serverInfo.GameMode = lobby.GetData("gamemode") ?? ""; - if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - - string pingLocation = lobby.GetData("pinglocation"); - if (!string.IsNullOrEmpty(pingLocation)) - { - serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); - } - - bool? getLobbyBool(string key) - { - string data = lobby.GetData(key); - if (string.IsNullOrEmpty(data)) { return null; } - return data == "True" || data == "true"; - } - } - - public static void AssignServerRulesToServerInfo(Dictionary rules, ServerInfo serverInfo) - { - serverInfo.OwnerVerified = true; - - if (rules == null) { return; } - - if (rules.ContainsKey("message")) serverInfo.ServerMessage = rules["message"]; - if (rules.ContainsKey("version")) serverInfo.GameVersion = rules["version"]; - - if (rules.ContainsKey("playercount")) - { - if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; - } - - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - if (rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); - if (rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); - if (rules.ContainsKey("contentpackageid")) - { - serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); - } - else if (rules.ContainsKey("contentpackageurl")) - { - string[] workshopUrls = rules["contentpackageurl"].Split(','); - serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); - } - - if (rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True"; - if (rules.ContainsKey("modeselectionmode")) - { - if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode; - } - if (rules.ContainsKey("subselectionmode")) - { - if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode; - } - if (rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = rules["allowspectating"] == "True"; - if (rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; - if (rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; - if (rules.ContainsKey("traitors")) - { - if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; - } - - if (rules.ContainsKey("gamestarted")) serverInfo.GameStarted = rules["gamestarted"] == "True"; - if (rules.ContainsKey("gamemode")) - { - serverInfo.GameMode = rules["gamemode"]; - } - if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) - { - serverInfo.PlayStyle = playStyle; - } - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - serverInfo.ContentPackageWorkshopIds.Clear(); - } - } - -#region Connecting to servers - - public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; - - DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); - Steamworks.BeginAuthResult startResult = Steamworks.SteamUser.BeginAuthSession(authTicketData, clientSteamID); - if (startResult != Steamworks.BeginAuthResult.OK) - { - DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); - } - - return startResult; - } - - public static void StopAuthSession(ulong clientSteamID) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) return; - - DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID); - Steamworks.SteamUser.EndAuthSession(clientSteamID); - } - -#endregion - -#region Workshop - - public const string WorkshopItemPreviewImageFolder = "Workshop"; - public const string PreviewImageName = "PreviewImage.png"; - public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; - - private static Sprite defaultPreviewImage; - public static Sprite DefaultPreviewImage - { - get - { - if (defaultPreviewImage == null) - { - defaultPreviewImage = new Sprite(DefaultPreviewImagePath, sourceRectangle: null); - } - return defaultPreviewImage; - } - } - - private static async Task> GetWorkshopItemsAsync(Steamworks.Ugc.Query query, int clampResults = 0, Predicate itemPredicate=null) - { - await Task.Yield(); - - int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - - List retVal = new List(); - while (resultPage.HasValue && resultPage?.ResultCount > 0) - { - if (itemPredicate != null) - { - retVal.AddRange(resultPage.Value.Entries.Where(it => itemPredicate(it))); - } - else - { - retVal.AddRange(resultPage.Value.Entries); - } - - if (clampResults > 0 && retVal.Count >= clampResults) - { - retVal = retVal.Take(clampResults).ToList(); - break; - } - - pageIndex++; - resultPage = await query.GetPageAsync(pageIndex); - } - - return retVal; - } - - public static void GetSubscribedWorkshopItems(Action> onItemsFound, List requireTags = null) - { - if (!isInitialized) return; - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByTotalUniqueSubscriptions() - .WhereUserSubscribed() - .WithLongDescription(); - if (requireTags != null) { query = query.WithTags(requireTags); } - - TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => - { - task.TryGetResult(out List result); onItemsFound?.Invoke(result); - }); - } - - public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) - { - if (!isInitialized) return; - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByTrend() - .WithLongDescription(); - if (requireTags != null) query.WithTags(requireTags); - - TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => - { - task.TryGetResult(out List entries); - - //count the number of each unique tag - foreach (var item in entries) - { - foreach (string tag in item.Tags) - { - if (string.IsNullOrEmpty(tag)) { continue; } - string caseInvariantTag = tag.ToLowerInvariant(); - if (!tagCommonness.ContainsKey(caseInvariantTag)) - { - tagCommonness[caseInvariantTag] = 1; - } - else - { - tagCommonness[caseInvariantTag]++; - } - } - } - //populate the popularTags list with tags sorted by commonness - popularTags.Clear(); - foreach (KeyValuePair tagCommonnessKVP in tagCommonness) - { - int i = 0; - while (i < popularTags.Count && - tagCommonness[popularTags[i]] > tagCommonnessKVP.Value) - { - i++; - } - popularTags.Insert(i, tagCommonnessKVP.Key); - } - onItemsFound?.Invoke(entries); - }); - } - - public static void GetPublishedWorkshopItems(Action> onItemsFound, List requireTags = null) - { - if (!isInitialized) return; - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .RankedByPublicationDate() - .WhereUserPublished() - .WithLongDescription(); - if (requireTags != null) query.WithTags(requireTags); - - TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => - { - task.TryGetResult(out List result); onItemsFound?.Invoke(result); - }); - } - - private static readonly HashSet pendingWorkshopSubscriptions = new HashSet(); - - public static void SubscribeToWorkshopItem(ulong id, Action onInstalled = null) - { - if (!isInitialized) return; - - if (id == 0) { return; } - - if (pendingWorkshopSubscriptions.Contains(id)) { return; } - - pendingWorkshopSubscriptions.Add(id); - TaskPool.Add( - $"SubscribeToWorkshopItem({id})", - Task.Run(async () => - { - Steamworks.Ugc.Item? item = await Steamworks.SteamUGC.QueryFileAsync(id); - - if (!item.HasValue) - { - DebugConsole.ThrowError($"Failed to find a Steam Workshop item with the ID {id}."); - return null; - } - - if (!(item?.IsSubscribed ?? false)) - { - bool subscribed = await item?.Subscribe(); - if (!subscribed) - { - DebugConsole.ThrowError($"Failed to subscribe to Steam Workshop item with the ID {id}."); - return null; - } - } - - return item; - }), - (t) => - { - bool shouldCleanup = true; - if (t.IsFaulted) - { - TaskPool.PrintTaskExceptions(t, $"Workshop subscription task {id} faulted"); - } - else - { - t.TryGetResult(out Steamworks.Ugc.Item? item); - if (item != null) - { - if (item?.IsInstalled ?? false) - { - onInstalled?.Invoke(); - } - else - { - void _onInstalled() - { - onInstalled?.Invoke(); - pendingWorkshopSubscriptions.Remove(id); - } - bool downloading = item?.Download(_onInstalled) ?? false; - if (!downloading) - { - DebugConsole.ThrowError($"Failed to start downloading Steam Workshop item with the ID {id}."); - } - else - { - shouldCleanup = false; - } - } - } - - if (shouldCleanup) - { - pendingWorkshopSubscriptions.Remove(id); - } - } - }); - } - - public static void CreateWorkshopItemStaging(ContentPackage contentPackage, out Steamworks.Ugc.Editor? itemEditor) - { - string folderPath = Path.GetDirectoryName(contentPackage.Path); - if (!Directory.Exists(folderPath)) { Directory.CreateDirectory(folderPath); } - itemEditor = Steamworks.Ugc.Editor.NewCommunityFile - .WithPublicVisibility() - .ForAppId(AppID) - .WithContent(folderPath); - - string previewImagePath = Path.GetFullPath(Path.Combine(folderPath, PreviewImageName)); - - if (!File.Exists(previewImagePath)) - { - File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); - } - } - - /// - /// Creates a new empty content package - /// - public static void CreateWorkshopItemStaging(string itemName, out Steamworks.Ugc.Editor? itemEditor, out ContentPackage contentPackage) - { - string dirPath = Path.Combine("Mods", ToolBox.RemoveInvalidFileNameChars(itemName)); - Directory.CreateDirectory("Mods"); - Directory.CreateDirectory(dirPath); - - itemEditor = Steamworks.Ugc.Editor.NewCommunityFile -#if DEBUG - .WithPrivateVisibility() -#else - .WithPublicVisibility() -#endif - .ForAppId(AppID) - .WithContent(dirPath); - - string previewImagePath = Path.GetFullPath(Path.Combine(dirPath, PreviewImageName)); - if (!File.Exists(previewImagePath)) - { - File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath); - } - - //create a new content package and include the copied files in it - contentPackage = ContentPackage.CreatePackage(itemName, Path.Combine(dirPath, MetadataFileName), false); - contentPackage.Save(Path.Combine(dirPath, MetadataFileName)); - } - - /// - /// Creates a copy of the specified workshop item in the staging folder and an editor that can be used to edit and update the item - /// - public static bool CreateWorkshopItemStaging(Steamworks.Ugc.Item? existingItem, out Steamworks.Ugc.Editor? itemEditor, out ContentPackage contentPackage) - { - if (!(existingItem?.IsInstalled ?? false)) - { - itemEditor = null; - contentPackage = null; - DebugConsole.ThrowError("Cannot edit the workshop item \"" + (existingItem?.Title ?? "[NULL]") + "\" because it has not been installed."); - return false; - } - - itemEditor = new Steamworks.Ugc.Editor(existingItem.Value.Id) - .ForAppId(AppID) - .WithTitle(existingItem.Value.Title) - .WithTags(existingItem.Value.Tags) - .WithDescription(existingItem.Value.Description); - - if (existingItem.Value.IsPublic) - { - itemEditor = itemEditor?.WithPublicVisibility(); - } - else if (existingItem.Value.IsFriendsOnly) - { - itemEditor = itemEditor?.WithFriendsOnlyVisibility(); - } - else if (existingItem.Value.IsPrivate) - { - itemEditor = itemEditor?.WithPrivateVisibility(); - } - - if (!CheckWorkshopItemInstalled(existingItem)) - { - if (!InstallWorkshopItem(existingItem, out string errorMsg)) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { existingItem?.Title, errorMsg })); - itemEditor = null; - contentPackage = null; - return false; - } - } - - ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem?.Directory, MetadataFileName)) { SteamWorkshopId = existingItem.Value.Id }; - string installedContentPackagePath = Path.GetFullPath(GetWorkshopItemContentPackagePath(tempContentPackage)); - contentPackage = ContentPackage.AllPackages.FirstOrDefault(cp => Path.GetFullPath(cp.Path) == installedContentPackagePath); - - itemEditor = itemEditor?.WithContent(Path.GetDirectoryName(installedContentPackagePath)); - - string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor?.ContentFolder.FullName, PreviewImageName)); - itemEditor = itemEditor?.WithPreviewFile(previewImagePath); - - try - { - if (File.Exists(previewImagePath)) { File.Delete(previewImagePath); } - - Uri baseAddress = new Uri(existingItem?.PreviewImageUrl); - Uri directory = new Uri(baseAddress, "."); // "." == current dir, like MS-DOS - string fileName = Path.GetFileName(baseAddress.LocalPath); - - IRestClient client = new RestClient(directory); - var request = new RestRequest(fileName, Method.GET); - var response = client.Execute(request); - - if (response.ResponseStatus == ResponseStatus.Completed) - { - File.WriteAllBytes(previewImagePath, response.RawBytes); - } - } - - catch (Exception e) - { - string errorMsg = "Failed to save workshop item preview image when creating workshop item staging folder."; - GameAnalyticsManager.AddErrorEventOnce("SteamManager.CreateWorkshopItemStaging:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsManager.ErrorSeverity.Error, errorMsg + "\n" + e.Message); - } - - return true; - } - - public class WorkshopPublishStatus - { - public CoroutineHandle Coroutine; - public ContentPackage ContentPackage; - public Steamworks.Ugc.Editor? Item; - public bool? Success; - public Steamworks.Ugc.PublishResult? Result; - public TaskStatus? TaskStatus; - } - - public static WorkshopPublishStatus StartPublishItem(ContentPackage contentPackage, Steamworks.Ugc.Editor? item) - { - if (!isInitialized) return null; - - if (string.IsNullOrEmpty(item?.Title)) - { - DebugConsole.ThrowError("Cannot publish workshop item - title not set."); - return null; - } - if (string.IsNullOrEmpty(item?.ContentFolder?.FullName)) - { - DebugConsole.ThrowError("Cannot publish workshop item \"" + item?.Title + "\" - folder not set."); - return null; - } - if (!contentPackage.Files.Any()) - { - DebugConsole.ThrowError("Cannot publish workshop item \"" + item?.Title + "\" - no files defined."); - return null; - } - - contentPackage.GameVersion = GameMain.Version; - contentPackage.Save(contentPackage.Path); - - if (File.Exists(PreviewImageName)) { File.Delete(PreviewImageName); } - //move the preview image out of the staging folder, it does not need to be included in the folder sent to Workshop - File.Move(Path.GetFullPath(Path.Combine(item?.ContentFolder?.FullName, PreviewImageName)), PreviewImageName); - item = item?.WithPreviewFile(Path.GetFullPath(PreviewImageName)); - - var workshopPublishStatus = new WorkshopPublishStatus() { Item = item, Result = null, Success = null, ContentPackage = contentPackage }; - workshopPublishStatus.Coroutine = CoroutineManager.StartCoroutine(PublishItem(workshopPublishStatus)); - return workshopPublishStatus; - } - - private static IEnumerable PublishItem(WorkshopPublishStatus workshopPublishStatus) - { - if (!isInitialized) - { - yield return CoroutineStatus.Success; - } - - var item = workshopPublishStatus.Item; - var contentPackage = workshopPublishStatus.ContentPackage; - - Task task = item?.SubmitAsync(); - while (!task.IsCompleted) - { - yield return new WaitForSeconds(1.0f); - } - - if (task.Status != TaskStatus.RanToCompletion) - { - workshopPublishStatus.Success = false; - workshopPublishStatus.TaskStatus = task.Status; - - DebugConsole.NewMessage("Publishing workshop item " + item?.Title + " failed: task failed with status " + task.Status.ToString(), Color.Red); - } - else if (!task.Result.Success) - { - workshopPublishStatus.Success = false; - workshopPublishStatus.Result = task.Result; - DebugConsole.NewMessage("Publishing workshop item " + item?.Title + " failed: Workshop result "+task.Result.Result.ToString(), Color.Red); - } - else - { - //nuke the existing steamworks cache for the item we just published - ForceRedownload(task.Result.FileId); - - workshopPublishStatus.Success = true; - workshopPublishStatus.Result = task.Result; - DebugConsole.NewMessage("Published workshop item " + item?.Title + " successfully.", Microsoft.Xna.Framework.Color.LightGreen); - - contentPackage.SteamWorkshopId = task.Result.FileId.Value; - //NOTE: This sets InstallTime one hour into the future to guarantee - //that the published content package won't be autoupdated incorrectly. - //Change if it causes issues. - contentPackage.InstallTime = DateTime.UtcNow + TimeSpan.FromHours(1); - contentPackage.Save(contentPackage.Path); - - SubscribeToWorkshopItem(task.Result.FileId); - } - - yield return CoroutineStatus.Success; - } - - /// - /// Forces a Workshop item to redownload. - /// - public static void ForceRedownload(Steamworks.Data.PublishedFileId itemId, Action onDownloadFinished = null) - { - Steamworks.Ugc.Item itemToNuke = new Steamworks.Ugc.Item(itemId); - string directory = itemToNuke.Directory; - if (Directory.Exists(directory)) - { - try - { - Directory.Delete(directory, true); - } - catch (Exception e) { DebugConsole.ThrowError("Failed to delete Workshop item cache", e); } - } - DebugConsole.NewMessage($"{itemToNuke.Download(onDownloadFinished, highPriority: true)}"); - } - - /// - /// Installs a workshop item by moving it to the game folder. - /// - public static bool InstallWorkshopItem(Steamworks.Ugc.Item? itemOrNull, out string errorMsg, bool enableContentPackage = false, bool suppressInstallNotif = false, Action onInstall = null) - { - errorMsg = "Item is null"; - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return false; } - if (!item.IsInstalled) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEnable", "[itemname]", item.Title); - DebugConsole.NewMessage(errorMsg, Color.Red); - return false; - } - - string metaDataFilePath = Path.Combine(item.Directory, MetadataFileName); - - if (!File.Exists(metaDataFilePath)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEnable", "[itemname]", item.Title); - DebugConsole.ThrowError(errorMsg); - return false; - } - - ContentPackage contentPackage = new ContentPackage(metaDataFilePath) - { - SteamWorkshopId = item.Id - }; - string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage); - - List existingPackages = ContentPackage.AllPackages.Where(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath()).ToList(); - if (existingPackages.Any()) - { - if (item.Owner.Id != Steamworks.SteamClient.SteamId) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorSamePathInstalled", - new string[] { "[itemname]", "[itempath]" }, - new string[] { item.Title, Path.GetDirectoryName(newContentPackagePath) }); - return false; - } - else - { - RemoveMods(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == contentPackage.SteamWorkshopId, - false); - } - } - - if (!contentPackage.IsCompatible()) - { - errorMsg = TextManager.GetWithVariables(contentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage", - new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { contentPackage.Name, contentPackage.GameVersion.ToString(), GameMain.Version.ToString() }); - return false; - } - - Task newTask = null; - - lock (modCopiesInProgress) - { - if (modCopiesInProgress.ContainsKey(item.Id)) - { - errorMsg = ""; return true; - } - newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath); - modCopiesInProgress.Add(item.Id, newTask); - } - - TaskPool.Add("CopyWorkShopItemAsync", - newTask, - contentPackage, - (task, cp) => - { - try - { - if (task.IsFaulted || task.IsCanceled) - { - DebugConsole.ThrowError($"Failed to copy \"{item.Title}\"", task.Exception); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - task.TryGetResult(out string errorMsg); - if (!string.IsNullOrWhiteSpace(errorMsg)) - { - DebugConsole.ThrowError($"Failed to copy \"{item.Title}\": {errorMsg}"); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - - GameMain.Config.SuppressModFolderWatcher = true; - - var newPackage = new ContentPackage(cp.Path, newContentPackagePath) - { - SteamWorkshopId = item.Id, - InstallTime = item.Updated > item.Created ? item.Updated : item.Created - }; - - foreach (ContentFile contentFile in newPackage.Files) - { - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, cp, true); - } - - foreach (ContentFile file in existingPackages.SelectMany(p => p.Files)) - { - string path = CorrectContentFilePath(file.Path, file.Type, cp, true).CleanUpPath(); - if (newPackage.Files.Any(f => f.Path.CleanUpPath() == path)) { continue; } - newPackage.AddFile(path, file.Type); - } - - if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); - } - newPackage.Save(newContentPackagePath); - ContentPackage.AddPackage(newPackage); - - if (enableContentPackage) - { - if (newPackage.IsCorePackage) - { - GameMain.Config.SelectCorePackage(newPackage); - } - else - { - GameMain.Config.EnableRegularPackage(newPackage); - } - GameMain.Config.SaveNewPlayerConfig(); - - GameMain.Config.WarnIfContentPackageSelectionDirty(); - - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) - { - SubmarineInfo.RefreshSavedSubs(); - } - } - else if (!suppressInstallNotif) - { - GameMain.MainMenuScreen?.SetEnableModsNotification(true); - } - - GameMain.Config.SuppressModFolderWatcher = false; - - onInstall?.Invoke(newPackage); - - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); - } - catch - { - throw; - } - finally - { - modCopiesInProgress.Remove(item.Id); - } - }); - - errorMsg = ""; - return true; - } - - /// - /// Asynchronously copies a Workshop item into the Mods folder. - /// - /// Returns an empty string on success, otherwise returns an error message. - private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? itemOrNull, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath) - { - await Task.Yield(); - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return "Item is null"; } - - if (item.NeedsUpdate) - { - item.Download(highPriority: true); - await Task.Delay(1000); - } - while (item.NeedsUpdate && !item.IsDownloading && !item.IsDownloadPending && !item.IsInstalled) - { - if (!item.IsDownloading && !item.IsDownloadPending) - { - if (!item.Download()) - { - return TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item.Title); - } - } - await Task.Delay(1000); - } - - string targetPath = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)); - string copyingPath = Path.Combine(targetPath, CopyIndicatorFileName); - - string errorMsg = ""; - if (contentPackage.GameVersion > new Version(0, 9, 1, 0)) - { - Directory.CreateDirectory(targetPath); - File.WriteAllText(copyingPath, "TEMPORARY FILE"); - - SaveUtil.CopyFolder(item.Directory, targetPath, copySubDirs: true, overwriteExisting: false); - - File.Delete(copyingPath); - return ""; - } - - var allPackageFiles = Directory.GetFiles(item.Directory, "*", System.IO.SearchOption.AllDirectories); - List nonContentFiles = new List(); - foreach (string file in allPackageFiles) - { - if (file == metaDataFilePath) { continue; } - string relativePath = Path.GetRelativePath(item.Directory, file); - string fullPath = Path.GetFullPath(relativePath); - if (contentPackage.Files.Any(f => { string fp = Path.GetFullPath(f.Path); return fp == fullPath; })) { continue; } - nonContentFiles.Add(relativePath); - } - - /*if (File.Exists(newContentPackagePath) && !CheckFileEquality(newContentPackagePath, metaDataFilePath)) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); - DebugConsole.NewMessage(errorMsg, Color.Red); - return errorMsg; - } - - foreach (ContentFile contentFile in contentPackage.Files) - { - string sourceFile = Path.Combine(item?.Directory, contentFile.Path); - - if (File.Exists(sourceFile) && File.Exists(contentFile.Path) && !CheckFileEquality(sourceFile, contentFile.Path)) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); - DebugConsole.NewMessage(errorMsg, Color.Red); - return errorMsg; - } - }*/ - - Directory.CreateDirectory(targetPath); - File.WriteAllText(copyingPath, "TEMPORARY FILE"); - - foreach (ContentFile contentFile in contentPackage.Files) - { - contentFile.Path = contentFile.Path.CleanUpPathCrossPlatform(correctFilenameCase: true, item.Directory); - string sourceFile = Path.Combine(item.Directory, contentFile.Path); - if (!File.Exists(sourceFile)) - { - string[] splitPath = contentFile.Path.Split('/'); - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - sourceFile = Path.Combine(item.Directory, string.Join("/", splitPath.Skip(2))); - } - } - - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, contentPackage, - contentFile.Type != ContentType.Submarine); - - //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) - if (!ContentPackage.IsModFilePathAllowed(contentFile)) - { - //the content package is trying to copy a file to a prohibited path, which is not allowed - if (File.Exists(sourceFile)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorIllegalPathOnEnable", "[filename]", contentFile.Path); - return errorMsg; - } - //not trying to copy anything, so this is a reference to an external file - //if the external file doesn't exist, we cannot enable the package - else if (!File.Exists(contentFile.Path)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return errorMsg; - } - continue; - } - else if (!File.Exists(sourceFile)) - { - if (File.Exists(contentFile.Path)) - { - //the file is already present in the game folder, all good - continue; - } - else - { - //file not present in either the mod or the game folder -> cannot enable the package - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return errorMsg; - } - } - - //make sure the destination directory exists - Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); - CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: false); - } - - foreach (string nonContentFile in nonContentFiles) - { - string sourceFile = Path.Combine(item.Directory, nonContentFile); - if (!File.Exists(sourceFile)) { continue; } - string destinationPath = CorrectContentFilePath(nonContentFile, ContentType.None, contentPackage, false); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: false); - } - - File.Delete(copyingPath); - return ""; - } - - private static void RemoveMods(Func predicate, bool delete = true) - { - var toRemoveCore = ContentPackage.CorePackages.Where(predicate).ToList(); - if (toRemoveCore.Contains(GameMain.Config.CurrentCorePackage)) { GameMain.Config.AutoSelectCorePackage(toRemoveCore); } - - var toRemoveRegular = ContentPackage.RegularPackages.Where(predicate).ToList(); - var packagesToDeselect = GameMain.Config.EnabledRegularPackages.Where(p => toRemoveRegular.Contains(p)).ToList(); - foreach (var cp in packagesToDeselect) - { - GameMain.Config.DisableRegularPackage(cp); - } - - if (delete) - { - var toRemove = toRemoveCore.Concat(toRemoveRegular); - foreach (var cp in toRemove) - { - try - { - string path = Path.GetDirectoryName(cp.Path); - if (Directory.Exists(path)) { Directory.Delete(path, true); } - } - catch (Exception e) - { - DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); - } - ContentPackage.RemovePackage(cp); - } - } - - GameMain.Config.SaveNewPlayerConfig(); - - GameMain.Config.WarnIfContentPackageSelectionDirty(); - } - - /// - /// Uninstalls a workshop item by removing the files from the game folder. - /// - public static bool UninstallWorkshopItem(Steamworks.Ugc.Item? item, bool noLog, out string errorMsg) - { - errorMsg = null; - if (!(item?.IsInstalled ?? false)) - { - errorMsg = "Cannot disable workshop item \"" + item?.Title + "\" because it has not been installed."; - if (!noLog) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - } - return false; - } - - ContentPackage contentPackage = new ContentPackage(Path.Combine(item?.Directory, MetadataFileName)) - { - SteamWorkshopId = item?.Id ?? 0 - }; - - GameMain.Config.SuppressModFolderWatcher = true; - try - { - RemoveMods(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == contentPackage.SteamWorkshopId); - } - catch (Exception e) - { - errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace(); - if (!noLog) - { - DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red); - } - return false; - } - GameMain.Config.SuppressModFolderWatcher = false; - - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, false, null); - - errorMsg = ""; - return true; - } - - /// - /// Is the item compatible with this version of Barotrauma. Returns null if compatibility couldn't be determined (item not installed) - /// - public static bool? CheckWorkshopItemCompatibility(Steamworks.Ugc.Item? item) - { - if (!(item?.IsInstalled ?? false)) { return null; } - - string metaDataPath = Path.Combine(item?.Directory, MetadataFileName); - if (!File.Exists(metaDataPath)) - { - DebugConsole.ThrowError("Metadata file for the Workshop item \"" + item?.Title + "\" not found. The file may be corrupted.", appendStackTrace: true); - return null; - } - - ContentPackage contentPackage = new ContentPackage(metaDataPath); - return contentPackage.IsCompatible(); - } - - public static bool CheckWorkshopItemInstalled(Steamworks.Ugc.Item? itemOrNull) - { - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return false; } - if (!item.IsInstalled) { return false; } - - lock (modCopiesInProgress) - { - if (modCopiesInProgress.ContainsKey(item.Id)) - { - return true; - } - } - - if (item.NeedsUpdate && !item.IsDownloading && !item.IsDownloadPending) - { - item.Download(); - return false; - } - if (!Directory.Exists(item.Directory)) - { - DebugConsole.ThrowError("Workshop item \"" + item.Title + "\" has been installed but the install directory cannot be found. Attempting to redownload..."); - item.Download(); - return false; - } - - string metaDataPath = ""; - try - { - metaDataPath = Path.Combine(item.Directory, MetadataFileName); - } - catch (ArgumentException) - { - string errorMessage = "Metadata file for the Workshop item \"" + item.Title + - "\" not found. Could not combine path (" + (item.Directory ?? "directory name empty") + ")."; - DebugConsole.ThrowError(errorMessage); - GameAnalyticsManager.AddErrorEventOnce("SteamManager.CheckWorkshopItemInstalled:PathCombineException" + item.Title, - GameAnalyticsManager.ErrorSeverity.Error, - "Metadata file for a Workshop item not found. Could not combine path."); - return false; - } - - if (!File.Exists(metaDataPath)) - { - DebugConsole.ThrowError("Metadata file for the Workshop item \"" + item.Title + "\" not found. The file may be corrupted."); - return false; - } - - ContentPackage contentPackage = new ContentPackage(metaDataPath) - { - SteamWorkshopId = item.Id - }; - //make sure the contentpackage file is present - if (!File.Exists(GetWorkshopItemContentPackagePath(contentPackage)) || - !ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId == contentPackage.SteamWorkshopId || - (cp.SteamWorkshopId == 0 && cp.Name == contentPackage.Name))) - { - return false; - } - - return true; - } - - public static bool CheckWorkshopItemUpToDate(Steamworks.Ugc.Item? itemOrNull) - { - if (!itemOrNull.TryGetValue(out Steamworks.Ugc.Item item)) { return false; } - if (!item.IsInstalled || item.NeedsUpdate || item.IsDownloading || item.IsDownloadPending) { return false; } - - string metaDataPath = Path.Combine(item.Directory, MetadataFileName); - if (!File.Exists(metaDataPath)) - { - DebugConsole.ThrowError("Metadata file for the Workshop item \"" + item.Title + "\" not found. The file may be corrupted."); - return false; - } - - ContentPackage steamPackage = new ContentPackage(metaDataPath) - { - SteamWorkshopId = item.Id - }; - ContentPackage myPackage = ContentPackage.AllPackages.FirstOrDefault(cp => cp.SteamWorkshopId == steamPackage.SteamWorkshopId); - - if (myPackage?.InstallTime == null) - { - return false; - } - DateTime latestTime = item.Updated > item.Created ? item.Updated : item.Created; - bool upToDate = latestTime <= myPackage.InstallTime.Value; - return upToDate; - } - - public static async Task AutoUpdateWorkshopItemsAsync() - { - await Task.Yield(); - - if (!isInitialized) { return false; } - - var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) - .WhereUserSubscribed() - .WithLongDescription(); - - List items = await GetWorkshopItemsAsync(query); - - GameMain.Config.SuppressModFolderWatcher = true; - - //remove mods that the player is no longer subscribed to - RemoveMods(cp => cp.SteamWorkshopId != 0 && !items.Any(it => it.Id == cp.SteamWorkshopId)); - - GameMain.Config.SuppressModFolderWatcher = false; - - - List updateNotifications = new List(); - foreach (var item in items) - { - try - { - if (!item.IsInstalled) { continue; } - - bool installedSuccessfully = false; - string errorMsg; - if (!CheckWorkshopItemInstalled(item)) - { - installedSuccessfully = InstallWorkshopItem(item, out errorMsg); - } - else if (!CheckWorkshopItemUpToDate(item)) - { - installedSuccessfully = UpdateWorkshopItem(item, out errorMsg); - } - else - { - continue; - } - - if (!installedSuccessfully) - { - CrossThread.RequestExecutionOnMainThread(() => - { - DebugConsole.NewMessage(errorMsg, Color.Red); - string errorId = errorMsg; - if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })) - { - UserData = errorId - }; - } - }); - } - else - { - updateNotifications.Add(TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); - } - } - catch (Exception e) - { - CrossThread.RequestExecutionOnMainThread(() => - { - string errorId = e.Message; - if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })) - { - UserData = errorId - }; - } - GameAnalyticsManager.AddErrorEventOnce( - "SteamManager.AutoUpdateWorkshopItems:" + e.Message, - GameAnalyticsManager.ErrorSeverity.Error, - "Failed to autoupdate workshop item. " + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - }); - } - } - - if (updateNotifications.Count > 0) - { - CrossThread.RequestExecutionOnMainThread(() => - { - while (updateNotifications.Count > 0) - { - float width = updateNotifications.Max(notif => GUI.Font.MeasureString(notif).X) * 1.25f; - - int notificationsPerMsgBox = 20; - new GUIMessageBox("", string.Join('\n', updateNotifications.Take(notificationsPerMsgBox)), - relativeSize: new Microsoft.Xna.Framework.Vector2(0.25f, 0.0f), - minSize: new Microsoft.Xna.Framework.Point((int)width, 0)); - updateNotifications.RemoveRange(0, Math.Min(notificationsPerMsgBox, updateNotifications.Count)); - } - }); - } - - List tasks; - lock (modCopiesInProgress) - { - tasks = modCopiesInProgress.Values.ToList(); - } - await Task.WhenAll(tasks); - - return true; - } - - public static bool UpdateWorkshopItem(Steamworks.Ugc.Item? item, out string errorMsg) - { - errorMsg = ""; - if (!(item?.IsInstalled ?? false)) { return false; } - bool reenable = GameMain.Config.AllEnabledPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item?.Id); - if (item?.Owner.Id != Steamworks.SteamClient.SteamId) - { - if (!UninstallWorkshopItem(item, false, out errorMsg)) { return false; } - } - if (!InstallWorkshopItem(item, errorMsg: out errorMsg, enableContentPackage: reenable)) { return false; } - return true; - } - - private static string GetWorkshopItemContentPackagePath(ContentPackage contentPackage) - { - string packageName = contentPackage.Name.Trim(); - packageName = ToolBox.RemoveInvalidFileNameChars(packageName); - while (packageName.Last() == '.') { packageName = packageName.Substring(0, packageName.Length-1); } - //packageName = packageName + "_" + contentPackage.SteamWorkshopId.ToString(); - - return Path.Combine("Mods", packageName, MetadataFileName); - } - - private static void CorrectXMLFilePaths(ContentPackage package, XElement element) - { - foreach (var attr in element.Attributes()) - { - if ((attr.Name.ToString() == "file" || - attr.Name.ToString() == "folder" || - attr.Name.ToString() == "texture" || - attr.Name.ToString() == "monsterfile" || - attr.Name.ToString() == "characterfile") && - attr.Value.CleanUpPath().Contains("/")) - { - Enum.TryParse(attr.Name.LocalName, true, out ContentType type); - attr.Value = CorrectContentFilePath(attr.Value, type, package, true); - } - } - - foreach (var child in element.Elements()) - { - CorrectXMLFilePaths(package, child); - } - } - - private static void CorrectContentFileCopy(ContentPackage package, string src, string dest, bool overwrite) - { - if (!overwrite && File.Exists(dest)) { return; } - - if (Path.GetExtension(src).Equals(".xml", StringComparison.OrdinalIgnoreCase)) - { - XDocument doc = XMLExtensions.TryLoadXml(src); - if (doc != null) - { - CorrectXMLFilePaths(package, doc.Root); - using (System.IO.MemoryStream stream = new System.IO.MemoryStream()) - { - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); - settings.Indent = true; - settings.Encoding = new System.Text.UTF8Encoding(false); - using (var xmlWriter = System.Xml.XmlWriter.Create(stream, settings)) - { - doc.WriteTo(xmlWriter); - xmlWriter.Flush(); - string contents = System.Text.Encoding.UTF8.GetString(stream.ToArray()).Replace("\r\n", "\n"); - File.WriteAllText(dest, contents, System.Text.Encoding.UTF8); - } - } - } - else - { - File.Copy(src, dest, overwrite: true); - } - } - else - { - File.Copy(src, dest, overwrite: true); - } - } - - private static string CorrectContentFilePath(string contentFilePath, ContentType type, ContentPackage package, bool checkIfFileExists = false) - { - string packageName = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(package)); - - contentFilePath = contentFilePath.CleanUpPathCrossPlatform(); - - if (checkIfFileExists) - { - bool exists = File.Exists(contentFilePath); - if (type == ContentType.ServerExecutable) - { - exists |= File.Exists(Path.GetFileNameWithoutExtension(contentFilePath) + ".dll"); - } - if (exists) - { - return contentFilePath; - } - } - - string[] splitPath = contentFilePath.Split('/'); - if (splitPath.Length < 2 || splitPath[0] != "Mods" || splitPath[1] != packageName) - { - string newPath; - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - if (checkIfFileExists) - { - ContentPackage otherContentPackage = ContentPackage.AllPackages.FirstOrDefault(cp => cp.Name.Equals(splitPath[1], StringComparison.OrdinalIgnoreCase)); - if (otherContentPackage != null) - { - string otherPackageName = Path.GetDirectoryName(otherContentPackage.Path); - newPath = Path.Combine(otherPackageName, string.Join("/", splitPath.Skip(2))); - if (File.Exists(newPath)) - { - contentFilePath = newPath; - return contentFilePath; - } - } - } - splitPath = splitPath.Skip(Math.Clamp(splitPath.Length-1, 0, 2)).ToArray(); - newPath = Path.Combine(packageName, string.Join("/", splitPath)); - } - else - { - newPath = Path.Combine(packageName, contentFilePath); - } - contentFilePath = newPath; - } - - return contentFilePath.CleanUpPathCrossPlatform(false); - } - -#endregion - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index a32ef3bd0..98073f188 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -3,6 +3,7 @@ using Concentus.Structs; using Microsoft.Xna.Framework; using OpenAL; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; @@ -18,6 +19,9 @@ namespace Barotrauma.Networking private set; } + public static IReadOnlyList CaptureDeviceNames => + Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + private IntPtr captureDevice; private Thread captureThread; @@ -40,7 +44,7 @@ namespace Barotrauma.Networking public float Gain { - get { return GameMain.Config?.MicrophoneVolume ?? 1.0f; } + get { return GameSettings.CurrentConfig.Audio.MicrophoneVolume; } } public DateTime LastEnqueueAudio; @@ -106,16 +110,18 @@ namespace Barotrauma.Networking string errorCode = Alc.GetError(IntPtr.Zero).ToString(); if (!GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "capturedevicenotfound")) { - GUI.SettingsMenuOpen = false; + //GUI.SettingsMenuOpen = false; new GUIMessageBox(TextManager.Get("Error"), - (TextManager.Get("VoipCaptureDeviceNotFound", returnNull: true) ?? "Could not start voice capture, suitable capture device not found.") + " (" + errorCode + ")") + (TextManager.Get("VoipCaptureDeviceNotFound").Fallback("Could not start voice capture, suitable capture device not found.")) + " (" + errorCode + ")") { UserData = "capturedevicenotfound" }; } GameAnalyticsManager.AddErrorEventOnce("Alc.CaptureDeviceOpenFailed", GameAnalyticsManager.ErrorSeverity.Error, "Alc.CaptureDeviceOpen(" + deviceName + ") failed. Error code: " + errorCode); - GameMain.Config.VoiceSetting = GameSettings.VoiceMode.Disabled; + var config = GameSettings.CurrentConfig; + config.Audio.VoiceSetting = VoiceMode.Disabled; + GameSettings.SetCurrentConfig(config); Instance?.Dispose(); Instance = null; return; @@ -157,13 +163,15 @@ namespace Barotrauma.Networking public static void ChangeCaptureDevice(string deviceName) { - GameMain.Config.VoiceCaptureDevice = deviceName; + var config = GameSettings.CurrentConfig; + config.Audio.VoiceCaptureDevice = deviceName; + GameSettings.SetCurrentConfig(config); if (Instance != null) { UInt16 storedBufferID = Instance.LatestBufferID; Instance.Dispose(); - Create(GameMain.Config.VoiceCaptureDevice, storedBufferID); + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } } @@ -222,9 +230,9 @@ namespace Barotrauma.Networking LastAmplitude = maxAmplitude; bool allowEnqueue = overrideSound != null; - if (GameMain.WindowActive) + if (GameMain.WindowActive && SettingsMenu.Instance is null) { - ForceLocal = captureTimer > 0 ? ForceLocal : GameMain.Config.UseLocalVoiceByDefault; + ForceLocal = captureTimer > 0 ? ForceLocal : GameSettings.CurrentConfig.Audio.UseLocalVoiceByDefault; bool pttDown = false; if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && GUI.KeyboardDispatcher.Subscriber == null) @@ -239,14 +247,14 @@ namespace Barotrauma.Networking ForceLocal = false; } } - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Activity) { - if (dB > GameMain.Config.NoiseGateThreshold) + if (dB > GameSettings.CurrentConfig.Audio.NoiseGateThreshold) { allowEnqueue = true; } } - else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) + else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.PushToTalk) { if (pttDown) { @@ -277,7 +285,7 @@ namespace Barotrauma.Networking captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; if (allowEnqueue) { - captureTimer = GameMain.Config.VoiceChatCutoffPrevention; + captureTimer = GameSettings.CurrentConfig.Audio.VoiceChatCutoffPrevention; } prevCaptured = true; } @@ -378,6 +386,7 @@ namespace Barotrauma.Networking capturing = false; captureThread?.Join(); captureThread = null; + if (captureDevice != IntPtr.Zero) { Alc.CaptureCloseDevice(captureDevice); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 0d360bd80..85570fcbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -43,7 +43,7 @@ namespace Barotrauma.Networking public void SendToServer() { - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { if (VoipCapture.Instance != null) { @@ -54,8 +54,8 @@ namespace Barotrauma.Networking } else { - if (VoipCapture.Instance == null) VoipCapture.Create(GameMain.Config.VoiceCaptureDevice, storedBufferID); - if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) return; + if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) { return; } } if (DateTime.Now >= lastSendTime + VoipConfig.SEND_INTERVAL) @@ -80,7 +80,7 @@ namespace Barotrauma.Networking if (queue == null) { #if DEBUG - DebugConsole.NewMessage("Couldn't find VoipQueue with id " + queueId.ToString() + "!", GUI.Style.Red); + DebugConsole.NewMessage("Couldn't find VoipQueue with id " + queueId.ToString() + "!", GUIStyle.Red); #endif return; } @@ -102,7 +102,7 @@ namespace Barotrauma.Networking var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); - client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameMain.Config.DisableVoiceChatFilters; + client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; if (messageType == ChatMessageType.Radio) { client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); @@ -111,7 +111,7 @@ namespace Barotrauma.Networking { client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange); } - if (messageType != ChatMessageType.Radio && Character.Controlled != null && !GameMain.Config.DisableVoiceChatFilters) + if (messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters) { client.VoipSound.UseMuffleFilter = SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); } @@ -144,7 +144,7 @@ namespace Barotrauma.Networking { if (voiceIconSheetRects == null) { - var soundIconStyle = GUI.Style.GetComponentStyle("GUISoundIcon"); + var soundIconStyle = GUIStyle.GetComponentStyle("GUISoundIcon"); Rectangle sourceRect = soundIconStyle.Sprites.First().Value.First().Sprite.SourceRect; var indexPieces = soundIconStyle.Element.Attribute("sheetindices").Value.Split(';'); voiceIconSheetRects = new Rectangle[indexPieces.Length]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 8d5406f96..23458baca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -122,7 +122,7 @@ namespace Barotrauma if (sub.EqualityCheckVal == 0) { //sub doesn't exist client-side, use hash to let the server know which one we voted for - msg.Write(sub.MD5Hash.Hash); + msg.Write(sub.MD5Hash.StringRepresentation); } break; case VoteType.Mode: @@ -302,7 +302,7 @@ namespace Barotrauma } else if (GameMain.Client.ConnectedClients.Count > 1) { - GameMain.NetworkMember.AddChatMessage(VotingInterface.GetSubmarineVoteResultMessage(subInfo, voteType, yesClientCount.ToString(), noClientCount.ToString(), passed), ChatMessageType.Server); + GameMain.NetworkMember.AddChatMessage(VotingInterface.GetSubmarineVoteResultMessage(subInfo, voteType, yesClientCount.ToString(), noClientCount.ToString(), passed).Value, ChatMessageType.Server); } if (passed) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 0f535c9d7..f0457e030 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Particles public float AngleMinRad { get; private set; } public float AngleMaxRad { get; private set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)] public float AngleMin { get => angleMin; @@ -28,7 +28,7 @@ namespace Barotrauma.Particles } } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 360, MinValueFloat = -360f), Serialize(0f, IsPropertySaveable.Yes)] public float AngleMax { get => angleMax; @@ -39,77 +39,77 @@ namespace Barotrauma.Particles } } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float DistanceMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float DistanceMax { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float VelocityMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, IsPropertySaveable.Yes)] public float VelocityMax { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)] public float ScaleMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, IsPropertySaveable.Yes)] public float ScaleMax { get; set; } - [Editable(), Serialize("1,1", true)] + [Editable(), Serialize("1,1", IsPropertySaveable.Yes)] public Vector2 ScaleMultiplier { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)] public float EmitInterval { get; set; } - [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, true, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] + [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, IsPropertySaveable.Yes, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] public int ParticleAmount { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes)] public float ParticlesPerSecond { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] public float EmitAcrossRayInterval { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "Delay before the emitter becomes active after being created.")] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, IsPropertySaveable.Yes, description: "Delay before the emitter becomes active after being created.")] public float InitialDelay { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool HighQualityCollisionDetection { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool CopyEntityAngle { get; set; } - [Editable, Serialize("1,1,1,1", true)] + [Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)] public Color ColorMultiplier { get; set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DrawOnTop { get; set; } - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Angle { get => AngleMin; set => AngleMin = AngleMax = value; } - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Distance { get => DistanceMin; set => DistanceMin = DistanceMax = value; } - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Velocity { get => VelocityMin; set => VelocityMin = VelocityMax = value; } - public Dictionary SerializableProperties { get; } + public Dictionary SerializableProperties { get; } public ParticleEmitterProperties(XElement element) { @@ -125,7 +125,7 @@ namespace Barotrauma.Particles public readonly ParticleEmitterPrefab Prefab; - public ParticleEmitter(XElement element) + public ParticleEmitter(ContentXElement element) { Prefab = new ParticleEmitterPrefab(element); } @@ -253,40 +253,24 @@ namespace Barotrauma.Particles class ParticleEmitterPrefab { - private string particlePrefabName; + private readonly Identifier particlePrefabName; - private ParticlePrefab particlePrefab; - public ParticlePrefab ParticlePrefab - { - get - { - if (particlePrefab == null && particlePrefabName != null) - { - particlePrefab = GameMain.ParticleManager?.FindPrefab(particlePrefabName); - if (particlePrefab == null) - { - DebugConsole.ThrowError($"Failed to find particle prefab \"{particlePrefabName}\"."); - particlePrefabName = null; - } - } - return particlePrefab; - } - } + public ParticlePrefab ParticlePrefab => ParticlePrefab.Prefabs[particlePrefabName]; public readonly ParticleEmitterProperties Properties; public bool DrawOnTop => Properties.DrawOnTop || ParticlePrefab.DrawOnTop; - public ParticleEmitterPrefab(XElement element) + public ParticleEmitterPrefab(ContentXElement element) { Properties = new ParticleEmitterProperties(element); - particlePrefabName = element.GetAttributeString("particle", ""); + particlePrefabName = element.GetAttributeIdentifier("particle", ""); } public ParticleEmitterPrefab(ParticlePrefab prefab, ParticleEmitterProperties properties) { Properties = properties; - particlePrefab = prefab; + particlePrefabName = prefab.Identifier; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 97099d85f..558faee69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -44,8 +44,6 @@ namespace Barotrauma.Particles } private Particle[] particles; - public readonly PrefabCollection Prefabs = new PrefabCollection(); - private Camera cam; public Camera Camera @@ -58,62 +56,9 @@ namespace Barotrauma.Particles { this.cam = cam; - MaxParticles = GameMain.Config.ParticleLimit; + MaxParticles = GameSettings.CurrentConfig.Graphics.ParticleLimit; } - public void LoadPrefabs() - { - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) - { - LoadPrefabsFromFile(configFile); - } - } - - public void LoadPrefabsFromFile(ContentFile configFile) - { - var particleElements = new Dictionary(); - - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { return; } - - bool allowOverriding = false; - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - string name = element.Name.ToString().ToLowerInvariant(); - if (Prefabs.ContainsKey(name) || particleElements.ContainsKey(name)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing particle prefab '{name}' using the file '{configFile.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate particle prefab '{name}' found in '{configFile.Path}'! Each particle prefab must have a unique name. " + - "Use tags to override prefabs."); - continue; - } - } - particleElements.Add(name, element); - } - - foreach (var kvp in particleElements) - { - Prefabs.Add(new ParticlePrefab(kvp.Value, configFile), allowOverriding); - } - } - - public void RemovePrefabsByFile(string configFile) - { - Prefabs.RemoveByFile(configFile); - } public Particle CreateParticle(string prefabName, Vector2 position, float angle, float speed, Hull hullGuess = null, float collisionIgnoreTimer = 0f, Tuple tracerPoints = null) { return CreateParticle(prefabName, position, new Vector2((float)Math.Cos(angle), (float)-Math.Sin(angle)) * speed, angle, hullGuess, collisionIgnoreTimer, tracerPoints: tracerPoints); @@ -179,12 +124,12 @@ namespace Barotrauma.Particles public List GetPrefabList() { - return Prefabs.ToList(); + return ParticlePrefab.Prefabs.ToList(); } public ParticlePrefab FindPrefab(string prefabName) { - return Prefabs.Find(p => p.Identifier.Equals(prefabName, StringComparison.OrdinalIgnoreCase)); + return ParticlePrefab.Prefabs.Find(p => p.Identifier == prefabName); } private void RemoveParticle(int index) @@ -211,7 +156,7 @@ namespace Barotrauma.Particles public void Update(float deltaTime) { - MaxParticles = GameMain.Config.ParticleLimit; + MaxParticles = GameSettings.CurrentConfig.Graphics.ParticleLimit; for (int i = 0; i < particleCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 9882048e8..de2a858cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -6,13 +6,15 @@ using System.Xml.Linq; namespace Barotrauma.Particles { - class ParticlePrefab : IPrefab, IDisposable, ISerializableEntity + class ParticlePrefab : Prefab, ISerializableEntity { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public enum DrawTargetType { Air = 1, Water = 2, Both = 3 } public readonly List Sprites; - public void Dispose() + public override void Dispose() { GameMain.ParticleManager?.RemoveByPrefab(this); foreach (Sprite spr in Sprites) @@ -22,51 +24,25 @@ namespace Barotrauma.Particles Sprites.Clear(); } - public string Name - { - get; - private set; - } - - public string FilePath - { - get; - private set; - } - - public string OriginalName { get { return Name; } } - - public string Identifier { get { return Name; } } - - public ContentPackage ContentPackage - { - get; - private set; - } - - public string DisplayName - { - get; - private set; - } - - [Editable(0.0f, float.MaxValue), Serialize(5.0f, false, description: "How many seconds the particle remains alive.")] + public string Name => Identifier.Value; + + [Editable(0.0f, float.MaxValue), Serialize(5.0f, IsPropertySaveable.No, description: "How many seconds the particle remains alive.")] public float LifeTime { get; private set; } - [Editable(0.0f, float.MaxValue), Serialize(0.0f, false, description: "Will randomize lifetime value between lifetime and lifetimeMin. If left to 0 will use only lifetime value.")] + [Editable(0.0f, float.MaxValue), Serialize(0.0f, IsPropertySaveable.No, description: "Will randomize lifetime value between lifetime and lifetimeMin. If left to 0 will use only lifetime value.")] public float LifeTimeMin { get; private set; } - [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMin { get; private set; } - [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMax { get; private set; } //movement ----------------------------------------- private float angularVelocityMin; public float AngularVelocityMinRad { get; private set; } - [Editable, Serialize(0.0f, false)] + [Editable, Serialize(0.0f, IsPropertySaveable.No)] public float AngularVelocityMin { get { return angularVelocityMin; } @@ -80,7 +56,7 @@ namespace Barotrauma.Particles private float angularVelocityMax; public float AngularVelocityMaxRad { get; private set; } - [Editable, Serialize(0.0f, false)] + [Editable, Serialize(0.0f, IsPropertySaveable.No)] public float AngularVelocityMax { get { return angularVelocityMax; } @@ -94,7 +70,7 @@ namespace Barotrauma.Particles private float startRotationMin; public float StartRotationMinRad { get; private set; } - [Editable, Serialize(0.0f, false, description: "The minimum initial rotation of the particle (in degrees).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The minimum initial rotation of the particle (in degrees).")] public float StartRotationMin { get { return startRotationMin; } @@ -108,7 +84,7 @@ namespace Barotrauma.Particles private float startRotationMax; public float StartRotationMaxRad { get; private set; } - [Editable, Serialize(0.0f, false, description: "The maximum initial rotation of the particle (in degrees).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The maximum initial rotation of the particle (in degrees).")] public float StartRotationMax { get { return startRotationMax; } @@ -119,19 +95,19 @@ namespace Barotrauma.Particles } } - [Editable, Serialize(false, false, description: "Should the particle face the direction it's moving towards.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle face the direction it's moving towards.")] public bool RotateToDirection { get; private set; } - [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.No, description: "Drag applied to the particle when it's moving through air.")] public float Drag { get; private set; } - [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.No, description: "Drag applied to the particle when it's moving through water.")] public float WaterDrag { get; private set; } private Vector2 velocityChange; public Vector2 VelocityChangeDisplay { get; private set; } - [Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the velocity of the particle changes per second.")] public Vector2 VelocityChange { get { return velocityChange; } @@ -145,7 +121,7 @@ namespace Barotrauma.Particles private Vector2 velocityChangeWater; public Vector2 VelocityChangeWaterDisplay { get; private set; } - [Editable, Serialize("0.0,0.0", false, description: "How much the velocity of the particle changes per second when in water.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the velocity of the particle changes per second when in water.")] public Vector2 VelocityChangeWater { get { return velocityChangeWater; } @@ -156,78 +132,78 @@ namespace Barotrauma.Particles } } - [Editable(0.0f, 10000.0f), Serialize(0.0f, false, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] + [Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")] public float CollisionRadius { get; private set; } - [Editable, Serialize(false, false, description: "Does the particle collide with the walls of the submarine and the level.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle collide with the walls of the submarine and the level.")] public bool UseCollision { get; private set; } - [Editable, Serialize(false, false, description: "Does the particle disappear when it collides with something.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle disappear when it collides with something.")] public bool DeleteOnCollision { get; private set; } - [Editable(0.0f, 1.0f), Serialize(0.5f, false, description: "The friction coefficient of the particle, i.e. how much it slows down when it's sliding against a surface.")] + [Editable(0.0f, 1.0f), Serialize(0.5f, IsPropertySaveable.No, description: "The friction coefficient of the particle, i.e. how much it slows down when it's sliding against a surface.")] public float Friction { get; private set; } [Editable(0.0f, 1.0f)] - [Serialize(0.5f, false, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (1.0 = the particle stops completely).")] + [Serialize(0.5f, IsPropertySaveable.No, description: "How much of the particle's velocity is conserved when it collides with something, i.e. the \"bounciness\" of the particle. (1.0 = the particle stops completely).")] public float Restitution { get; private set; } //size ----------------------------------------- - [Editable, Serialize("1.0,1.0", false, description: "The minimum initial size of the particle.")] + [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The minimum initial size of the particle.")] public Vector2 StartSizeMin { get; private set; } - [Editable, Serialize("1.0,1.0", false, description: "The maximum initial size of the particle.")] + [Editable, Serialize("1.0,1.0", IsPropertySaveable.No, description: "The maximum initial size of the particle.")] public Vector2 StartSizeMax { get; private set; } - - [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMin { get; private set; } - [Editable, Serialize("0.0,0.0", false, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the size of the particle changes per second. The rate of growth for each particle is randomize between SizeChangeMin and SizeChangeMax.")] public Vector2 SizeChangeMax { get; private set; } - [Editable, Serialize(0.0f, false, description: "How many seconds it takes for the particle to grow to it's initial size.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How many seconds it takes for the particle to grow to it's initial size.")] public float GrowTime { get; private set; } //rendering ----------------------------------------- - [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No, description: "The initial color of the particle.")] public Color StartColor { get; private set; } - [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No, description: "The initial color of the particle.")] public Color MiddleColor { get; private set; } - [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The color of the particle at the end of its lifetime.")] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No, description: "The color of the particle at the end of its lifetime.")] public Color EndColor { get; private set; } - [Editable, Serialize(false, false, description: "If true the color will go from StartColor to EndcColor and back to StartColor.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "If true the color will go from StartColor to EndcColor and back to StartColor.")] public bool UseMiddleColor { get; private set; } - [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] + [Editable, Serialize(DrawTargetType.Air, IsPropertySaveable.No, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } - [Editable, Serialize(false, false, description: "Should the particle be always rendered on top of entities?")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the particle be always rendered on top of entities?")] public bool DrawOnTop { get; private set; } - [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] + [Editable, Serialize(ParticleBlendState.AlphaBlend, IsPropertySaveable.No, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } - [Editable, Serialize(0, false, description: "Particles with a higher priority can replace lower-priority ones if the maximum number of active particles has been reached.")] + [Editable, Serialize(0, IsPropertySaveable.No, description: "Particles with a higher priority can replace lower-priority ones if the maximum number of active particles has been reached.")] public int Priority { get; private set; } //animation ----------------------------------------- - [Editable(0.0f, float.MaxValue), Serialize(1.0f, false, description: "The duration of the particle's animation cycle (if it's animated).")] + [Editable(0.0f, float.MaxValue), Serialize(1.0f, IsPropertySaveable.No, description: "The duration of the particle's animation cycle (if it's animated).")] public float AnimDuration { get; private set; } - [Editable, Serialize(true, false, description: "Should the sprite animation be looped, or stay at the last frame when the animation finishes.")] + [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the sprite animation be looped, or stay at the last frame when the animation finishes.")] public bool LoopAnim { get; private set; } //---------------------------------------------------- public readonly List SubEmitters = new List(); - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -235,18 +211,13 @@ namespace Barotrauma.Particles //---------------------------------------------------- - public ParticlePrefab(XElement element, ContentFile file) + public ParticlePrefab(ContentXElement element, ContentFile file) : base(file, element.NameAsIdentifier()) { - Name = element.Name.ToString(); - FilePath = file.Path; - ContentPackage = file.ContentPackage; - DisplayName = TextManager.Get("particle." + Name, true) ?? Name; - Sprites = new List(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index 989e459a4..f14b1e743 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -66,7 +66,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Vector2(pos.X - 5, -(pos.Y + 5)), - Vector2.One * 10.0f, GUI.Style.Red, false, 0, 3); + Vector2.One * 10.0f, GUIStyle.Red, false, 0, 3); } if (drawOffset != Vector2.Zero) diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index d2ca4d04b..3aedb98d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -24,11 +24,11 @@ namespace Barotrauma public class KeyOrMouse { - public Keys Key { get; private set; } + public readonly Keys Key; - private string name; + private LocalizedString name; - public string Name + public LocalizedString Name { get { @@ -39,6 +39,9 @@ namespace Barotrauma public MouseButton MouseButton { get; private set; } + public static implicit operator KeyOrMouse(Keys key) { return new KeyOrMouse(key); } + public static implicit operator KeyOrMouse(MouseButton mouseButton) { return new KeyOrMouse(mouseButton); } + public KeyOrMouse(Keys keyBinding) { this.Key = keyBinding; @@ -47,6 +50,7 @@ namespace Barotrauma public KeyOrMouse(MouseButton mouseButton) { + this.Key = Keys.None; this.MouseButton = mouseButton; } @@ -112,14 +116,7 @@ namespace Barotrauma { if (obj is KeyOrMouse keyOrMouse) { - if (MouseButton != MouseButton.None) - { - return keyOrMouse.MouseButton == MouseButton; - } - else - { - return keyOrMouse.Key.Equals(Key); - } + return this == keyOrMouse; } else { @@ -127,6 +124,68 @@ namespace Barotrauma } } + public static bool operator ==(KeyOrMouse a, KeyOrMouse b) + { + if (a is null) + { + return b is null; + } + else if (a.MouseButton != MouseButton.None) + { + return a.MouseButton == b.MouseButton; + } + else + { + return a.Key.Equals(b.Key); + } + } + + public static bool operator !=(KeyOrMouse a, KeyOrMouse b) + { + return !(a == b); + } + + public static bool operator ==(KeyOrMouse keyOrMouse, Keys key) + { + if (keyOrMouse.MouseButton != MouseButton.None) { return false; } + return keyOrMouse.Key == key; + } + + public static bool operator !=(KeyOrMouse keyOrMouse, Keys key) + { + return !(keyOrMouse == key); + } + + public static bool operator ==(Keys key, KeyOrMouse keyOrMouse) + { + return keyOrMouse == key; + } + + public static bool operator !=(Keys key, KeyOrMouse keyOrMouse) + { + return keyOrMouse != key; + } + + public static bool operator ==(KeyOrMouse keyOrMouse, MouseButton mb) + { + return keyOrMouse.MouseButton == mb && keyOrMouse.Key == Keys.None; + } + + public static bool operator !=(KeyOrMouse keyOrMouse, MouseButton mb) + { + return !(keyOrMouse == mb); + } + + public static bool operator ==(MouseButton mb, KeyOrMouse keyOrMouse) + { + return keyOrMouse == mb; + } + + public static bool operator !=(MouseButton mb, KeyOrMouse keyOrMouse) + { + return keyOrMouse != mb; + } + public override string ToString() { switch (MouseButton) @@ -146,7 +205,7 @@ namespace Barotrauma return hashCode; } - public string GetName() + public LocalizedString GetName() { if (PlayerInput.NumberKeys.Contains(Key)) { @@ -196,10 +255,11 @@ namespace Barotrauma #if WINDOWS [DllImport("user32.dll")] static extern int GetSystemMetrics(int smIndex); + private const int SM_SWAPBUTTON = 23; public static bool MouseButtonsSwapped() { - return GetSystemMetrics(23) != 0; //SM_SWAPBUTTON + return GetSystemMetrics(SM_SWAPBUTTON) != 0; } #else public static bool MouseButtonsSwapped() @@ -428,17 +488,17 @@ namespace Barotrauma public static bool KeyHit(InputType inputType) { - return AllowInput && GameMain.Config.KeyBind(inputType).IsHit(); + return AllowInput && GameSettings.CurrentConfig.KeyMap.Bindings[inputType].IsHit(); } public static bool KeyDown(InputType inputType) { - return AllowInput && GameMain.Config.KeyBind(inputType).IsDown(); + return AllowInput && GameSettings.CurrentConfig.KeyMap.Bindings[inputType].IsDown(); } public static bool KeyUp(InputType inputType) { - return AllowInput && !GameMain.Config.KeyBind(inputType).IsDown(); + return AllowInput && !GameSettings.CurrentConfig.KeyMap.Bindings[inputType].IsDown(); } public static bool KeyHit(Keys button) @@ -449,7 +509,7 @@ namespace Barotrauma public static bool InventoryKeyHit(int index) { if (index == -1) return false; - return AllowInput && GameMain.Config.InventoryKeyBind(index).IsHit(); + return AllowInput && GameSettings.CurrentConfig.InventoryKeyMap.Bindings[index].IsHit(); } public static bool KeyDown(Keys button) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index cbc8188bf..436132dba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -104,9 +104,7 @@ namespace Barotrauma try { string exePath = System.Reflection.Assembly.GetEntryAssembly().Location; - var md5 = System.Security.Cryptography.MD5.Create(); - byte[] exeBytes = File.ReadAllBytes(exePath); - exeHash = new Md5Hash(exeBytes); + exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect); } catch { @@ -125,19 +123,17 @@ namespace Barotrauma { //exception occurred in loading screen: //assume content packages are the culprit and reset them - XDocument doc = XMLExtensions.TryLoadXml(GameSettings.PlayerSavePath); - XDocument baseDoc = XMLExtensions.TryLoadXml(GameSettings.SavePath); - if (doc != null && baseDoc != null) + XDocument doc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); + if (doc?.Root != null) { XElement newElement = new XElement(doc.Root.Name); newElement.Add(doc.Root.Attributes()); - string[] contentPackageTags = { "contentpackage", "contentpackages" }; - bool elementNameMatches(XElement element) - => contentPackageTags.Any(t => element.Name.LocalName.Equals(t, StringComparison.InvariantCultureIgnoreCase)); - newElement.Add(doc.Root.Elements().Where(e => !elementNameMatches(e))); - newElement.Add(baseDoc.Root.Elements().Where(e => elementNameMatches(e))); + Identifier[] contentPackageTags = { "contentpackage".ToIdentifier(), "contentpackages".ToIdentifier() }; + newElement.Add(doc.Root.Elements().Where(e => !contentPackageTags.Contains(e.NameAsIdentifier()))); + newElement.Add(new XElement("core", + new XAttribute("path", ContentPackageManager.VanillaFileList))); XDocument newDoc = new XDocument(newElement); - newDoc.Save(GameSettings.PlayerSavePath); + newDoc.Save(GameSettings.PlayerConfigPath); sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); sb.AppendLine("\n"); } @@ -148,22 +144,19 @@ namespace Barotrauma //welp i guess we couldn't reset the config! } - if (exeHash?.Hash != null) + if (exeHash?.StringRepresentation != null) { - sb.AppendLine(exeHash.Hash); + sb.AppendLine(exeHash.StringRepresentation); } sb.AppendLine("\n"); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); - if (GameMain.Config != null) + sb.AppendLine($"Graphics mode: {GameSettings.CurrentConfig.Graphics.Width}x{GameSettings.CurrentConfig.Graphics.Height} ({GameSettings.CurrentConfig.Graphics.DisplayMode})"); + sb.AppendLine("VSync " + (GameSettings.CurrentConfig.Graphics.VSync ? "ON" : "OFF")); + sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); + if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Graphics mode: " + GameMain.Config.GraphicsWidth + "x" + GameMain.Config.GraphicsHeight + " (" + GameMain.Config.WindowMode.ToString() + ")"); - sb.AppendLine("VSync " + (GameMain.Config.VSyncEnabled ? "ON" : "OFF")); - sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); - if (GameMain.Config.AllEnabledPackages != null) - { - sb.AppendLine("Selected content packages: " + (!GameMain.Config.AllEnabledPackages.Any() ? "None" : string.Join(", ", GameMain.Config.AllEnabledPackages.Select(c => c.Name)))); - } + sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); @@ -246,7 +239,12 @@ namespace Barotrauma if (GameAnalyticsManager.SendUserStatistics) { //send crash report before appending debug console messages (which may contain non-anonymous information) - GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, sb.ToString()); + string crashHeader = exception.Message; + if (exception.TargetSite != null) + { + crashHeader += " " + exception.TargetSite.ToString(); + } + GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Critical, crashHeader + "\n\n" + sb.ToString()); GameAnalyticsManager.ShutDown(); } @@ -260,8 +258,9 @@ namespace Barotrauma File.WriteAllText(filePath, crashReport); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } - + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameAnalyticsManager.SendUserStatistics) { CrashMessageBox("A crash report (\"" + filePath + "\") was saved in the root folder of the game and sent to the developers.", filePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs index cf7190c75..17e268d1e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignEndScreen.cs @@ -15,7 +15,7 @@ namespace Barotrauma public Action OnFinished; - private string textOverlay; + private LocalizedString textOverlay; private float textOverlayTimer; private Vector2 textOverlaySize; @@ -43,15 +43,15 @@ namespace Barotrauma { base.Select(); - textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUI.Font); - textOverlaySize = GUI.Font.MeasureString(textOverlay); + textOverlay = ToolBox.WrapText(TextManager.Get("campaignend1"), GameMain.GraphicsWidth / 3, GUIStyle.Font); + textOverlaySize = GUIStyle.Font.MeasureString(textOverlay); textOverlayTimer = 0.0f; video = Video.Load(GameMain.GraphicsDeviceManager.GraphicsDevice, GameMain.SoundManager, "Content/SplashScreens/Ending.webm"); video.Play(); creditsPlayer.Restart(); creditsPlayer.Visible = false; - SteamAchievementManager.UnlockAchievement("campaigncompleted", unlockClients: true); + SteamAchievementManager.UnlockAchievement("campaigncompleted".ToIdentifier(), unlockClients: true); } public override void Deselect() @@ -59,7 +59,7 @@ namespace Barotrauma video?.Dispose(); video = null; GUI.HideCursor = false; - SoundPlayer.OverrideMusicType = null; + SoundPlayer.OverrideMusicType = Identifier.Empty; } public override void Update(double deltaTime) @@ -67,7 +67,7 @@ namespace Barotrauma if (creditsPlayer.Finished) { OnFinished?.Invoke(); - SoundPlayer.OverrideMusicType = null; + SoundPlayer.OverrideMusicType = Identifier.Empty; } } @@ -82,7 +82,7 @@ namespace Barotrauma } else { - SoundPlayer.OverrideMusicType = "ending"; + SoundPlayer.OverrideMusicType = "ending".ToIdentifier(); float duration = 20.0f; float creditsDelay = 3.0f; if (textOverlayTimer < duration + creditsDelay) @@ -102,7 +102,7 @@ namespace Barotrauma { textAlpha = 1.0f; } - GUI.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); + GUIStyle.Font.DrawString(spriteBatch, textOverlay, new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 - textOverlaySize / 2, Color.White * textAlpha); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index 2bc4bee2b..e93563192 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -22,13 +22,13 @@ namespace Barotrauma }; // New game - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft); + 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) { textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), verticalLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft); + 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)); GUIFrame radiationBoxContainer @@ -36,7 +36,7 @@ namespace Barotrauma 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: GUI.Style.Font) + 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 @@ -44,8 +44,8 @@ namespace Barotrauma } 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", fallBackTag: "missions"), wrap: 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; @@ -91,7 +91,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) { - saveNameBox.Flash(GUI.Style.Red); + saveNameBox.Flash(GUIStyle.Red); return false; } @@ -106,7 +106,7 @@ namespace Barotrauma return false; } - if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) + if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) { new GUIMessageBox(TextManager.Get("error"), TextManager.Get("nohashsubmarineselected")); return false; @@ -127,7 +127,7 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => @@ -147,7 +147,7 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), TextManager.Get("ShuttleWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (button, obj) => { @@ -173,7 +173,7 @@ 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: GUI.Style.SmallFont, textColor: GUI.Style.Green) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUIStyle.SmallFont, textColor: GUIStyle.Green) { TextGetter = () => { @@ -195,8 +195,8 @@ namespace Barotrauma private IEnumerable WaitForCampaignSetup() { GUI.SetCursorWaiting(); - string headerText = TextManager.Get("CampaignStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("CampaignStarting"), new string[] { TextManager.Get("Cancel") }); + var headerText = TextManager.Get("CampaignStartingPleaseWait"); + var msgBox = new GUIMessageBox(headerText, TextManager.Get("CampaignStarting"), new LocalizedString[] { TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { @@ -270,7 +270,8 @@ namespace Barotrauma prevSaveFiles?.Add(saveFile); string[] splitSaveFile = saveFile.Split(';'); saveFrame.UserData = splitSaveFile[0]; - fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); + fileName = Path.GetFileNameWithoutExtension(splitSaveFile[0]); + nameText.Text = fileName; if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } @@ -283,27 +284,27 @@ namespace Barotrauma if (!string.IsNullOrEmpty(contentPackageStr)) { List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out LocalizedString errorMsg)) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); } } if (!isCompatible) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: subName, font: GUI.SmallFont) + text: subName, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTime, textAlignment: Alignment.Right, font: GUI.SmallFont) + text: saveTime, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName @@ -373,8 +374,8 @@ namespace Barotrauma string saveFile = obj as string; if (obj == null) { return false; } - string header = TextManager.Get("deletedialoglabel"); - string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + var header = TextManager.Get("deletedialoglabel"); + var body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); EventEditorScreen.AskForConfirmation(header, body, () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 2ce5f1b82..9e15a34c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -134,16 +134,16 @@ namespace Barotrauma columnContainer.Recalculate(); // New game left side - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont); saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) { textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); + 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: GUI.SubHeadingFont); + + 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); moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); @@ -155,11 +155,11 @@ namespace Barotrauma { Stretch = true }; - + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: 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); filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; @@ -187,7 +187,7 @@ namespace Barotrauma RelativeSpacing = 0.025f }; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1f), firstPageButtonContainer.RectTransform), "", font: GUI.Style.Font, textColor: GUI.Style.Green, textAlignment: Alignment.CenterLeft) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1f), firstPageButtonContainer.RectTransform), "", font: GUIStyle.Font, textColor: GUIStyle.Green, textAlignment: Alignment.CenterLeft) { TextGetter = () => { @@ -200,7 +200,7 @@ 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) => @@ -218,7 +218,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, @@ -238,8 +238,8 @@ namespace Barotrauma secondPageLayout.RelativeSpacing = 0.01f; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), - TextManager.Get("Crew"), font: GUI.Style.SubHeadingFont, textAlignment: Alignment.TopLeft); - + TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft); + characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) { Stretch = true, @@ -266,7 +266,7 @@ namespace Barotrauma OnClicked = FinishSetup }; } - + public void RandomizeCrew() { var characterInfos = new List<(CharacterInfo Info, JobPrefab Job)>(); @@ -275,9 +275,10 @@ namespace Barotrauma for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); - characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant), jobPrefab)); + characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant), jobPrefab)); } } + characterInfos.Sort((a, b) => Math.Sign(b.Job.MinKarma - a.Job.MinKarma)); characterInfoColumns.ClearChildren(); CharacterMenus?.ForEach(m => m.Dispose()); @@ -344,7 +345,7 @@ namespace Barotrauma private void CreateCustomizeWindow() { - CampaignCustomizeSettings = new GUIMessageBox("", "", new string[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); + CampaignCustomizeSettings = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); CampaignCustomizeSettings.Buttons[0].OnClicked += CampaignCustomizeSettings.Close; CampaignSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), CampaignCustomizeSettings.Content.RectTransform, Anchor.TopCenter)) @@ -355,7 +356,7 @@ namespace Barotrauma 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: GUI.Style.Font) + 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") @@ -366,25 +367,25 @@ namespace Barotrauma Stretch = true, ToolTip = TextManager.Get("maxmissioncounttooltip") }; - var maxMissionCountDescription = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), maxMissionCountSettingHolder.RectTransform), TextManager.Get("maxmissioncount", fallBackTag: "missions"), wrap: true); + 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) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) - 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); return true; } }; - string prevMaxMissionCountText = MaxMissionCountText?.Text ?? CampaignSettings.DefaultMaxMissionCount.ToString(); + 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") { OnClicked = (button, obj) => { - MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); + MaxMissionCountText.Text = Math.Clamp(Int32.Parse(MaxMissionCountText.Text.SanitizedValue) + 1, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit).ToString(); return true; } }; @@ -405,10 +406,10 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) { - saveNameBox.Flash(GUI.Style.Red); + saveNameBox.Flash(GUIStyle.Red); return false; } - + SubmarineInfo selectedSub = null; if (!(subList.SelectedData is SubmarineInfo)) { return false; } @@ -420,7 +421,7 @@ namespace Barotrauma return false; } - if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) + if (string.IsNullOrEmpty(selectedSub.MD5Hash.StringRepresentation)) { ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; subList.SelectedComponent.CanBeFocused = false; @@ -433,7 +434,7 @@ namespace Barotrauma CampaignSettings settings = new CampaignSettings(); settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) + if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text.SanitizedValue, out int missionCount)) { settings.MaxMissionCount = missionCount; } @@ -448,8 +449,8 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => { @@ -467,7 +468,7 @@ namespace Barotrauma { var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), TextManager.Get("ShuttleWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (button, obj) => { @@ -499,7 +500,7 @@ namespace Barotrauma { var sub = child.UserData as SubmarineInfo; if (sub == null) { return; } - child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.ToLower().Contains(filter.ToLower()); + child.Visible = string.IsNullOrEmpty(filter) || sub.DisplayName.Contains(filter.ToLower(), StringComparison.OrdinalIgnoreCase); } } @@ -522,7 +523,7 @@ namespace Barotrauma sub.CreatePreviewWindow(subPreviewContainer); return true; } - + public void CreateDefaultSaveName() { string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer); @@ -555,7 +556,7 @@ namespace Barotrauma { var textBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), style: "ListBoxElement") + ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), style: "ListBoxElement") { ToolTip = sub.Description, UserData = sub @@ -564,13 +565,13 @@ namespace Barotrauma if (!sub.RequiredContentPackagesInstalled) { textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); - textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.RawToolTip; + textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.ToolTip.SanitizedString; } 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: GUI.SmallFont) + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { - TextColor = sub.Price > CampaignMode.InitialMoney ? GUI.Style.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignMode.InitialMoney ? GUIStyle.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; #if !DEBUG @@ -629,7 +630,7 @@ namespace Barotrauma { new GUIMessageBox( TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", new string[] { "[folder]", "[errormessage]" }, new string[] { SaveUtil.SaveFolder, e.Message })); + TextManager.GetWithVariables("showinfoldererror", ("[folder]", SaveUtil.SaveFolder), ("[errormessage]", e.Message))); } return true; } @@ -653,14 +654,14 @@ namespace Barotrauma bool isCompatible = true; prevSaveFiles ??= new List(); - + nameText.Text = Path.GetFileNameWithoutExtension(saveFile); XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); if (doc?.Root == null) { DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; continue; } if (doc.Root.GetChildElement("multiplayercampaign") != null) @@ -672,7 +673,7 @@ namespace Barotrauma subName = doc.Root.GetAttributeString("submarine", ""); saveTime = doc.Root.GetAttributeString("savetime", ""); isCompatible = SaveUtil.IsSaveFileCompatible(doc); - contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); + contentPackageStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", ""); prevSaveFiles?.Add(saveFile); if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) { @@ -682,27 +683,27 @@ namespace Barotrauma if (!string.IsNullOrEmpty(contentPackageStr)) { List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out LocalizedString errorMsg)) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); } } if (!isCompatible) { - nameText.TextColor = GUI.Style.Red; + nameText.TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: subName, font: GUI.SmallFont) + text: subName, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTime, textAlignment: Alignment.Right, font: GUI.SmallFont) + text: saveTime, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) { CanBeFocused = false, UserData = fileName @@ -782,8 +783,8 @@ namespace Barotrauma var titleText = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.2f), saveFileFrame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0, 0.05f) - }, - Path.GetFileNameWithoutExtension(fileName), font: GUI.LargeFont, textAlignment: Alignment.Center); + }, + Path.GetFileNameWithoutExtension(fileName), font: GUIStyle.LargeFont, textAlignment: Alignment.Center); titleText.Text = ToolBox.LimitString(titleText.Text, titleText.Font, titleText.Rect.Width); var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), saveFileFrame.RectTransform, Anchor.Center) @@ -791,9 +792,9 @@ namespace Barotrauma RelativeOffset = new Vector2(0, 0.1f) }); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUI.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUI.SmallFont); - new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUI.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("Submarine")} : {subName}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("LastSaved")} : {saveTime}", font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1, 0), layoutGroup.RectTransform), $"{TextManager.Get("MapSeed")} : {mapseed}", font: GUIStyle.SmallFont); new GUIButton(new RectTransform(new Vector2(0.4f, 0.15f), saveFileFrame.RectTransform, Anchor.BottomCenter) { @@ -812,8 +813,8 @@ namespace Barotrauma string saveFile = obj as string; if (obj == null) { return false; } - string header = TextManager.Get("deletedialoglabel"); - string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + LocalizedString header = TextManager.Get("deletedialoglabel"); + LocalizedString body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); EventEditorScreen.AskForConfirmation(header, body, () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index e8bf31049..7de7bbc32 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -115,7 +115,7 @@ namespace Barotrauma RelativeSpacing = 0.05f, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), repairContent.RectTransform), "", font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), repairContent.RectTransform), "", font: GUIStyle.LargeFont) { TextGetter = GetMoney }; @@ -132,11 +132,11 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - var repairHullsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUI.SubHeadingFont) + var repairHullsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), CampaignMode.HullRepairCost.ToString(), textAlignment: Alignment.Right, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), CampaignMode.HullRepairCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); repairHullsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairHullsHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("Repair")) { OnClicked = (btn, userdata) => @@ -178,11 +178,11 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - var repairItemsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUI.SubHeadingFont) + var repairItemsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), CampaignMode.ItemRepairCost.ToString(), textAlignment: Alignment.Right, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), CampaignMode.ItemRepairCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); repairItemsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), repairItemsHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("Repair")) { OnClicked = (btn, userdata) => @@ -224,11 +224,11 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - var replaceShuttlesLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUI.SubHeadingFont) + var replaceShuttlesLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont) { - ForceUpperCase = true + ForceUpperCase = ForceUpperCase.Yes }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), CampaignMode.ShuttleReplaceCost.ToString(), textAlignment: Alignment.Right, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), CampaignMode.ShuttleReplaceCost.ToString(), textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); replaceShuttlesButton = new GUIButton(new RectTransform(new Vector2(0.4f, 0.3f), replaceShuttlesHolder.RectTransform) { MinSize = new Point(140, 0) }, TextManager.Get("ReplaceShuttles")) { OnClicked = (btn, userdata) => @@ -404,11 +404,11 @@ namespace Barotrauma RelativeSpacing = 0.02f, }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUI.LargeFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Name, font: GUIStyle.LargeFont) { AutoScaleHorizontal = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont); Sprite portrait = location.Type.GetPortrait(location.PortraitId); portrait.EnsureLazyLoaded(); @@ -429,11 +429,11 @@ namespace Barotrauma if (connection?.LevelData != null) { var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - TextManager.Get("Biome", fallBackTag: "location"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), connection.Biome.DisplayName, textAlignment: Alignment.CenterRight); var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), - TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); if (connection.LevelData.HasBeaconStation) @@ -448,7 +448,7 @@ namespace Barotrauma ToolTip = TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip") }; new GUITextBlock(new RectTransform(Vector2.One, beaconStationContent.RectTransform), - TextManager.Get("submarinetype.beaconstation", fallBackTag: "beaconstationsonarlabel"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + TextManager.Get("submarinetype.beaconstation", "beaconstationsonarlabel"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { Padding = Vector4.Zero, ToolTip = icon.ToolTip @@ -465,7 +465,7 @@ namespace Barotrauma ToolTip = TextManager.Get("HuntingGroundsTooltip") }; new GUITextBlock(new RectTransform(Vector2.One, huntingGroundsContent.RectTransform), - TextManager.Get("missionname.huntinggrounds"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + TextManager.Get("missionname.huntinggrounds"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) { Padding = Vector4.Zero, ToolTip = icon.ToolTip @@ -513,7 +513,7 @@ namespace Barotrauma AbsoluteSpacing = GUI.IntScale(5) }; - var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUI.SubHeadingFont, wrap: true); + var missionName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission?.Name ?? TextManager.Get("NoMission"), font: GUIStyle.SubHeadingFont, wrap: true); // missionName.RectTransform.MinSize = new Point(0, (int)(missionName.Rect.Height * 1.5f)); if (mission != null) { @@ -541,7 +541,7 @@ namespace Barotrauma foreach (GUITextBlock rewardText in missionRewardTexts) { Mission otherMission = rewardText.UserData as Mission; - rewardText.SetRichText(otherMission.GetMissionRewardText(Submarine.MainSub)); + rewardText.Text = otherMission.GetMissionRewardText(Submarine.MainSub); } UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); @@ -586,17 +586,17 @@ namespace Barotrauma //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform) { MinSize = new Point(0, GUI.IntScale(10)) }, style: null); - - var rewardText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.GetMissionRewardText(Submarine.MainSub), wrap: true, parseRichText: true) + + var rewardText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.GetMissionRewardText(Submarine.MainSub)), wrap: true) { UserData = mission }; missionRewardTexts.Add(rewardText); - string reputationText = mission.GetReputationRewardText(mission.Locations[0]); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true, parseRichText: true); + LocalizedString reputationText = mission.GetReputationRewardText(mission.Locations[0]); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(reputationText), wrap: true); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true, parseRichText: true); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(mission.Description), wrap: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height + missionTextContent.AbsoluteSpacing) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(0)); foreach (GUIComponent child in missionTextContent.Children) @@ -636,7 +636,7 @@ namespace Barotrauma var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), isHorizontal: true); - new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), buttonArea.RectTransform), "", font: GUI.Style.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), buttonArea.RectTransform), "", font: GUIStyle.SubHeadingFont) { TextGetter = () => { @@ -652,7 +652,7 @@ namespace Barotrauma if (missionList.Content.FindChild(c => c is GUITickBox tickBox && tickBox.Selected, recursive: true) == null && missionList.Content.Children.Any(c => c.UserData is Mission)) { - var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => { StartRound?.Invoke(); @@ -740,7 +740,7 @@ namespace Barotrauma } } - public static string GetMoney() + public static LocalizedString GetMoney() { return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Money)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 41b8e927c..1556924c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -16,7 +16,7 @@ using Barotrauma.IO; namespace Barotrauma.CharacterEditor { - class CharacterEditorScreen : Screen + class CharacterEditorScreen : EditorScreen { public static CharacterEditorScreen Instance { get; private set; } @@ -142,14 +142,14 @@ namespace Barotrauma.CharacterEditor Submarine.MainSub.GodMode = true; if (Character.Controlled == null) { - var humanConfig = CharacterPrefab.HumanConfigFile; - if (string.IsNullOrEmpty(humanConfig)) + var humanSpeciesName = CharacterPrefab.HumanSpeciesName; + if (humanSpeciesName.IsEmpty) { - SpawnCharacter(AllFiles.First()); + SpawnCharacter(AllSpecies.First()); } else { - SpawnCharacter(humanConfig); + SpawnCharacter(humanSpeciesName); } } else @@ -162,7 +162,7 @@ namespace Barotrauma.CharacterEditor GameMain.Instance.ResolutionChanged += OnResolutionChanged; Instance = this; - if (!GameMain.Config.EditorDisclaimerShown) + if (!GameSettings.CurrentConfig.EditorDisclaimerShown) { GameMain.Instance.ShowEditorDisclaimer(); } @@ -199,7 +199,7 @@ namespace Barotrauma.CharacterEditor jointEndLimb = null; anchor1Pos = null; jointStartLimb = null; - allFiles = null; + allSpecies = null; onlyShowSourceRectForSelectedLimbs = false; unrestrictSpritesheet = false; editedCharacters.Clear(); @@ -246,8 +246,8 @@ namespace Barotrauma.CharacterEditor public override void Deselect() { base.Deselect(); - SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); + SoundPlayer.OverrideMusicType = Identifier.Empty; + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); GUI.ForceMouseOn(null); if (isEndlessRunner) { @@ -263,7 +263,7 @@ namespace Barotrauma.CharacterEditor else { #if !DEBUG - Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.ConfigPath))); + Reset(Character.CharacterList.Where(c => VanillaCharacters.Any(vchar => vchar == c.Prefab.ContentFile))); #endif } GameMain.Instance.ResolutionChanged -= OnResolutionChanged; @@ -277,7 +277,7 @@ namespace Barotrauma.CharacterEditor CreateGUI(); } - public static string GetCharacterEditorTranslation(string tag) + public static LocalizedString GetCharacterEditorTranslation(string tag) { return TextManager.Get(screenTextTag + tag); } @@ -815,7 +815,7 @@ namespace Barotrauma.CharacterEditor { if (!limb.Hide) { - limb.body.DebugDraw(spriteBatch, GUI.Style.Green, forceColor: true); + limb.body.DebugDraw(spriteBatch, GUIStyle.Green, forceColor: true); } } } @@ -861,7 +861,7 @@ namespace Barotrauma.CharacterEditor var mouthPos = character.AnimController.GetMouthPosition(); if (mouthPos.HasValue) { - ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUI.Style.Red, size: 8); + ShapeExtensions.DrawPoint(spriteBatch, SimToScreen(mouthPos.Value), GUIStyle.Red, size: 8); } } if (showSpritesheet) @@ -877,11 +877,11 @@ namespace Barotrauma.CharacterEditor var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 240, GameMain.GraphicsHeight / 4); if (jointCreationMode == JointCreationMode.Select) { - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectAnchor1Pos"), Color.Yellow, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectAnchor1Pos"), Color.Yellow, font: GUIStyle.LargeFont); } else { - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectLimbToConnect"), Color.Yellow, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("SelectLimbToConnect"), Color.Yellow, font: GUIStyle.LargeFont); } if (jointStartLimb != null && jointStartLimb.ActiveSprite != null) { @@ -890,8 +890,8 @@ namespace Barotrauma.CharacterEditor } if (jointEndLimb != null && jointEndLimb.ActiveSprite != null) { - GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointEndLimb), GUI.Style.Green, thickness: 3); - GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointEndLimb), GUI.Style.Green, thickness: 3); + GUI.DrawRectangle(spriteBatch, GetLimbSpritesheetRect(jointEndLimb), GUIStyle.Green, thickness: 3); + GUI.DrawRectangle(spriteBatch, GetLimbPhysicRect(jointEndLimb), GUIStyle.Green, thickness: 3); } if (spriteSheetRect.Contains(PlayerInput.MousePosition)) { @@ -901,7 +901,7 @@ namespace Barotrauma.CharacterEditor var offset = anchor1Pos ?? Vector2.Zero; offset = -offset; startPos += offset; - GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUI.Style.Green, width: 3); + GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3); } } else @@ -911,37 +911,37 @@ namespace Barotrauma.CharacterEditor // TODO: there's something wrong here var offset = anchor1Pos.HasValue ? Vector2.Transform(ConvertUnits.ToSimUnits(anchor1Pos.Value), Matrix.CreateRotationZ(jointStartLimb.Rotation)) : Vector2.Zero; var startPos = SimToScreen(jointStartLimb.SimPosition + offset); - GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUI.Style.Green, width: 3); + GUI.DrawLine(spriteBatch, startPos, PlayerInput.MousePosition, GUIStyle.Green, width: 3); } } } if (isDrawingLimb) { var textPos = new Vector2(GameMain.GraphicsWidth / 2 - 200, GameMain.GraphicsHeight / 4); - GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("DrawLimbOnSpritesheet"), Color.Yellow, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, GetCharacterEditorTranslation("DrawLimbOnSpritesheet"), Color.Yellow, font: GUIStyle.LargeFont); } if (isEndlessRunner) { Structure wall = CurrentWall.walls.FirstOrDefault(); Vector2 indicatorPos = wall == null ? originalWall.walls.First().DrawPosition : wall.DrawPosition; - GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUI.SubmarineIcon, Color.White); + GUI.DrawIndicator(spriteBatch, indicatorPos, Cam, 700, GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.White); } GUI.Draw(Cam, spriteBatch); if (isFrozen) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 40, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 40, 200), GetCharacterEditorTranslation("Frozen"), Color.Blue, Color.White * 0.5f, 10, GUIStyle.LargeFont); } if (animTestPoseToggle.Selected) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.White, Color.Black * 0.5f, 10, GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 300), GetCharacterEditorTranslation("AnimationTestPoseEnabled"), Color.White, Color.Black * 0.5f, 10, GUIStyle.LargeFont); } if (selectedJoints.Count == 1) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedJoints.First().Params.Name}", Color.White, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedJoints.First().Params.Name}", Color.White, font: GUIStyle.LargeFont); } if (selectedLimbs.Count == 1) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedLimbs.First().Params.Name}", Color.White, font: GUI.LargeFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"{GetCharacterEditorTranslation("Selected")}: {selectedLimbs.First().Params.Name}", Color.White, font: GUIStyle.LargeFont); } if (showSpritesheet) { @@ -958,23 +958,23 @@ namespace Barotrauma.CharacterEditor { var topLeft = spriteSheetControls.RectTransform.TopLeft; bool useSpritesheetOrientation = float.IsNaN(lastLimb.Params.SpriteOrientation); - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation") + ":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteOrientation") + ":", useSpritesheetOrientation ? Color.White : Color.Yellow, Color.Gray * 0.5f, 10, GUIStyle.Font); float orientation = useSpritesheetOrientation ? RagdollParams.SpritesheetOrientation : lastLimb.Params.SpriteOrientation; DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), orientation, string.Empty, useSpritesheetOrientation ? Color.White : Color.Yellow, angle => { - TryUpdateSubParam(lastLimb.Params, "spriteorientation", angle); - selectedLimbs.ForEach(l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); + TryUpdateSubParam(lastLimb.Params, "spriteorientation".ToIdentifier(), angle); + selectedLimbs.ForEach(l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), angle)); if (limbPairEditing) { - UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); + UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), angle)); } }, circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } else { var topLeft = spriteSheetControls.RectTransform.TopLeft; - GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUIStyle.Font); DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 610 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: 0, autoFreeze: false, rounding: 10); } @@ -990,21 +990,21 @@ namespace Barotrauma.CharacterEditor GUI.DrawLine(spriteBatch, limbDrawPos + Vector2.UnitX * 5.0f, limbDrawPos - Vector2.UnitX * 5.0f, Color.White); } - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 0), $"Cursor World Pos: {character.CursorWorldPosition}", Color.White, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"Cursor Pos: {character.CursorPosition}", Color.White, font: GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 40), $"Cursor Screen Pos: {PlayerInput.MousePosition}", Color.White, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 0), $"Cursor World Pos: {character.CursorWorldPosition}", Color.White, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 20), $"Cursor Pos: {character.CursorPosition}", Color.White, font: GUIStyle.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, 40), $"Cursor Screen Pos: {PlayerInput.MousePosition}", Color.White, font: GUIStyle.SmallFont); // Collider var collider = character.AnimController.Collider; var colliderDrawPos = SimToScreen(collider.SimPosition); Vector2 forward = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(collider.Rotation)); var endPos = SimToScreen(collider.SimPosition + forward * collider.radius); - GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUI.Style.Green); + GUI.DrawLine(spriteBatch, colliderDrawPos, endPos, GUIStyle.Green); GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + forward * 0.25f), Color.Blue); Vector2 left = forward.Left(); - GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + left * 0.25f), GUI.Style.Red); - ShapeExtensions.DrawCircle(spriteBatch, colliderDrawPos, (endPos - colliderDrawPos).Length(), 40, GUI.Style.Green); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 300, 0), $"Collider rotation: {MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(collider.Rotation))}", Color.White, font: GUI.SmallFont); + GUI.DrawLine(spriteBatch, colliderDrawPos, SimToScreen(collider.SimPosition + left * 0.25f), GUIStyle.Red); + ShapeExtensions.DrawCircle(spriteBatch, colliderDrawPos, (endPos - colliderDrawPos).Length(), 40, GUIStyle.Green); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 300, 0), $"Collider rotation: {MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(collider.Rotation))}", Color.White, font: GUIStyle.SmallFont); } spriteBatch.End(); } @@ -1174,7 +1174,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("height", limb.Params.Height), new XElement("sprite", new XAttribute("texture", spriteParams.Texture), - new XAttribute("sourcerect", $"{rect.X}, {rect.Y}, {rect.Size.X}, {rect.Size.Y}"))); + new XAttribute("sourcerect", $"{rect.X}, {rect.Y}, {rect.Size.X}, {rect.Size.Y}"))).FromPackage(character.Prefab.ContentPackage); CreateLimb(newLimbElement); } @@ -1186,13 +1186,13 @@ namespace Barotrauma.CharacterEditor new XAttribute("height", sourceRect.Height * RagdollParams.TextureScale), new XElement("sprite", new XAttribute("texture", RagdollParams.Limbs.First().GetSprite().Texture), - new XAttribute("sourcerect", $"{sourceRect.X}, {sourceRect.Y}, {sourceRect.Width}, {sourceRect.Height}"))); + new XAttribute("sourcerect", $"{sourceRect.X}, {sourceRect.Y}, {sourceRect.Width}, {sourceRect.Height}"))).FromPackage(character.Prefab.ContentPackage); CreateLimb(newLimbElement); lockSpriteOriginToggle.Selected = false; recalculateColliderToggle.Selected = true; } - private void CreateLimb(XElement newElement) + private void CreateLimb(ContentXElement newElement) { var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastElement != null) @@ -1232,7 +1232,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("limb2", toLimb), new XAttribute("limb1anchor", $"{a1.X.Format(2)}, {a1.Y.Format(2)}"), new XAttribute("limb2anchor", $"{a2.X.Format(2)}, {a2.Y.Format(2)}") - ); + ).FromPackage(character.Prefab.ContentPackage); var lastJointElement = RagdollParams.MainElement.GetChildElements("joint").LastOrDefault() ?? RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastJointElement == null) { @@ -1441,65 +1441,65 @@ namespace Barotrauma.CharacterEditor #region Character spawning private int characterIndex = -1; - private string currentCharacterConfig; - private string selectedJob = null; + private Identifier currentCharacterIdentifier; + private Identifier selectedJob = Identifier.Empty; - private List allFiles; - private List AllFiles + private List allSpecies; + private List AllSpecies { get { - if (allFiles == null) + if (allSpecies == null) { #if DEBUG - allFiles = CharacterPrefab.ConfigFilePaths.OrderBy(p => p).ToList(); + allSpecies = CharacterPrefab.Prefabs.Keys.OrderBy(p => p).ToList(); #else - allFiles = CharacterPrefab.ConfigFilePaths.Where(p => !p.Contains("variant", StringComparison.OrdinalIgnoreCase)).OrderBy(p => p).ToList(); + allSpecies = CharacterPrefab.Prefabs.Keys.Where(p => !p.Contains("variant")).OrderBy(p => p).ToList(); #endif - allFiles.ForEach(f => DebugConsole.NewMessage(f, Color.White)); + allSpecies.ForEach(f => DebugConsole.NewMessage(f.Value, Color.White)); } - return allFiles; + return allSpecies; } } - private List vanillaCharacters; - private List VanillaCharacters + private List vanillaCharacters; + private List VanillaCharacters { get { if (vanillaCharacters == null) { - vanillaCharacters = GameMain.VanillaContent?.GetFilesOfType(ContentType.Character).ToList(); + vanillaCharacters = GameMain.VanillaContent.GetFiles().ToList(); } return vanillaCharacters; } } - private string GetNextConfigFile() + private Identifier GetNextCharacterIdentifier() { GetCurrentCharacterIndex(); IncreaseIndex(); - currentCharacterConfig = AllFiles[characterIndex]; - return currentCharacterConfig; + currentCharacterIdentifier = AllSpecies[characterIndex]; + return currentCharacterIdentifier; } - private string GetPreviousConfigFile() + private Identifier GetPreviousCharacterIdentifier() { GetCurrentCharacterIndex(); ReduceIndex(); - currentCharacterConfig = AllFiles[characterIndex]; - return currentCharacterConfig; + currentCharacterIdentifier = AllSpecies[characterIndex]; + return currentCharacterIdentifier; } private void GetCurrentCharacterIndex() { - characterIndex = AllFiles.IndexOf(CharacterPrefab.FindBySpeciesName(character.SpeciesName).FilePath); + characterIndex = AllSpecies.IndexOf(character.SpeciesName); } private void IncreaseIndex() { characterIndex++; - if (characterIndex > AllFiles.Count - 1) + if (characterIndex > AllSpecies.Count - 1) { characterIndex = 0; } @@ -1510,13 +1510,13 @@ namespace Barotrauma.CharacterEditor characterIndex--; if (characterIndex < 0) { - characterIndex = AllFiles.Count - 1; + characterIndex = AllSpecies.Count - 1; } } - private Character SpawnCharacter(string configFile, RagdollParams ragdoll = null) + private Character SpawnCharacter(Identifier speciesName, RagdollParams ragdoll = null) { - DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", configFile.ToString()), Color.HotPink); + DebugConsole.NewMessage(GetCharacterEditorTranslation("TryingToSpawnCharacter").Replace("[config]", speciesName.ToString()), Color.HotPink); OnPreSpawn(); bool dontFollowCursor = true; if (character != null) @@ -1530,10 +1530,10 @@ namespace Barotrauma.CharacterEditor } character = null; } - if (configFile == CharacterPrefab.HumanConfigFile && selectedJob != null) + if (speciesName == CharacterPrefab.HumanSpeciesName && !selectedJob.IsEmpty) { - var characterInfo = new CharacterInfo(configFile, jobPrefab: JobPrefab.Get(selectedJob)); - character = Character.Create(configFile, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll); + var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: JobPrefab.Prefabs[selectedJob.Value]); + character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), characterInfo, hasAi: false, ragdoll: ragdoll); character.GiveJobItems(); HideWearables(); if (displayWearables) @@ -1544,8 +1544,8 @@ namespace Barotrauma.CharacterEditor } else { - character = Character.Create(configFile, spawnPosition, ToolBox.RandomSeed(8), hasAi: false, ragdoll: ragdoll); - selectedJob = null; + character = Character.Create(speciesName, spawnPosition, ToolBox.RandomSeed(8), hasAi: false, ragdoll: ragdoll); + selectedJob = Identifier.Empty; } if (character != null) { @@ -1553,14 +1553,14 @@ namespace Barotrauma.CharacterEditor } if (character == null) { - if (currentCharacterConfig == configFile) + if (currentCharacterIdentifier == speciesName) { return null; } else { // Respawn the current character; - SpawnCharacter(currentCharacterConfig); + SpawnCharacter(currentCharacterIdentifier); } } OnPostSpawn(); @@ -1584,7 +1584,7 @@ namespace Barotrauma.CharacterEditor private void OnPostSpawn() { - currentCharacterConfig = character.ConfigPath; + currentCharacterIdentifier = character.SpeciesName; GetCurrentCharacterIndex(); character.Submarine = Submarine.MainSub; character.AnimController.forceStanding = character.AnimController.CanWalk; @@ -1662,16 +1662,16 @@ namespace Barotrauma.CharacterEditor Cam.Position = character.WorldPosition; } - public bool CreateCharacter(string name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) + public bool CreateCharacter(Identifier name, string mainFolder, bool isHumanoid, ContentPackage contentPackage, XElement ragdoll, XElement config = null, IEnumerable animations = null) { var vanilla = GameMain.VanillaContent; if (contentPackage == null) { #if DEBUG - contentPackage = GameMain.Config.AllEnabledPackages.LastOrDefault(); + contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(); #else - contentPackage = GameMain.Config.AllEnabledPackages.LastOrDefault(cp => cp != vanilla); + contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(cp => cp != vanilla); #endif } if (contentPackage == null) @@ -1683,24 +1683,24 @@ namespace Barotrauma.CharacterEditor #if !DEBUG if (vanilla != null && contentPackage == vanilla) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } #endif // Content package - if (!GameMain.Config.AllEnabledPackages.Contains(contentPackage)) + if (contentPackage is RegularPackage regular && !ContentPackageManager.EnabledPackages.Regular.Contains(regular)) { - GameMain.Config.EnableRegularPackage(contentPackage); + ContentPackageManager.EnabledPackages.EnableRegular(regular); } - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); // Config file string configFilePath = Path.Combine(mainFolder, $"{name}.xml").Replace(@"\", @"/"); - var duplicate = CharacterPrefab.ConfigFiles.FirstOrDefault(f => (f.Root.IsOverride() ? f.Root.FirstElement() : f.Root).GetAttributeString("speciesname", string.Empty).Equals(name, StringComparison.OrdinalIgnoreCase)); + var duplicate = CharacterPrefab.ConfigElements.FirstOrDefault(e => e.GetAttributeIdentifier("speciesname", Identifier.Empty) == name); XElement overrideElement = null; if (duplicate != null) { - allFiles = null; + allSpecies = null; if (!File.Exists(configFilePath)) { // If the file exists, we just want to overwrite it. @@ -1760,20 +1760,22 @@ namespace Barotrauma.CharacterEditor config = overrideElement; } XDocument doc = new XDocument(config); - if (!Directory.Exists(mainFolder)) - { - Directory.CreateDirectory(mainFolder); - } + + ContentPath configFileContentPath = ContentPath.FromRaw(contentPackage, configFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(configFileContentPath.Value)); #if DEBUG - doc.Save(configFilePath); + doc.Save(configFileContentPath.Value); #else - doc.SaveSafe(configFilePath); + doc.SaveSafe(configFileContentPath.Value); #endif // Add to the selected content package - contentPackage.AddFile(configFilePath, ContentType.Character); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - contentPackage.Save(contentPackage.Path); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + var modProject = new ModProject(contentPackage); + var newFile = ModProject.File.FromPath(configFilePath); + modProject.AddFile(newFile); + + modProject.Save(contentPackage.Path); + contentPackage = ContentPackageManager.ReloadContentPackage(contentPackage); + DebugConsole.NewMessage(GetCharacterEditorTranslation("ContentPackageSaved").Replace("[path]", contentPackage.Path)); // Ragdoll @@ -1828,11 +1830,11 @@ namespace Barotrauma.CharacterEditor AnimationParams.Create(fullPath, name, animType, type); } } - if (!AllFiles.Contains(configFilePath)) + if (!AllSpecies.Contains(name)) { - AllFiles.Add(configFilePath); + AllSpecies.Add(name); } - SpawnCharacter(configFilePath, ragdollParams); + SpawnCharacter(name, ragdollParams); limbPairEditing = false; limbsToggle.Selected = true; recalculateColliderToggle.Selected = true; @@ -1976,7 +1978,7 @@ namespace Barotrauma.CharacterEditor AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("MinorModesTitle"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("MinorModesTitle"), font: GUIStyle.LargeFont); paramsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ShowParameters")) { Selected = showParamsEditor }; paramsToggle.OnSelected = box => { @@ -2034,7 +2036,7 @@ namespace Barotrauma.CharacterEditor AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("ModesPanel"), font: GUIStyle.LargeFont); characterInfoToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditCharacter")) { Selected = editCharacterInfo }; ragdollToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditRagdoll")) { Selected = editRagdoll }; limbsToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("EditLimbs")) { Selected = editLimbs }; @@ -2126,11 +2128,11 @@ namespace Barotrauma.CharacterEditor { if (value) { - toggle.Box.Flash(GUI.Style.Green, useRectangleFlash: true); + toggle.Box.Flash(GUIStyle.Green, useRectangleFlash: true); } else { - toggle.Box.Flash(GUI.Style.Red, useRectangleFlash: true); + toggle.Box.Flash(GUIStyle.Red, useRectangleFlash: true); } } toggle.Selected = value; @@ -2177,7 +2179,7 @@ namespace Barotrauma.CharacterEditor AbsoluteSpacing = 2, Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("OptionsPanel"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("OptionsPanel"), font: GUIStyle.LargeFont); freezeToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("Freeze")) { Selected = isFrozen, @@ -2283,24 +2285,24 @@ namespace Barotrauma.CharacterEditor MaxSize = new Point(100, 50) }, 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: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 255; - numberInput.Font = GUI.SmallFont; + numberInput.Font = GUIStyle.SmallFont; switch (i) { case 0: - colorLabel.TextColor = GUI.Style.Red; + colorLabel.TextColor = GUIStyle.Red; numberInput.IntValue = backgroundColor.R; numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)numInput.IntValue; break; case 1: - colorLabel.TextColor = GUI.Style.Green; + colorLabel.TextColor = GUIStyle.Green; numberInput.IntValue = backgroundColor.G; numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)numInput.IntValue; break; @@ -2403,10 +2405,10 @@ namespace Barotrauma.CharacterEditor } foreach (var limb in limbs) { - TryUpdateSubParam(limb.Params, "spriteorientation", float.NaN); + TryUpdateSubParam(limb.Params, "spriteorientation".ToIdentifier(), float.NaN); if (limbPairEditing) { - UpdateOtherLimbs(limb, l => TryUpdateSubParam(l.Params, "spriteorientation", float.NaN)); + UpdateOtherLimbs(limb, l => TryUpdateSubParam(l.Params, "spriteorientation".ToIdentifier(), float.NaN)); } } return true; @@ -2475,11 +2477,11 @@ namespace Barotrauma.CharacterEditor { ToolTip = GetCharacterEditorTranslation("CopyJointSettingsTooltip"), Selected = copyJointSettings, - TextColor = copyJointSettings ? GUI.Style.Red : Color.White, + TextColor = copyJointSettings ? GUIStyle.Red : Color.White, OnSelected = (GUITickBox box) => { copyJointSettings = box.Selected; - box.TextColor = copyJointSettings ? GUI.Style.Red : Color.White; + box.TextColor = copyJointSettings ? GUIStyle.Red : Color.White; return true; } }; @@ -2685,7 +2687,7 @@ namespace Barotrauma.CharacterEditor }; // Character selection - var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUI.LargeFont); + var characterLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), GetCharacterEditorTranslation("CharacterPanel"), font: GUIStyle.LargeFont); var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(0.2f, 0.7f), characterLabel.RectTransform, Anchor.CenterRight), style: "GUINotificationButton") { OnClicked = (btn, userdata) => { GameMain.Instance.ShowEditorDisclaimer(); return true; } @@ -2696,25 +2698,25 @@ namespace Barotrauma.CharacterEditor RelativeOffset = new Vector2(0, 0.2f) }, elementCount: 8, style: null); characterDropDown.ListBox.Color = new Color(characterDropDown.ListBox.Color.R, characterDropDown.ListBox.Color.G, characterDropDown.ListBox.Color.B, byte.MaxValue); - foreach (var file in AllFiles) + foreach (var file in AllSpecies) { - characterDropDown.AddItem(Path.GetFileNameWithoutExtension(file).CapitaliseFirstInvariant(), file); + characterDropDown.AddItem(file.Value.CapitaliseFirstInvariant(), file); } - characterDropDown.SelectItem(currentCharacterConfig); + characterDropDown.SelectItem(currentCharacterIdentifier); characterDropDown.OnSelected = (component, data) => { - string configFile = (string)data; + Identifier characterIdentifier = (Identifier)data; try { - SpawnCharacter(configFile); + SpawnCharacter(characterIdentifier); } catch (Exception e) { - HandleSpawnException(configFile, e); + HandleSpawnException(characterIdentifier, e); } return true; }; - if (currentCharacterConfig == CharacterPrefab.HumanConfigFile) + if (currentCharacterIdentifier == CharacterPrefab.HumanSpeciesName) { var jobDropDown = new GUIDropDown(new RectTransform(new Vector2(1, 0.15f), content.RectTransform) { @@ -2726,11 +2728,11 @@ namespace Barotrauma.CharacterEditor jobDropDown.SelectItem(selectedJob); jobDropDown.OnSelected = (component, data) => { - string newJob = data is string jobIdentifier ? jobIdentifier : null; + Identifier newJob = data is Identifier jobIdentifier ? jobIdentifier : Identifier.Empty; if (newJob != selectedJob) { selectedJob = newJob; - SpawnCharacter(currentCharacterConfig); + SpawnCharacter(currentCharacterIdentifier); } return true; }; @@ -2740,14 +2742,14 @@ namespace Barotrauma.CharacterEditor prevCharacterButton.TextBlock.AutoScaleHorizontal = true; prevCharacterButton.OnClicked += (b, obj) => { - string configFile = GetPreviousConfigFile(); + Identifier characterIdentifier = GetPreviousCharacterIdentifier(); try { - SpawnCharacter(configFile); + SpawnCharacter(characterIdentifier); } catch (Exception e) { - HandleSpawnException(configFile, e); + HandleSpawnException(characterIdentifier, e); } return true; }; @@ -2755,14 +2757,14 @@ namespace Barotrauma.CharacterEditor prevCharacterButton.TextBlock.AutoScaleHorizontal = true; nextCharacterButton.OnClicked += (b, obj) => { - string configFile = GetNextConfigFile(); + Identifier characterIdentifier = GetNextCharacterIdentifier(); try { - SpawnCharacter(configFile); + SpawnCharacter(characterIdentifier); } catch (Exception e) { - HandleSpawnException(configFile, e); + HandleSpawnException(characterIdentifier, e); } return true; }; @@ -2770,16 +2772,16 @@ namespace Barotrauma.CharacterEditor characterPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), characterSelectionPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); characterSelectionPanel.RectTransform.MinSize = new Point(0, (int)(content.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.2f)); - void HandleSpawnException(string configFile, Exception e) + void HandleSpawnException(Identifier characterIdentifier, Exception e) { - if (configFile != CharacterPrefab.HumanConfigFile) + if (characterIdentifier != CharacterPrefab.HumanSpeciesName) { - DebugConsole.ThrowError($"Failed to spawn the character \"{configFile}\".", e); - SpawnCharacter(CharacterPrefab.HumanConfigFile); + DebugConsole.ThrowError($"Failed to spawn the character \"{characterIdentifier}\".", e); + SpawnCharacter(CharacterPrefab.HumanSpeciesName); } else { - throw new Exception($"Failed to spawn the character \"{configFile}\".", innerException: e); + throw new Exception($"Failed to spawn the character \"{characterIdentifier}\".", innerException: e); } } } @@ -2795,18 +2797,18 @@ namespace Barotrauma.CharacterEditor Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("FileEditPanel"), font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(0.03f, 0.0f), layoutGroup.RectTransform), GetCharacterEditorTranslation("FileEditPanel"), font: GUIStyle.LargeFont); // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; var saveAllButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), TextManager.Get("editor.saveall")); - saveAllButton.Color = GUI.Style.Green; + saveAllButton.Color = GUIStyle.Green; saveAllButton.OnClicked += (button, userData) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) + if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } #endif @@ -2818,9 +2820,9 @@ namespace Barotrauma.CharacterEditor else { character.Params.Save(); - GUI.AddMessage(GetCharacterEditorTranslation("CharacterSavedTo").Replace("[path]", CharacterParams.FullPath), GUI.Style.Green, font: GUI.Font, lifeTime: 5); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterSavedTo").Replace("[path]", CharacterParams.Path.Value), GUIStyle.Green, font: GUIStyle.Font, lifeTime: 5); character.AnimController.SaveRagdoll(); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), GUI.Style.Green, font: GUI.Font, lifeTime: 5); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), GUIStyle.Green, font: GUIStyle.Font, lifeTime: 5); AnimParams.ForEach(p => p.Save()); } return true; @@ -2833,7 +2835,7 @@ namespace Barotrauma.CharacterEditor var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll")); saveRagdollButton.OnClicked += (button, userData) => { - var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); + var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveRagdoll"), $"{GetCharacterEditorTranslation("ProvideFileName")}: ", new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var inputField = new GUITextBox(new RectTransform(new Point(box.Content.Rect.Width, (int)(30 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), RagdollParams.Name.RemoveWhitespace()); box.Buttons[0].OnClicked += (b, d) => { @@ -2843,15 +2845,15 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) + if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); return false; } #endif character.AnimController.SaveRagdoll(inputField.Text); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.FullPath), Color.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollSavedTo").Replace("[path]", RagdollParams.Path.Value), Color.Green, font: GUIStyle.Font); box.Close(); return true; }; @@ -2860,7 +2862,7 @@ namespace Barotrauma.CharacterEditor var loadRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadRagdoll")); loadRagdollButton.OnClicked += (button, userData) => { - var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadRagdoll"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); + 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 deleteButton = loadBox.Buttons[2]; @@ -2873,7 +2875,7 @@ namespace Barotrauma.CharacterEditor foreach (var path in filePaths) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUI.Font, listBox.Rect.Width - 80)) + ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80)) { UserData = path, ToolTip = path @@ -2905,14 +2907,14 @@ namespace Barotrauma.CharacterEditor } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), - TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", selectedFile), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked += (b, d) => { try { File.Delete(selectedFile); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollDeletedFrom").Replace("[file]", selectedFile), GUI.Style.Red, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollDeletedFrom").Replace("[file]", selectedFile), GUIStyle.Red, font: GUIStyle.Font); } catch (Exception e) { @@ -2936,7 +2938,7 @@ namespace Barotrauma.CharacterEditor string fileName = Path.GetFileNameWithoutExtension(selectedFile); var ragdoll = character.IsHumanoid ? HumanRagdollParams.GetRagdollParams(character.SpeciesName, fileName) as RagdollParams : RagdollParams.GetRagdollParams(character.SpeciesName, fileName); ragdoll.Reset(true); - GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("RagdollLoadedFrom").Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); RecreateRagdoll(ragdoll); CreateContextualControls(); loadBox.Close(); @@ -2947,7 +2949,7 @@ namespace Barotrauma.CharacterEditor var saveAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveAnimation")); saveAnimationButton.OnClicked += (button, userData) => { - var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); + var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new LocalizedString[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null); var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform, Anchor.CenterLeft) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); var inputField = new GUITextBox(new RectTransform(new Vector2(0.45f, 1), textArea.RectTransform, Anchor.CenterRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); @@ -2978,9 +2980,9 @@ namespace Barotrauma.CharacterEditor box.Buttons[1].OnClicked += (b, d) => { #if !DEBUG - if (VanillaCharacters != null && VanillaCharacters.Contains(currentCharacterConfig)) + if (VanillaCharacters != null && VanillaCharacters.Contains(CharacterPrefab.Prefabs[currentCharacterIdentifier].ContentFile)) { - GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); box.Close(); return false; } @@ -2988,7 +2990,7 @@ namespace Barotrauma.CharacterEditor var animParams = character.AnimController.GetAnimationParamsFromType(selectedType); if (animParams == null) { return true; } animParams.Save(inputField.Text); - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.FullPath), Color.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeSavedTo").Replace("[type]", animParams.AnimationType.ToString()).Replace("[path]", animParams.Path.Value), Color.Green, font: GUIStyle.Font); ResetParamsEditor(); box.Close(); return true; @@ -2998,7 +3000,7 @@ namespace Barotrauma.CharacterEditor var loadAnimationButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("LoadAnimation")); loadAnimationButton.OnClicked += (button, userData) => { - var loadBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadAnimation"), "", new string[] { TextManager.Get("Cancel"), TextManager.Get("Load"), TextManager.Get("Delete") }, messageBoxRelSize); + 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 deleteButton = loadBox.Buttons[2]; @@ -3030,7 +3032,7 @@ namespace Barotrauma.CharacterEditor var filePaths = Directory.GetFiles(CurrentAnimation.Folder); foreach (var path in AnimationParams.FilterFilesByType(filePaths, selectedType)) { - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUI.Font, listBox.Rect.Width - 80)) + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(Path.GetFileNameWithoutExtension(path), GUIStyle.Font, listBox.Rect.Width - 80)) { UserData = path, ToolTip = path @@ -3062,18 +3064,18 @@ namespace Barotrauma.CharacterEditor } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), - TextManager.Get("DeleteDialogQuestion").Replace("[file]", selectedFile), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", selectedFile), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked += (b, d) => { try { File.Delete(selectedFile); - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeDeleted").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), GUI.Style.Red, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeDeleted").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), GUIStyle.Red, font: GUIStyle.Font); } catch (Exception e) { - DebugConsole.ThrowError(TextManager.Get("DeleteFileError").Replace("[file]", selectedFile), e); + DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", selectedFile), e); } msgBox.Close(); PopulateListBox(); @@ -3135,7 +3137,7 @@ namespace Barotrauma.CharacterEditor break; } } - GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("AnimationOfTypeLoaded").Replace("[type]", selectedType.ToString()).Replace("[file]", selectedFile), Color.WhiteSmoke, font: GUIStyle.Font); character.AnimController.AllAnimParams.ForEach(a => a.Reset(forceReload: true)); ResetParamsEditor(); loadBox.Close(); @@ -3147,7 +3149,7 @@ namespace Barotrauma.CharacterEditor // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; var resetButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("ResetButton")); - resetButton.Color = GUI.Style.Red; + resetButton.Color = GUIStyle.Red; resetButton.OnClicked += (button, userData) => { CharacterParams.Reset(true); @@ -3426,7 +3428,7 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - new GUIButton(new RectTransform(new Vector2(0.9f), parent.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton", color: GUI.Style.Red) + new GUIButton(new RectTransform(new Vector2(0.9f), parent.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton", color: GUIStyle.Red) { OnClicked = (button, data) => { @@ -3438,7 +3440,7 @@ namespace Barotrauma.CharacterEditor editor.AddCustomContent(parent, 0); } - void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, string text) + void CreateAddButtonAtLast(ParamsEditor editor, Action onButtonClicked, LocalizedString text) { if (editor == null) { return; } var parentFrame = new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, (int)(50 * GUI.yScale)), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) @@ -3456,7 +3458,7 @@ namespace Barotrauma.CharacterEditor }; } - void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, string text) + void CreateAddButton(SerializableEntityEditor editor, Action onButtonClicked, LocalizedString text) { if (editor == null) { return; } var parent = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, (int)(60 * GUI.yScale)), editor.RectTransform), style: null) @@ -3476,10 +3478,12 @@ namespace Barotrauma.CharacterEditor } } - private void TryUpdateAnimParam(string name, object value) => TryUpdateParam(character.AnimController.CurrentAnimationParams, name, value); - private void TryUpdateRagdollParam(string name, object value) => TryUpdateParam(RagdollParams, name, value); + private void TryUpdateAnimParam(string name, object value) => TryUpdateAnimParam(name.ToIdentifier(), value); + private void TryUpdateAnimParam(Identifier name, object value) => TryUpdateParam(character.AnimController.CurrentAnimationParams, name, value); + private void TryUpdateRagdollParam(string name, object value) => TryUpdateRagdollParam(name.ToIdentifier(), value); + private void TryUpdateRagdollParam(Identifier name, object value) => TryUpdateParam(RagdollParams, name, value); - private void TryUpdateParam(EditableParams editableParams, string name, object value) + private void TryUpdateParam(EditableParams editableParams, Identifier name, object value) { if (editableParams.SerializableEntityEditor == null) { @@ -3491,10 +3495,12 @@ namespace Barotrauma.CharacterEditor } } - private void TryUpdateJointParam(LimbJoint joint, string name, object value) => TryUpdateSubParam(joint.Params, name, value); - private void TryUpdateLimbParam(Limb limb, string name, object value) => TryUpdateSubParam(limb.Params, name, value); + private void TryUpdateJointParam(LimbJoint joint, string name, object value) => TryUpdateJointParam(joint, name.ToIdentifier(), value); + private void TryUpdateJointParam(LimbJoint joint, Identifier name, object value) => TryUpdateSubParam(joint.Params, name, value); + private void TryUpdateLimbParam(Limb limb, string name, object value) => TryUpdateLimbParam(limb, name.ToIdentifier(), value); + private void TryUpdateLimbParam(Limb limb, Identifier name, object value) => TryUpdateSubParam(limb.Params, name, value); - private void TryUpdateSubParam(RagdollParams.SubParam ragdollSubParams, string name, object value) + private void TryUpdateSubParam(RagdollParams.SubParam ragdollSubParams, Identifier name, object value) { if (ragdollSubParams.SerializableEntityEditor == null) { @@ -3520,7 +3526,7 @@ namespace Barotrauma.CharacterEditor } else { - DebugConsole.ThrowError(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name)); + DebugConsole.ThrowError(GetCharacterEditorTranslation("NoFieldForParameterFound").Replace("[parameter]", name.Value)); } } } @@ -3806,7 +3812,7 @@ namespace Barotrauma.CharacterEditor bool ShowCycleWidget() => PlayerInput.KeyDown(Keys.LeftAlt) && (CurrentAnimation is IHumanAnimation || CurrentAnimation is GroundedMovementParams); if (!PlayerInput.KeyDown(Keys.LeftAlt) && (animParams is IHumanAnimation || animParams is GroundedMovementParams)) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 120, 150), GetCharacterEditorTranslation("HoldLeftAltToAdjustCycleSpeed"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font); } // Widgets for all anims --> Vector2 referencePoint = SimToScreen(head != null ? head.SimPosition: collider.SimPosition); @@ -3915,7 +3921,7 @@ namespace Barotrauma.CharacterEditor DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + head.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); // Head position and leaning - Color color = GUI.Style.Red; + Color color = GUIStyle.Red; if (animParams.IsGroundedAnimation) { if (humanGroundedParams != null && character.AnimController is HumanoidAnimController humanAnimController) @@ -4193,7 +4199,7 @@ namespace Barotrauma.CharacterEditor { if (hand != null || arm != null) { - GetAnimationWidget("HandMoveAmount", GUI.Style.Green, Color.Black, initMethod: w => + GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); float offset = 0.1f; @@ -4214,7 +4220,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); + GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4333,7 +4339,7 @@ namespace Barotrauma.CharacterEditor lengthWidget.Draw(spriteBatch, deltaTime); amplitudeWidget.Draw(spriteBatch, deltaTime); // Arms - GetAnimationWidget("HandMoveAmount", GUI.Style.Green, Color.Black, initMethod: w => + GetAnimationWidget("HandMoveAmount", GUIStyle.Green, Color.Black, initMethod: w => { w.tooltip = GetCharacterEditorTranslation("HandMoveAmount"); float offset = 0.4f; @@ -4356,7 +4362,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); + GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUIStyle.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4367,7 +4373,7 @@ namespace Barotrauma.CharacterEditor if (limb.type == LimbType.LeftFoot || limb.type == LimbType.RightFoot) { GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugRefPos) - Vector2.One * 3, Vector2.One * 6, Color.White, isFilled: true); - GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugTargetPos) - Vector2.One * 3, Vector2.One * 6, GUI.Style.Green, isFilled: true); + GUI.DrawRectangle(spriteBatch, SimToScreen(limb.DebugTargetPos) - Vector2.One * 3, Vector2.One * 6, GUIStyle.Green, isFilled: true); } } } @@ -4451,7 +4457,7 @@ namespace Barotrauma.CharacterEditor if (!altDown && editJoints && selectedJoints.Any() && jointCreationMode == JointCreationMode.None) { - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 100), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUI.Font); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 180, 100), GetCharacterEditorTranslation("HoldLeftAltToManipulateJoint"), Color.White, Color.Black * 0.5f, 10, GUIStyle.Font); } foreach (Limb limb in character.AnimController.Limbs) @@ -4462,7 +4468,7 @@ namespace Barotrauma.CharacterEditor { var pullJointWidgetSize = new Vector2(5, 5); Vector2 tformedPullPos = SimToScreen(limb.PullJointWorldAnchorA); - GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUI.Style.Red, true); + GUI.DrawRectangle(spriteBatch, tformedPullPos - pullJointWidgetSize / 2, pullJointWidgetSize, GUIStyle.Red, true); DrawWidget(spriteBatch, tformedPullPos, WidgetType.Rectangle, 8, Color.Cyan, $"IK ({limb.Name})", () => { if (!selectedLimbs.Contains(limb)) @@ -4540,7 +4546,7 @@ namespace Barotrauma.CharacterEditor //GUI.DrawRectangle(spriteBatch, tformedJointPos - dotSize / 2, dotSize, color, true); //GUI.DrawLine(spriteBatch, tformedJointPos, tformedJointPos + up * 20, Color.White, width: 3); GUI.DrawLine(spriteBatch, limbScreenPos, tformedJointPos, Color.Yellow, width: 3); - //GUI.DrawRectangle(spriteBatch, inputRect, GUI.Style.Red); + //GUI.DrawRectangle(spriteBatch, inputRect, GUIStyle.Red); GUI.DrawString(spriteBatch, tformedJointPos + new Vector2(dotSize.X, -dotSize.Y) * 2, $"{joint.Params.Name} {jointPos.FormatZeroDecimal()}", Color.White, Color.Black * 0.5f); if (PlayerInput.PrimaryMouseButtonHeld()) { @@ -4723,10 +4729,10 @@ namespace Barotrauma.CharacterEditor texturePaths = new List(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.ActiveSprite == null || texturePaths.Contains(limb.ActiveSprite.FilePath)) { continue; } + if (limb.ActiveSprite == null || texturePaths.Contains(limb.ActiveSprite.FilePath.Value)) { continue; } if (limb.ActiveSprite.Texture == null) { continue; } textures.Add(limb.ActiveSprite.Texture); - texturePaths.Add(limb.ActiveSprite.FilePath); + texturePaths.Add(limb.ActiveSprite.FilePath.Value); } } @@ -4806,7 +4812,7 @@ namespace Barotrauma.CharacterEditor { if (isSelected || !onlyShowSourceRectForSelectedLimbs) { - GUI.DrawRectangle(spriteBatch, rect, isSelected ? Color.Yellow : (isMouseOn ? Color.White : GUI.Style.Red)); + GUI.DrawRectangle(spriteBatch, rect, isSelected ? Color.Yellow : (isMouseOn ? Color.White : GUIStyle.Red)); } } if (isSelected) @@ -5130,7 +5136,7 @@ namespace Barotrauma.CharacterEditor private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, bool holdPosition, float rotationOffset = 0) { bool clockWise = joint.Params.ClockWiseRotation; - Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? GUI.Style.Green * 0.5f : GUI.Style.Red; + Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? GUIStyle.Green * 0.5f : GUIStyle.Red; DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle => { joint.UpperLimit = MathHelper.ToRadians(angle); @@ -5168,7 +5174,7 @@ namespace Barotrauma.CharacterEditor } DrawAngle(20, angleColor, 4); DrawAngle(40, Color.Cyan); - GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Cyan, font: GUIStyle.SmallFont); }, circleRadius: 40, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition); DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.LowerLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("LowerLimit")}", Color.Yellow, angle => { @@ -5207,7 +5213,7 @@ namespace Barotrauma.CharacterEditor } DrawAngle(20, angleColor, 4); DrawAngle(25, Color.Yellow); - GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: Color.Yellow, font: GUIStyle.SmallFont); }, circleRadius: 25, rotationOffset: rotationOffset, displayAngle: false, clockWise: clockWise, holdPosition: holdPosition); void DrawAngle(float radius, Color color, float thickness = 5) { @@ -5314,7 +5320,7 @@ namespace Barotrauma.CharacterEditor #endregion #region Widgets as methods - private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, string toolTip, Color color, Action onClick, + private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, LocalizedString toolTip, Color color, Action onClick, float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false, int rounding = 1) { var angle = value; @@ -5338,11 +5344,11 @@ namespace Barotrauma.CharacterEditor if (angle >= 360 || angle <= -360) { angle = 0; } if (displayAngle) { - GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUI.SmallFont); + GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUIStyle.SmallFont); } onClick(angle); var zeroPos = drawPos + VectorExtensions.Forward(rotationOffset - MathHelper.PiOver2, circleRadius); - GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUI.Style.Red, width: 3); + GUI.DrawLine(spriteBatch, drawPos, zeroPos, GUIStyle.Red, width: 3); }, autoFreeze, holdPosition, onHovered: () => { if (!PlayerInput.PrimaryMouseButtonHeld()) @@ -5354,7 +5360,7 @@ namespace Barotrauma.CharacterEditor } private enum WidgetType { Rectangle, Circle } - private void DrawWidget(SpriteBatch spriteBatch, Vector2 drawPos, WidgetType widgetType, int size, Color color, string toolTip, Action onPressed, bool? autoFreeze = null, bool holdPosition = false, Action onHovered = null) + private void DrawWidget(SpriteBatch spriteBatch, Vector2 drawPos, WidgetType widgetType, int size, Color color, LocalizedString toolTip, Action onPressed, bool? autoFreeze = null, bool holdPosition = false, Action onHovered = null) { var drawRect = new Rectangle((int)drawPos.X - size / 2, (int)drawPos.Y - size / 2, size, size); var inputRect = drawRect; @@ -5501,7 +5507,7 @@ namespace Barotrauma.CharacterEditor widget.refresh = () => { widget.showTooltip = !selectedJoints.Contains(joint); - widget.color = selectedJoints.Contains(joint) ? Color.Yellow : GUI.Style.Red; + widget.color = selectedJoints.Contains(joint) ? Color.Yellow : GUIStyle.Red; }; widget.refresh(); widget.PreUpdate += dTime => widget.Enabled = editJoints; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 5c41420ac..48336c09b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -11,7 +11,7 @@ namespace Barotrauma.CharacterEditor class Wizard { // Ragdoll data - private string name; + private Identifier name; private bool isHumanoid; private bool canEnterSubmarine = true; private bool canWalk; @@ -39,7 +39,7 @@ namespace Barotrauma.CharacterEditor canEnterSubmarine = ragdoll.CanEnterSubmarine; canWalk = ragdoll.CanWalk; texturePath = ragdoll.Texture; - if (string.IsNullOrEmpty(texturePath) && !name.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(texturePath) && name != CharacterPrefab.HumanSpeciesName) { texturePath = ragdoll.Limbs.FirstOrDefault()?.GetSprite().Texture; } @@ -58,7 +58,7 @@ namespace Barotrauma.CharacterEditor } } - public static string GetCharacterEditorTranslation(string text) => CharacterEditorScreen.GetCharacterEditorTranslation(text); + public static LocalizedString GetCharacterEditorTranslation(string text) => CharacterEditorScreen.GetCharacterEditorTranslation(text); public void Reset() { @@ -97,11 +97,11 @@ namespace Barotrauma.CharacterEditor public void CreateCharacter(XElement ragdollElement, XElement characterElement = null, IEnumerable animations = null) { - if (CharacterPrefab.Find(p => p.Identifier.Equals(name, StringComparison.OrdinalIgnoreCase)) != null) + if (CharacterPrefab.Find(p => p.Identifier == name) != null) { - bool isSamePackage = contentPackage.GetFilesOfType(ContentType.Character).Any(c => Path.GetFileNameWithoutExtension(c).Equals(name, StringComparison.OrdinalIgnoreCase)); - string verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); - var msgBox = new GUIMessageBox("", verificationText, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + bool isSamePackage = contentPackage.GetFiles().Any(f => Path.GetFileNameWithoutExtension(f.Path.Value) == name); + LocalizedString verificationText = isSamePackage ? GetCharacterEditorTranslation("existingcharacterfoundreplaceverification") : GetCharacterEditorTranslation("existingcharacterfoundoverrideverification"); + var msgBox = new GUIMessageBox("", verificationText, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "verificationprompt" }; @@ -110,7 +110,7 @@ namespace Barotrauma.CharacterEditor msgBox.Close(); if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) { - GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name), GUI.Style.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name.Value), GUIStyle.Green, font: GUIStyle.Font); } Wizard.Instance.SelectTab(Tab.None); return true; @@ -126,7 +126,7 @@ namespace Barotrauma.CharacterEditor { if (CharacterEditorScreen.Instance.CreateCharacter(name, Path.GetDirectoryName(xmlPath), isHumanoid, contentPackage, ragdollElement, characterElement, animations)) { - GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name), GUI.Style.Green, font: GUI.Font); + GUI.AddMessage(GetCharacterEditorTranslation("CharacterCreated").Replace("[name]", name.Value), GUIStyle.Green, font: GUIStyle.Font); } Wizard.Instance.SelectTab(Tab.None); } @@ -141,8 +141,8 @@ namespace Barotrauma.CharacterEditor protected override GUIMessageBox Create() { - var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new string[] { TextManager.Get("Cancel"), IsCopy ? TextManager.Get("Create") : TextManager.Get("Next") }, new Vector2(0.65f, 0.9f)); - box.Header.Font = GUI.LargeFont; + var box = new GUIMessageBox(GetCharacterEditorTranslation("CreateNewCharacter"), string.Empty, new LocalizedString[] { TextManager.Get("Cancel"), IsCopy ? TextManager.Get("Create") : TextManager.Get("Next") }, new Vector2(0.65f, 0.9f)); + box.Header.Font = GUIStyle.LargeFont; box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = 20; int elementSize = 30; @@ -161,7 +161,7 @@ namespace Barotrauma.CharacterEditor void UpdatePaths() { string pathBase = ContentPackage == GameMain.VanillaContent ? $"Content/Characters/{Name}/{Name}" - : $"Mods/{(ContentPackage != null ? ContentPackage.Name + "/" : string.Empty)}Characters/{Name}/{Name}"; + : $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; XMLPath = $"{pathBase}.xml"; xmlPathElement.Text = XMLPath; if (updateTexturePath) @@ -178,12 +178,12 @@ namespace Barotrauma.CharacterEditor { case 0: new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("Name")); - var nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name ?? GetCharacterEditorTranslation("DefaultName")) { CaretColor = Color.White }; + var nameField = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), Name.Value ?? GetCharacterEditorTranslation("DefaultName").Value) { CaretColor = Color.White }; string ProcessText(string text) => text.RemoveWhitespace().CapitaliseFirstInvariant(); - Name = ProcessText(nameField.Text); + Name = ProcessText(nameField.Text).ToIdentifier(); nameField.OnTextChanged += (tb, text) => { - Name = ProcessText(text); + Name = ProcessText(text).ToIdentifier(); UpdatePaths(); return true; }; @@ -255,7 +255,7 @@ namespace Barotrauma.CharacterEditor TexturePath = text; return true; }; - string title = GetCharacterEditorTranslation("SelectTexture"); + LocalizedString title = GetCharacterEditorTranslation("SelectTexture"); new GUIButton(new RectTransform(new Vector2(0.3f / texturePathElement.RectTransform.RelativeSize.X, 1.0f), texturePathElement.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), title, style: "GUIButtonSmall") { OnClicked = (button, data) => @@ -305,12 +305,12 @@ namespace Barotrauma.CharacterEditor new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), mainElement.RectTransform, Anchor.CenterLeft), TextManager.Get("ContentPackage")); var rightContainer = new GUIFrame(new RectTransform(new Vector2(0.7f, 1), mainElement.RectTransform, Anchor.CenterRight), style: null); contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); - foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) + foreach (ContentPackage contentPackage in ContentPackageManager.EnabledPackages.All) { #if !DEBUG - if (cp == GameMain.VanillaContent) { continue; } + if (contentPackage == GameMain.VanillaContent) { continue; } #endif - contentPackageDropDown.AddItem(cp.Name, userData: cp, toolTip: cp.Path); + contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); } contentPackageDropDown.OnSelected = (obj, userdata) => { @@ -321,7 +321,7 @@ namespace Barotrauma.CharacterEditor }; contentPackageDropDown.Select(0); var contentPackageNameElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 0.5f), rightContainer.RectTransform, Anchor.BottomLeft), - GetCharacterEditorTranslation("NewContentPackage")) + GetCharacterEditorTranslation("NewContentPackage").Value) { CaretColor = Color.White, }; @@ -334,15 +334,16 @@ namespace Barotrauma.CharacterEditor contentPackageNameElement.Flash(); return false; } - if (ContentPackage.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) + if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower())) { - new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", fallBackTag: "leveleditorlevelobjnametaken")); + new GUIMessageBox("", TextManager.Get("charactereditor.contentpackagenameinuse", "leveleditorlevelobjnametaken")); return false; } - string modName = ToolBox.RemoveInvalidFileNameChars(contentPackageNameElement.Text); - ContentPackage = ContentPackage.CreatePackage(contentPackageNameElement.Text, Path.Combine("Mods", modName, Steam.SteamManager.MetadataFileName), false); - ContentPackage.AddPackage(ContentPackage); - GameMain.Config.EnableRegularPackage(ContentPackage); + string modName = contentPackageNameElement.Text; + + var modProject = new ModProject { Name = modName }; + ContentPackage = ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); + contentPackageDropDown.AddItem(ContentPackage.Name, ContentPackage, ContentPackage.Path); contentPackageDropDown.SelectItem(ContentPackage); contentPackageNameElement.Text = ""; @@ -389,15 +390,15 @@ namespace Barotrauma.CharacterEditor } if (!File.Exists(TexturePath)) { - GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUI.Style.Red); - texturePathElement.Flash(GUI.Style.Red); + GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red); + texturePathElement.Flash(GUIStyle.Red); return false; } var path = Path.GetFileName(TexturePath); if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { - GUI.AddMessage(TextManager.Get("WrongFileType"), GUI.Style.Red); - texturePathElement.Flash(GUI.Style.Red); + GUI.AddMessage(TextManager.Get("WrongFileType"), GUIStyle.Red); + texturePathElement.Flash(GUIStyle.Red); return false; } if (IsCopy) @@ -427,8 +428,8 @@ namespace Barotrauma.CharacterEditor protected override GUIMessageBox Create() { - var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new string[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.65f, 1f)); - box.Header.Font = GUI.LargeFont; + var box = new GUIMessageBox(GetCharacterEditorTranslation("DefineRagdoll"), string.Empty, new LocalizedString[] { TextManager.Get("Previous"), TextManager.Get("Create") }, new Vector2(0.65f, 1f)); + box.Header.Font = GUIStyle.LargeFont; box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = (int)(20 * GUI.Scale); int elementSize = (int)(40 * GUI.Scale); @@ -453,7 +454,7 @@ namespace Barotrauma.CharacterEditor Stretch = true, RelativeSpacing = 0.02f }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), limbEditLayout.RectTransform), GetCharacterEditorTranslation("Limbs"), font: GUI.SubHeadingFont); + 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 removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { @@ -494,10 +495,10 @@ namespace Barotrauma.CharacterEditor for (int i = 3; i >= 0; i--) { 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: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; switch (i) { @@ -623,7 +624,7 @@ namespace Barotrauma.CharacterEditor // Joints new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; var jointsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), GetCharacterEditorTranslation("Joints"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), GetCharacterEditorTranslation("Joints"), font: GUIStyle.SubHeadingFont); var jointButtonElement = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), jointsElement.RectTransform) { RelativeOffset = new Vector2(0.15f, 0) @@ -658,12 +659,12 @@ namespace Barotrauma.CharacterEditor { if (htmlBox == null) { - htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new string[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); - htmlBox.Header.Font = GUI.LargeFont; + htmlBox = new GUIMessageBox(GetCharacterEditorTranslation("LoadHTML"), string.Empty, new LocalizedString[] { TextManager.Get("Close"), TextManager.Get("Load") }, new Vector2(0.65f, 1f)); + htmlBox.Header.Font = GUIStyle.LargeFont; var element = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.05f), htmlBox.Content.RectTransform), style: null, color: Color.Gray * 0.25f); //new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), GetCharacterEditorTranslation("HTMLPath")); - var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("HTMLPath")); - string title = GetCharacterEditorTranslation("SelectFile"); + var htmlPathElement = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.TopRight), GetCharacterEditorTranslation("HTMLPath").Value); + LocalizedString title = GetCharacterEditorTranslation("SelectFile"); new GUIButton(new RectTransform(new Vector2(0.3f, 1), element.RectTransform), title) { OnClicked = (button, data) => @@ -732,14 +733,14 @@ namespace Barotrauma.CharacterEditor LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("head", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (main == null) { - GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), GUI.Style.Red); + GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), GUIStyle.Red); return false; } if (IsHumanoid) { if (!IsValid(LimbXElements.Values, true, out string missingType)) { - GUI.AddMessage(GetCharacterEditorTranslation("MissingLimbType").Replace("[limbtype]", missingType.FormatCamelCaseWithSpaces()), GUI.Style.Red); + GUI.AddMessage(GetCharacterEditorTranslation("MissingLimbType").Replace("[limbtype]", missingType.FormatCamelCaseWithSpaces()), GUIStyle.Red); return false; } } @@ -829,11 +830,11 @@ namespace Barotrauma.CharacterEditor CanBeFocused = false }; var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 16 }; - var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name, font: GUI.SubHeadingFont); + var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name, font: GUIStyle.SubHeadingFont); var idField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); - var limbTypeField = GUI.CreateEnumField(limbType, elementSize, GetCharacterEditorTranslation("LimbType"), group.RectTransform, font: GUI.Font); - var sourceRectField = GUI.CreateRectangleField(sourceRect ?? new Rectangle(0, 100 * LimbGUIElements.Count, 100, 100), elementSize, GetCharacterEditorTranslation("SourceRectangle"), group.RectTransform, font: GUI.Font); + 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) { @@ -869,7 +870,7 @@ namespace Barotrauma.CharacterEditor CanBeFocused = false }; var group = new GUILayoutGroup(new RectTransform(Vector2.One, jointElement.RectTransform)) { AbsoluteSpacing = 2 }; - var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), jointName, font: GUI.SubHeadingFont); + var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), jointName, font: GUIStyle.SubHeadingFont); var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopLeft), TextManager.Get("Name")); var nameInput = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), nameField.RectTransform, Anchor.TopRight), jointName) @@ -898,8 +899,8 @@ namespace Barotrauma.CharacterEditor MaxValueInt = byte.MaxValue, IntValue = id2 }; - GUI.CreateVector2Field(anchor1 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1"), group.RectTransform, font: GUI.Font, decimalsToDisplay: 2); - GUI.CreateVector2Field(anchor2 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2"), group.RectTransform, font: GUI.Font, decimalsToDisplay: 2); + GUI.CreateVector2Field(anchor1 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "1"), group.RectTransform, font: GUIStyle.Font, decimalsToDisplay: 2); + GUI.CreateVector2Field(anchor2 ?? Vector2.Zero, elementSize, GetCharacterEditorTranslation("LimbWithIndexAnchor").Replace("[index]", "2"), group.RectTransform, font: GUIStyle.Font, decimalsToDisplay: 2); label.Text = GetJointName(jointName); limb1InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); limb2InputField.OnValueChanged += nInput => label.Text = GetJointName(jointName); @@ -917,7 +918,7 @@ namespace Barotrauma.CharacterEditor public CharacterParams SourceCharacter => Instance.SourceCharacter; public RagdollParams SourceRagdoll => Instance.SourceRagdoll; - public string Name + public Identifier Name { get => Instance.name; set => Instance.name = value; @@ -1005,7 +1006,7 @@ namespace Barotrauma.CharacterEditor { var limbGUIElement = LimbGUIElements[i]; var allChildren = limbGUIElement.GetAllChildren(); - GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; + GUITextBlock GetField(LocalizedString n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; int id = GetField(GetCharacterEditorTranslation("ID")).Parent.GetChild().IntValue; string limbName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; LimbType limbType = (LimbType)GetField(GetCharacterEditorTranslation("LimbType")).Parent.GetChild().SelectedData; @@ -1056,7 +1057,7 @@ namespace Barotrauma.CharacterEditor { var jointGUIElement = JointGUIElements[i]; var allChildren = jointGUIElement.GetAllChildren(); - GUITextBlock GetField(string n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; + GUITextBlock GetField(LocalizedString n) => allChildren.First(c => c is GUITextBlock textBlock && textBlock.Text == n) as GUITextBlock; string jointName = GetField(TextManager.Get("Name")).Parent.GetChild().Text; int limb1ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "1")).Parent.GetChild().IntValue; int limb2ID = GetField(GetCharacterEditorTranslation("LimbWithIndex").Replace("[index]", "2")).Parent.GetChild().IntValue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs index 18ae1af3d..e191d3e54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CreditsPlayer.cs @@ -8,7 +8,7 @@ namespace Barotrauma { private GUIListBox listBox; - private XElement configElement; + private ContentXElement configElement; private float scrollSpeed; @@ -48,7 +48,7 @@ namespace Barotrauma var doc = XMLExtensions.TryLoadXml(configFile); if (doc == null) { return; } - configElement = doc.Root; + configElement = doc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); Load(); } @@ -63,7 +63,7 @@ namespace Barotrauma Spacing = spacing }; - foreach (XElement subElement in configElement.Elements()) + foreach (var subElement in configElement.Elements()) { FromXML(subElement, listBox.Content.RectTransform); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs index dab80c527..6e102be08 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs @@ -93,7 +93,7 @@ namespace Barotrauma public bool EditorMode; - private string editModeText = ""; + private LocalizedString editModeText = ""; private Vector2 textSize = Vector2.Zero; public void Save(XElement element) @@ -117,7 +117,7 @@ namespace Barotrauma { Clear(alsoPending: true); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { EditorImageContainer? tempImage = EditorImageContainer.Load(subElement); if (tempImage != null) @@ -130,7 +130,7 @@ namespace Barotrauma public void OnEditorSelected() { editModeText = TextManager.Get("SubEditor.ImageEditingMode"); - textSize = GUI.LargeFont.MeasureString(editModeText); + textSize = GUIStyle.LargeFont.MeasureString(editModeText); TryLoadPendingImages(); } @@ -267,7 +267,7 @@ namespace Barotrauma pos.Y = -pos.Y; Images.Add(new EditorImage(file, pos) { DrawTarget = EditorImage.DrawTargetType.World }); UpdateImageCategories(); - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); }; FileSelection.ClearFileTypeFilters(); @@ -287,7 +287,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2f - (textSize.X / 2f), GameMain.GraphicsHeight / 10f - (textSize.Y / 2f)); - GUI.DrawString(spriteBatch, textPos, editModeText, GUI.Style.Yellow, Color.Black * 0.4f, 8, GUI.LargeFont); + GUI.DrawString(spriteBatch, textPos, editModeText, GUIStyle.Yellow, Color.Black * 0.4f, 8, GUIStyle.LargeFont); spriteBatch.End(); } @@ -352,7 +352,7 @@ namespace Barotrauma public EditorImage(string path, Vector2 pos) { - Image = Sprite.LoadTexture(path, out Sprite _, compress: false); + Image = Sprite.LoadTexture(path, compress: false); ImagePath = path; Position = pos; UpdateRectangle(); @@ -440,7 +440,7 @@ namespace Barotrauma { widget.MouseDown += () => { - widget.color = GUI.Style.Green; + widget.color = GUIStyle.Green; prevAngle = Rotation; disableMove = true; }; @@ -493,7 +493,7 @@ namespace Barotrauma }); currentWidget.Draw(spriteBatch, (float) Timing.Step); - GUI.DrawLine(spriteBatch, Position, currentWidget.DrawPos, GUI.Style.Green, width: width); + GUI.DrawLine(spriteBatch, Position, currentWidget.DrawPos, GUIStyle.Green, width: width); } private float GetRotationAngle(Vector2 drawPosition) @@ -561,7 +561,7 @@ namespace Barotrauma width = (int) (width / cam.Zoom); } - GUI.DrawRectangle(spriteBatch, bounds, Selected ? GUI.Style.Red : GUI.Style.Green, thickness: width); + GUI.DrawRectangle(spriteBatch, bounds, Selected ? GUIStyle.Red : GUIStyle.Green, thickness: width); if (Selected) { DrawWidgets(spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 6da198254..9b68e1dbe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -4,7 +4,8 @@ namespace Barotrauma { class EditorScreen : Screen { - public static Color BackgroundColor = GameSettings.SubEditorBackgroundColor; + public static Color BackgroundColor = GameSettings.CurrentConfig.SubEditorBackground; + public override bool IsEditor => true; public void CreateBackgroundColorPicker() { @@ -17,7 +18,7 @@ namespace Barotrauma for (int i = 0; i < 3; i++) { var colorContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1), rgbLayout.RectTransform), isHorizontal: true) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.ColorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.Center); layoutParents[i] = colorContainer; } @@ -33,7 +34,9 @@ namespace Barotrauma { var color = new Color(rInput.IntValue, gInput.IntValue, bInput.IntValue); BackgroundColor = color; - GameSettings.SubEditorBackgroundColor = color; + var config = GameSettings.CurrentConfig; + config.SubEditorBackground = color; + GameSettings.SetCurrentConfig(config); }; // Reset button @@ -49,7 +52,7 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked = (button, o) => { msgBox.Close(); - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); return true; }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index 5e92d8477..3b8f0f22b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -63,10 +63,10 @@ namespace Barotrauma connectionElement.Add(new XAttribute("optiontext", connection.OptionText)); } - if (!string.IsNullOrWhiteSpace(connection.OverrideValue?.ToString())) + if (connection.OverrideValue is { } overrideValue && !string.IsNullOrWhiteSpace(connection.OverrideValue?.ToString())) { - connectionElement.Add(new XAttribute("overridevalue", connection.OverrideValue?.ToString())); - connectionElement.Add(new XAttribute("valuetype", connection.OverrideValue?.GetType().ToString())); + connectionElement.Add(new XAttribute("overridevalue", overrideValue.ToString() ?? string.Empty)); + connectionElement.Add(new XAttribute("valuetype", overrideValue.GetType().ToString())); } foreach (var nodeConnection in connection.ConnectedTo) @@ -85,7 +85,7 @@ namespace Barotrauma public void LoadConnections(XElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { int id = subElement.GetAttributeInt("i", -1); string? connectionType = subElement.GetAttributeString("type", null); @@ -170,9 +170,9 @@ namespace Barotrauma { if (connection.Type == NodeConnectionType.Value) { - if (connection.GetValue() != null) + if (connection.GetValue() is { } connValue) { - newElement.Add(new XAttribute(connection.Attribute?.ToLowerInvariant(), connection.GetValue())); + newElement.Add(new XAttribute(connection.Attribute.ToLowerInvariant(), connValue)); } } } @@ -269,8 +269,8 @@ namespace Barotrauma } } - Vector2 headerSize = GUI.SubHeadingFont.MeasureString(Name); - GUI.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); + Vector2 headerSize = GUIStyle.SubHeadingFont.MeasureString(Name); + GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); } public virtual void AddOption() @@ -445,13 +445,13 @@ namespace Barotrauma nodeValue = value; if (value is string str) { - WrappedText = TextManager.Get(str, true) is { } translated ? translated : str; + WrappedText = TextManager.Get(str) is { Loaded:true } translated ? translated.Value : str; } else { WrappedText = value?.ToString() ?? string.Empty; } - valueTextSize = GUI.SubHeadingFont.MeasureString(WrappedText); + valueTextSize = GUIStyle.SubHeadingFont.MeasureString(WrappedText); } } @@ -545,7 +545,7 @@ namespace Barotrauma width -= 16; } - valueText = ToolBox.WrapText(valueText, width, GUI.SubHeadingFont); + valueText = ToolBox.WrapText(valueText, width, GUIStyle.SubHeadingFont.Value); wrappedText = valueText; } } @@ -553,7 +553,7 @@ namespace Barotrauma public override Rectangle GetDrawRectangle() { Rectangle drawRectangle = Rectangle; - Vector2 size = GUI.SubHeadingFont.MeasureString(WrappedText); + Vector2 size = GUIStyle.SubHeadingFont.MeasureString(WrappedText ?? ""); drawRectangle.Height = (int) Math.Max(size.Y + 16, drawRectangle.Height); return drawRectangle; } @@ -564,7 +564,7 @@ namespace Barotrauma Vector2 pos = GetDrawRectangle().Location.ToVector2() + (GetDrawRectangle().Size.ToVector2() / 2) - (valueTextSize / 2); Rectangle drawRect = Rectangle; drawRect.Inflate(-1, -1); - GUI.DrawString(spriteBatch, pos, WrappedText, NodeConnection.GetPropertyColor(Type), font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, pos, WrappedText, NodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index bc639deb0..890a4b1ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -13,7 +13,7 @@ using Directory = System.IO.Directory; namespace Barotrauma { - internal class EventEditorScreen : Screen + internal class EventEditorScreen : EditorScreen { private GUIFrame GuiFrame = null!; @@ -68,7 +68,7 @@ namespace Barotrauma // === LOAD PREFAB === // GUILayoutGroup loadEventLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUIStyle.SubHeadingFont); GUILayoutGroup loadDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, loadEventLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown loadDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, loadDropdownLayout), elementCount: 10); @@ -77,7 +77,7 @@ namespace Barotrauma // === ADD ACTION === // GUILayoutGroup addActionLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addActionDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addActionLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addActionDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addActionDropdownLayout), elementCount: 10); @@ -85,7 +85,7 @@ namespace Barotrauma // === ADD VALUE === // GUILayoutGroup addValueLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addValueDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addValueLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addValueDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addValueDropdownLayout), elementCount: 7); @@ -93,15 +93,15 @@ namespace Barotrauma // === ADD SPECIAL === // GUILayoutGroup addSpecialLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup)); - new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUI.SubHeadingFont); + new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addSpecialDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addSpecialLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addSpecialDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addSpecialDropdownLayout), elementCount: 1); GUIButton addSpecialButton = new GUIButton(RectTransform(0.2f, 1.0f, addSpecialDropdownLayout), TextManager.Get("EventEditor.Add")); // Add event prefabs with identifiers to the list - foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).Distinct()) + foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs().Where(prefab => !prefab.Identifier.IsEmpty).Distinct()) { - loadDropdown.AddItem(eventPrefab.Identifier, eventPrefab); + loadDropdown.AddItem(eventPrefab.Identifier.Value!, eventPrefab); } // Add all types that inherit the EventAction class @@ -183,7 +183,7 @@ namespace Barotrauma { if (!nameInput.Text.Contains(illegalChar)) { continue; } - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } @@ -195,7 +195,7 @@ namespace Barotrauma ToolBox.OpenFileWithShell(path); return true; }); - GUI.AddMessage($"XML exported to {path}", GUI.Style.Green); + GUI.AddMessage($"XML exported to {path}", GUIStyle.Green); return true; }; } @@ -206,7 +206,7 @@ namespace Barotrauma } else { - GUI.AddMessage("Unable to export because the project contains errors", GUI.Style.Red); + GUI.AddMessage("Unable to export because the project contains errors", GUIStyle.Red); } return true; @@ -219,15 +219,15 @@ namespace Barotrauma nodeList.Clear(); markedNodes.Clear(); selectedNodes.Clear(); - projectName = TextManager.Get("EventEditor.Unnamed"); + projectName = TextManager.Get("EventEditor.Unnamed").Value; return true; }); return true; } - public static GUIMessageBox AskForConfirmation(string header, string body, Func onConfirm) + public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm) { - string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); // Cancel button @@ -274,7 +274,7 @@ namespace Barotrauma { if (!nameInput.Text.Contains(illegalChar)) { continue; } - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } @@ -283,7 +283,7 @@ namespace Barotrauma XElement save = SaveEvent(projectName); string filePath = System.IO.Path.Combine(directory, $"{projectName}.sevproj"); File.WriteAllText(Path.Combine(directory, $"{projectName}.sevproj"), save.ToString()); - GUI.AddMessage($"Project saved to {filePath}", GUI.Style.Green); + GUI.AddMessage($"Project saved to {filePath}", GUIStyle.Green); AskForConfirmation(TextManager.Get("EventEditor.TestPromptHeader"), TextManager.Get("EventEditor.TestPromptBody"), CreateTestSetupMenu); return true; @@ -342,11 +342,11 @@ namespace Barotrauma Vector2 spawnPos = Cam.WorldViewCenter; spawnPos.Y = -spawnPos.Y; - ConstructorInfo? constructor = type.GetConstructor(new Type[0]); + ConstructorInfo? constructor = type.GetConstructor(Array.Empty()); SpecialNode? newNode = null; if (constructor != null) { - newNode = constructor.Invoke(new object[0]) as SpecialNode; + newNode = constructor.Invoke(Array.Empty()) as SpecialNode; } if (newNode != null) { @@ -358,10 +358,10 @@ namespace Barotrauma return false; } - private void CreateNodes(XElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0) + private void CreateNodes(ContentXElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0) { EditorNode? lastNode = null; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { bool skip = true; switch (subElement.Name.ToString().ToLowerInvariant()) @@ -404,9 +404,9 @@ namespace Barotrauma hadNodes = false; } - XElement? parentElement = subElement.Parent; + var parentElement = subElement.Parent; - foreach (XElement xElement in subElement.Elements()) + foreach (var xElement in subElement.Elements()) { if (xElement.Name.ToString().ToLowerInvariant() == "option") { @@ -515,7 +515,7 @@ namespace Barotrauma public override void Select() { GUI.PreventPauseMenuToggle = false; - projectName = TextManager.Get("EventEditor.Unnamed"); + projectName = TextManager.Get("EventEditor.Unnamed").Value; base.Select(); } @@ -627,14 +627,14 @@ namespace Barotrauma private void Load(XElement saveElement) { nodeList.Clear(); - projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed")); + projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed").Value); foreach (XElement element in saveElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "nodes": { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { EditorNode? node = EditorNode.Load(subElement); if (node != null) @@ -647,7 +647,7 @@ namespace Barotrauma } case "allconnections": { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { int id = subElement.GetAttributeInt("i", -1); EditorNode? node = nodeList.Find(editorNode => editorNode.ID == id); @@ -702,31 +702,31 @@ namespace Barotrauma var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), msgBox.Content.RectTransform)); - new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUI.SubHeadingFont); - GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.Params.Count); - foreach (OutpostGenerationParams param in OutpostGenerationParams.Params) + new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUIStyle.SubHeadingFont); + GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.OutpostParams.Count()); + foreach (OutpostGenerationParams param in OutpostGenerationParams.OutpostParams) { - paramInput.AddItem(param.Identifier, param); + paramInput.AddItem(param.Identifier.Value!, param); } paramInput.OnSelected = (_, param) => { lastTestParam = param as OutpostGenerationParams; return true; }; - paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.Params.FirstOrDefault()); + paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.OutpostParams.FirstOrDefault()); - new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUI.SubHeadingFont); - GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.List.Count); - foreach (LocationType type in LocationType.List) + new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUIStyle.SubHeadingFont); + GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.Prefabs.Count()); + foreach (LocationType type in LocationType.Prefabs) { - typeInput.AddItem(type.Identifier, type); + typeInput.AddItem(type.Identifier.Value!, type); } typeInput.OnSelected = (_, type) => { lastTestType = type as LocationType; return true; }; - typeInput.SelectItem(lastTestType ?? LocationType.List.FirstOrDefault()); + typeInput.SelectItem(lastTestType ?? LocationType.Prefabs.FirstOrDefault()); // Cancel button msgBox.Buttons[0].OnClicked = (button, o) => @@ -783,8 +783,8 @@ namespace Barotrauma if (type.IsEnum) { Array enums = Enum.GetValues(type); - GUIDropDown valueInput = new GUIDropDown(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString(), enums.Length); - foreach (object? @enum in enums) { valueInput.AddItem(@enum?.ToString(), @enum); } + GUIDropDown valueInput = new GUIDropDown(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? "", enums.Length); + foreach (object? @enum in enums) { valueInput.AddItem(@enum?.ToString() ?? "", @enum); } valueInput.OnSelected += (component, o) => { @@ -859,22 +859,22 @@ namespace Barotrauma private bool TestEvent(OutpostGenerationParams? param, LocationType? type) { - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)) ?? throw new NullReferenceException("Could not test event: There are no shuttles available."); XElement? eventXml = ExportXML(); EventPrefab? prefab; if (eventXml != null) { - prefab = new EventPrefab(eventXml); + prefab = new EventPrefab(eventXml.FromPackage(null), null); } else { - GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUI.Style.Red); + GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUIStyle.Red); return false; } GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); - TestGameMode gameMode = (TestGameMode) gameSession.GameMode; + TestGameMode gameMode = ((TestGameMode?)gameSession.GameMode) ?? throw new InvalidCastException(); gameMode.SpawnOutpost = true; gameMode.OutpostParams = param; @@ -930,8 +930,8 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(DrawnTooltip)) { - string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUI.SmallFont); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUI.SmallFont); + string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUIStyle.SmallFont.Value); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUIStyle.SmallFont); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs index d7be315f2..d5814d94a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/NodeConnection.cs @@ -55,7 +55,14 @@ namespace Barotrauma set { optionText = value; - actualValue = WrappedValue = TextManager.Get(value, true) is { } translated ? translated : value; + if (value is string) + { + actualValue = WrappedValue = TextManager.Get(value).Fallback(value).Value; + } + else + { + actualValue = WrappedValue = value; + } } } @@ -74,7 +81,7 @@ namespace Barotrauma overrideValue = value; if (value is string str) { - actualValue = WrappedValue = TextManager.Get(str, true) is { } translated ? translated : str; + actualValue = WrappedValue = TextManager.Get(str).Fallback(str).Value; } else { @@ -96,13 +103,13 @@ namespace Barotrauma wrappedValue = null; return; } - Vector2 textSize = GUI.SmallFont.MeasureString(valueText); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(valueText); bool wasWrapped = false; while (textSize.X > 96) { wasWrapped = true; valueText = $"{valueText}...".Substring(0, valueText.Length - 4); - textSize = GUI.SmallFont.MeasureString($"{valueText}..."); + textSize = GUIStyle.SmallFont.MeasureString($"{valueText}..."); } if (wasWrapped) @@ -178,20 +185,20 @@ namespace Barotrauma Point pos = GetRenderPos(parentRectangle, yOffset); DrawRectangle = new Rectangle(pos, new Point(16, 16)); GUI.DrawRectangle(spriteBatch, DrawRectangle, bgColor, isFilled: true); - GUI.DrawRectangle(spriteBatch, DrawRectangle, EndConversation ? GUI.Style.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); + GUI.DrawRectangle(spriteBatch, DrawRectangle, EndConversation ? GUIStyle.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); string label = string.IsNullOrWhiteSpace(Attribute) ? Type.Label : Attribute; - float xPos = parentRectangle.Center.X > pos.X ? 24 : -8 - GUI.SmallFont.MeasureString(label).X; + float xPos = parentRectangle.Center.X > pos.X ? 24 : -8 - GUIStyle.SmallFont.MeasureString(label).X; if (Type != NodeConnectionType.Out) { - Vector2 size = GUI.SmallFont.MeasureString(label); + Vector2 size = GUIStyle.SmallFont.MeasureString(label); Vector2 positon = new Vector2(pos.X + xPos, pos.Y); Rectangle bgRect = new Rectangle(positon.ToPoint(), size.ToPoint()); bgRect.Inflate(4, 4); GUI.DrawRectangle(spriteBatch, bgRect, Color.Black * 0.6f, isFilled: true); - GUI.DrawString(spriteBatch, positon, label, GetPropertyColor(ValueType), font: GUI.SmallFont); + GUI.DrawString(spriteBatch, positon, label, GetPropertyColor(ValueType), font: GUIStyle.SmallFont); Vector2 mousePos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); mousePos.Y = -mousePos.Y; @@ -250,13 +257,13 @@ namespace Barotrauma { float camZoom = Screen.Selected is EventEditorScreen eventEditor ? eventEditor.Cam.Zoom : 1.0f; Rectangle valueRect = new Rectangle((int)pos.X, (int)pos.Y, 96, 20); - Vector2 textSize = GUI.SmallFont.MeasureString(text); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(text); Vector2 position = valueRect.Location.ToVector2() + valueRect.Size.ToVector2() / 2 - textSize / 2; Rectangle drawRect = valueRect; drawRect.Inflate(4, 4); GUI.DrawRectangle(spriteBatch, drawRect, new Color(50, 50, 50), isFilled: true); - GUI.DrawRectangle(spriteBatch, drawRect, EndConversation ? GUI.Style.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); - GUI.DrawString(spriteBatch, position, text, GetPropertyColor(ValueType), font: GUI.SmallFont); + GUI.DrawRectangle(spriteBatch, drawRect, EndConversation ? GUIStyle.Red : outlineColor, isFilled: false, thickness: (int)Math.Max(1, 1.25f / camZoom)); + GUI.DrawString(spriteBatch, position, text, GetPropertyColor(ValueType), font: GUIStyle.SmallFont); DrawRectangle = Rectangle.Union(DrawRectangle, drawRect); if (!string.IsNullOrWhiteSpace(fullText)) @@ -304,7 +311,7 @@ namespace Barotrauma points[2].Y -= points[2].Y - points[1].Y; } - Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUI.Style.Red; + Color drawColor = Parent is ValueNode ? GetPropertyColor(ValueType) : GUIStyle.Red; if (overrideColor != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 9a0675524..e58af763e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -11,6 +11,8 @@ namespace Barotrauma { partial class GameScreen : Screen { + public override bool IsEditor => GameMain.GameSession?.GameMode is TestGameMode; + private RenderTarget2D renderTargetBackground; private RenderTarget2D renderTarget; private RenderTarget2D renderTargetWater; @@ -142,11 +144,11 @@ namespace Barotrauma Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; - Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUI.Style.Red * 0.5f; + Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUIStyle.Red * 0.5f; GUI.DrawIndicator( spriteBatch, position, cam, Math.Max(Submarine.MainSub.Borders.Width, Submarine.MainSub.Borders.Height), - GUI.SubmarineIcon, indicatorColor); + GUIStyle.SubmarineLocationIcon.Value.Sprite, indicatorColor); } } @@ -390,14 +392,14 @@ namespace Barotrauma float BlurStrength = 0.0f; float DistortStrength = 0.0f; - Vector3 chromaticAberrationStrength = GameMain.Config.ChromaticAberrationEnabled ? + Vector3 chromaticAberrationStrength = GameSettings.CurrentConfig.Graphics.ChromaticAberration ? new Vector3(-0.02f, -0.01f, 0.0f) : Vector3.Zero; if (Character.Controlled != null) { BlurStrength = Character.Controlled.BlurStrength * 0.005f; DistortStrength = Character.Controlled.DistortStrength; - if (GameMain.Config.EnableRadialDistortion) + if (GameSettings.CurrentConfig.Graphics.RadialDistortion) { chromaticAberrationStrength -= Vector3.One * Character.Controlled.RadialDistortStrength; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 061d6e262..71bc8cab5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -16,13 +16,9 @@ using Barotrauma.IO; namespace Barotrauma { - class LevelEditorScreen : Screen + class LevelEditorScreen : EditorScreen { - private readonly Camera cam; - public override Camera Cam - { - get { return cam; } - } + public override Camera Cam { get; } private readonly GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; @@ -46,7 +42,7 @@ namespace Barotrauma public LevelEditorScreen() { - cam = new Camera() + Cam = new Camera() { MinZoom = 0.01f, MaxZoom = 1.0f @@ -69,7 +65,7 @@ namespace Barotrauma return true; }; - var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUI.SubHeadingFont); + 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.OnSelected += (GUIComponent component, object obj) => @@ -78,7 +74,7 @@ namespace Barotrauma return true; }; - var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUI.SubHeadingFont); + 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.OnSelected += (GUIComponent component, object obj) => @@ -87,7 +83,7 @@ namespace Barotrauma return true; }; - var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUI.SubHeadingFont); + 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)); @@ -185,13 +181,13 @@ namespace Barotrauma levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; Level.Generate(levelData, 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) + if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { - cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); + Cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); } foreach (GUITextBlock param in paramsList.Content.Children) { - param.TextColor = param.UserData == selectedParams ? GUI.Style.Green : param.Style.TextColor; + param.TextColor = param.UserData == selectedParams ? GUIStyle.Green : param.Style.TextColor; } seedBox.Deselect(); return true; @@ -218,14 +214,13 @@ namespace Barotrauma Submarine.MainSub.Remove(); } - //TODO: hacky workaround to check for wrecks and outposts, refactor SubmarineInfo and ContentType at some point - var nonPlayerFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); - nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost)); - nonPlayerFiles.AddRange(ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule)); - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(GameMain.Config.QuickStartSubmarineName, StringComparison.InvariantCultureIgnoreCase)); - subInfo ??= SubmarineInfo.SavedSubmarines.GetRandom(s => + 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); + subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && - !nonPlayerFiles.Any(f => f.Path.CleanUpPath().Equals(s.FilePath.CleanUpPath(), StringComparison.InvariantCultureIgnoreCase))); + !nonPlayerFiles.Any(f => f.Path == s.FilePath)); GameSession gameSession = new GameSession(subInfo, "", GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(Level.Loaded.LevelData); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => @@ -309,7 +304,7 @@ namespace Barotrauma foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, - genParams.Identifier) + genParams.Identifier.Value) { Padding = Vector4.Zero, UserData = genParams @@ -354,7 +349,7 @@ namespace Barotrauma editorContainer.ClearChildren(); outpostParamsList.Content.ClearChildren(); - foreach (OutpostGenerationParams genParams in OutpostGenerationParams.Params) + foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -373,7 +368,7 @@ namespace Barotrauma int objectsPerRow = (int)Math.Ceiling(levelObjectList.Content.Rect.Width / Math.Max(100 * GUI.Scale, 100)); float relWidth = 1.0f / objectsPerRow; - foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.Prefabs) { var frame = new GUIFrame(new RectTransform( new Vector2(relWidth, relWidth * ((float)levelObjectList.Content.Rect.Width / levelObjectList.Content.Rect.Height)), @@ -384,7 +379,7 @@ namespace Barotrauma var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: ToolBox.LimitString(levelObjPrefab.Name, GUI.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUI.SmallFont) + text: ToolBox.LimitString(levelObjPrefab.Name, GUIStyle.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false, ToolTip = levelObjPrefab.Name @@ -414,7 +409,7 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), - TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier), textAlignment: Alignment.Center); + 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) { MinValueFloat = 0, @@ -443,14 +438,14 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } + HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; + foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), - text: string.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); - foreach (string locationType in availableLocationTypes) + text: LocalizedString.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); + foreach (Identifier locationType in availableLocationTypes) { - locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) { locationTypeDropDown.SelectItem(locationType); @@ -463,7 +458,7 @@ namespace Barotrauma locationTypeDropDown.OnSelected += (_, __) => { - outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); return true; }; @@ -473,13 +468,13 @@ namespace Barotrauma // module count ------------------------- - var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUI.SubHeadingFont); + 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 (KeyValuePair 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), textAlignment: Alignment.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) { MinValueInt = 0, @@ -502,24 +497,24 @@ 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 (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + HashSet availableFlags = new HashSet(); + foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } + foreach (Identifier flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } } var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), text: TextManager.Get("leveleditor.addmoduletype")); - foreach (string flag in availableFlags) + foreach (Identifier flag in availableFlags) { - if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } - moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key == flag)) { continue; } + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); } moduleTypeDropDown.OnSelected += (_, userdata) => { - outpostGenerationParams.SetModuleCount(userdata as string, 1); + outpostGenerationParams.SetModuleCount((Identifier)userdata, 1); outpostParamsList.Select(outpostParamsList.SelectedData); return true; }; @@ -532,14 +527,12 @@ namespace Barotrauma { editorContainer.ClearChildren(); - var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUI.LargeFont); + var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUIStyle.LargeFont); if (selectedParams != null) { - List availableIdentifiers = new List(); - { - if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } - } + List availableIdentifiers = new List(); + if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } foreach (var caveParam in CaveGenerationParams.CaveParams) { if (selectedParams != null && caveParam.GetCommonness(selectedParams, abyss: false) <= 0.0f) { continue; } @@ -547,7 +540,7 @@ namespace Barotrauma } availableIdentifiers.Reverse(); - foreach (string paramsId in availableIdentifiers) + foreach (Identifier paramsId in availableIdentifiers) { var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, isHorizontal: false, childAnchor: Anchor.TopCenter) @@ -556,7 +549,7 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), - TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId), textAlignment: Alignment.Center); + 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) { MinValueFloat = 0, @@ -599,7 +592,7 @@ namespace Barotrauma } //child object editing new GUITextBlock(new RectTransform(new Point(editor.Rect.Width, 40), editorContainer.Content.RectTransform), - TextManager.Get("leveleditor.childobjects"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); + TextManager.Get("leveleditor.childobjects"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); foreach (LevelObjectPrefab.ChildObject childObj in levelObjectPrefab.ChildObjects) { var childObjFrame = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, 30))); @@ -610,7 +603,7 @@ namespace Barotrauma }; var selectedChildObj = childObj; var dropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), elementCount: 10, selectMultiple: true); - foreach (LevelObjectPrefab objPrefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab objPrefab in LevelObjectPrefab.Prefabs) { dropdown.AddItem(objPrefab.Name, objPrefab); if (childObj.AllowedNames.Contains(objPrefab.Name)) { dropdown.SelectItem(objPrefab); } @@ -669,7 +662,7 @@ namespace Barotrauma //light editing new GUITextBlock(new RectTransform(new Point(editor.Rect.Width, 40), editorContainer.Content.RectTransform), - TextManager.Get("leveleditor.lightsources"), textAlignment: Alignment.BottomCenter, font: GUI.SubHeadingFont); + TextManager.Get("leveleditor.lightsources"), textAlignment: Alignment.BottomCenter, font: GUIStyle.SubHeadingFont); foreach (LightSourceParams lightSourceParams in selectedLevelObject.LightSourceParams) { new SerializableEntityEditor(editorContainer.Content.RectTransform, lightSourceParams, inGame: false, showName: true); @@ -696,9 +689,9 @@ namespace Barotrauma { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; float commonness = levelObj.GetCommonness(selectedParams); - levelObjFrame.Color = commonness > 0.0f ? GUI.Style.Green * 0.4f : Color.Transparent; - levelObjFrame.SelectedColor = commonness > 0.0f ? GUI.Style.Green * 0.6f : Color.White * 0.5f; - levelObjFrame.HoverColor = commonness > 0.0f ? GUI.Style.Green * 0.7f : Color.White * 0.6f; + 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; levelObjFrame.GetAnyChild().Color = commonness > 0.0f ? Color.White : Color.DarkGray; if (commonness <= 0.0f) @@ -735,20 +728,20 @@ namespace Barotrauma { if (lightingEnabled.Selected) { - GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, Cam); } graphics.Clear(Color.Black); if (Level.Loaded != null) { - Level.Loaded.DrawBack(graphics, spriteBatch, cam); - Level.Loaded.DrawFront(spriteBatch, cam); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: cam.Transform); - Level.Loaded.DrawDebugOverlay(spriteBatch, cam); + Level.Loaded.DrawBack(graphics, spriteBatch, Cam); + Level.Loaded.DrawFront(spriteBatch, Cam); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: Cam.Transform); + Level.Loaded.DrawDebugOverlay(spriteBatch, Cam); Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); - GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / cam.Zoom)); + GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / Cam.Zoom)); for (int i = 0; i < Level.Loaded.Tunnels.Count; i++) { @@ -758,21 +751,21 @@ namespace Barotrauma { Vector2 start = new Vector2(tunnel.Nodes[j - 1].X, -tunnel.Nodes[j - 1].Y); Vector2 end = new Vector2(tunnel.Nodes[j].X, -tunnel.Nodes[j].Y); - GUI.DrawLine(spriteBatch, start, end, tunnelColor, width: (int)(2.0f / cam.Zoom)); + GUI.DrawLine(spriteBatch, start, end, tunnelColor, width: (int)(2.0f / Cam.Zoom)); } } foreach (Level.InterestingPosition interestingPos in Level.Loaded.PositionsOfInterest) { - if (interestingPos.Position.X < cam.WorldView.X || interestingPos.Position.X > cam.WorldView.Right || - interestingPos.Position.Y > cam.WorldView.Y || interestingPos.Position.Y < cam.WorldView.Y - cam.WorldView.Height) + if (interestingPos.Position.X < Cam.WorldView.X || interestingPos.Position.X > Cam.WorldView.Right || + interestingPos.Position.Y > Cam.WorldView.Y || interestingPos.Position.Y < Cam.WorldView.Y - Cam.WorldView.Height) { continue; } Vector2 pos = new Vector2(interestingPos.Position.X, -interestingPos.Position.Y); - spriteBatch.DrawCircle(pos, 500, 6, Color.White * 0.5f, thickness: (int)(2 / cam.Zoom)); - GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUI.LargeFont); + spriteBatch.DrawCircle(pos, 500, 6, Color.White * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUIStyle.LargeFont); } // TODO: Improve this temporary level editor debug solution (or remove it) @@ -785,17 +778,17 @@ namespace Barotrauma foreach (var resource in location.Resources) { Vector2 resourcePos = new Vector2(resource.Position.X, -resource.Position.Y); - spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / cam.Zoom)); - GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUI.LargeFont); + spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUIStyle.LargeFont); var dist = Vector2.Distance(resourcePos, pathPointPos); var lineStartPos = Vector2.Lerp(resourcePos, pathPointPos, 110 / dist); var lineEndPos = Vector2.Lerp(pathPointPos, resourcePos, 310 / dist); - GUI.DrawLine(spriteBatch, lineStartPos, lineEndPos, Color.DarkGreen * 0.5f, width: (int)(2 / cam.Zoom)); + GUI.DrawLine(spriteBatch, lineStartPos, lineEndPos, Color.DarkGreen * 0.5f, width: (int)(2 / Cam.Zoom)); } } var color = pathPoint.ShouldContainResources ? Color.DarkGreen : Color.DarkRed; - spriteBatch.DrawCircle(pathPointPos, 300, 6, color * 0.5f, thickness: (int)(2 / cam.Zoom)); - GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUI.LargeFont); + spriteBatch.DrawCircle(pathPointPos, 300, 6, color * 0.5f, thickness: (int)(2 / Cam.Zoom)); + GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUIStyle.LargeFont); } /*for (int i = 0; i < Level.Loaded.distanceField.Count; i++) @@ -833,24 +826,24 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); if (Level.Loaded != null) { - float crushDepthScreen = cam.WorldToScreen(new Vector2(0.0f, -Level.Loaded.CrushDepth)).Y; + float crushDepthScreen = Cam.WorldToScreen(new Vector2(0.0f, -Level.Loaded.CrushDepth)).Y; if (crushDepthScreen > 0.0f && crushDepthScreen < GameMain.GraphicsHeight) { - GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUI.Style.Red * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUI.Style.Red, backgroundColor: Color.Black); + GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUIStyle.Red * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUIStyle.Red, backgroundColor: Color.Black); } - float abyssStartScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; + float abyssStartScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; if (abyssStartScreen > 0.0f && abyssStartScreen < GameMain.GraphicsHeight) { - GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUI.Style.Blue * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUI.Style.Blue, backgroundColor: Color.Black); + GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUIStyle.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUIStyle.Blue, backgroundColor: Color.Black); } - float abyssEndScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; + float abyssEndScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; if (abyssEndScreen > 0.0f && abyssEndScreen < GameMain.GraphicsHeight) { - GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUI.Style.Blue * 0.25f, width: 5); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUI.Style.Blue, backgroundColor: Color.Black); + GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUIStyle.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUIStyle.Blue, backgroundColor: Color.Black); } } GUI.Draw(Cam, spriteBatch); @@ -863,17 +856,17 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - item?.GetComponent()?.Update((float)deltaTime, cam); + item?.GetComponent()?.Update((float)deltaTime, Cam); } } GameMain.LightManager?.Update((float)deltaTime); - pointerLightSource.Position = cam.ScreenToWorld(PlayerInput.MousePosition); + pointerLightSource.Position = Cam.ScreenToWorld(PlayerInput.MousePosition); pointerLightSource.Enabled = cursorLightEnabled.Selected; pointerLightSource.IsBackground = true; - cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); - cam.UpdateTransform(); - Level.Loaded?.Update((float)deltaTime, cam); + Cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); + Cam.UpdateTransform(); + Level.Loaded?.Update((float)deltaTime, Cam); if (editingSprite != null) { @@ -888,7 +881,7 @@ namespace Barotrauma Indent = true, NewLineOnAttributes = true }; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -899,7 +892,7 @@ namespace Barotrauma { if (element.IsOverride()) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -915,14 +908,14 @@ namespace Barotrauma break; } } - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); } } - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -933,7 +926,7 @@ namespace Barotrauma { if (element.IsOverride()) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string id = subElement.GetAttributeString("identifier", null) ?? subElement.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -949,7 +942,7 @@ namespace Barotrauma break; } } - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -957,22 +950,22 @@ namespace Barotrauma } settings.NewLineOnAttributes = false; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.Prefabs) { foreach (XElement element in doc.Root.Elements()) { - string identifier = element.GetAttributeString("identifier", null); - if (!identifier.Equals(levelObjPrefab.Identifier, StringComparison.OrdinalIgnoreCase)) { continue; } + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + if (identifier != levelObjPrefab.Identifier) { continue; } levelObjPrefab.Save(element); break; } } - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -984,7 +977,7 @@ namespace Barotrauma private void Serialize(LevelGenerationParams genParams) { - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -1006,7 +999,7 @@ namespace Barotrauma NewLineOnAttributes = true }; - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -1043,7 +1036,7 @@ namespace Barotrauma public GUIMessageBox Create() { var box = new GUIMessageBox(TextManager.Get("leveleditor.createlevelobj"), string.Empty, - new string[] { TextManager.Get("cancel"), TextManager.Get("done") }, new Vector2(0.5f, 0.8f)); + new LocalizedString[] { TextManager.Get("cancel"), TextManager.Get("done") }, new Vector2(0.5f, 0.8f)); box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = 20; @@ -1057,14 +1050,15 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform), TextManager.Get("leveleditor.levelobjtexturepath")) { CanBeFocused = false }; var texturePathBox = new GUITextBox(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform)); - foreach (LevelObjectPrefab prefab in LevelObjectPrefab.List) + foreach (LevelObjectPrefab prefab in LevelObjectPrefab.Prefabs) { if (prefab.Sprites.FirstOrDefault() == null) continue; - texturePathBox.Text = Path.GetDirectoryName(prefab.Sprites.FirstOrDefault().FilePath); + texturePathBox.Text = Path.GetDirectoryName(prefab.Sprites.FirstOrDefault().FilePath.Value); break; } - newPrefab = new LevelObjectPrefab(null); + //this is nasty :( + newPrefab = new LevelObjectPrefab(null, null, Identifier.Empty); new SerializableEntityEditor(listBox.Content.RectTransform, newPrefab, false, false); @@ -1078,51 +1072,62 @@ namespace Barotrauma { if (string.IsNullOrEmpty(nameBox.Text)) { - nameBox.Flash(GUI.Style.Red); - GUI.AddMessage(TextManager.Get("leveleditor.levelobjnameempty"), GUI.Style.Red); + nameBox.Flash(GUIStyle.Red); + GUI.AddMessage(TextManager.Get("leveleditor.levelobjnameempty"), GUIStyle.Red); return false; } - if (LevelObjectPrefab.List.Any(obj => obj.Identifier.Equals(nameBox.Text, StringComparison.OrdinalIgnoreCase))) + if (LevelObjectPrefab.Prefabs.Any(obj => obj.Identifier == nameBox.Text)) { - nameBox.Flash(GUI.Style.Red); - GUI.AddMessage(TextManager.Get("leveleditor.levelobjnametaken"), GUI.Style.Red); + nameBox.Flash(GUIStyle.Red); + GUI.AddMessage(TextManager.Get("leveleditor.levelobjnametaken"), GUIStyle.Red); return false; } if (!File.Exists(texturePathBox.Text)) { - texturePathBox.Flash(GUI.Style.Red); - GUI.AddMessage(TextManager.Get("leveleditor.levelobjtexturenotfound"), GUI.Style.Red); + texturePathBox.Flash(GUIStyle.Red); + GUI.AddMessage(TextManager.Get("leveleditor.levelobjtexturenotfound"), GUIStyle.Red); return false; } - newPrefab.Identifier = nameBox.Text; - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true }; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { continue; } - var newElement = new XElement(newPrefab.Identifier); - newPrefab.Save(newElement); - newElement.Add(new XElement("Sprite", - new XAttribute("texture", texturePathBox.Text), - new XAttribute("sourcerect", "0,0,100,100"), - new XAttribute("origin", "0.5,0.5"))); - doc.Root.Add(newElement); - using (var writer = XmlWriter.Create(configFile.Path, settings)) - { - doc.WriteTo(writer); - writer.Flush(); - } - // Recreate the prefab so that the sprite loads correctly: TODO: consider a better way to do this - newPrefab = new LevelObjectPrefab(newElement); - break; - } + var newElement = new XElement(nameBox.Text); + newPrefab.Save(newElement); + newElement.Add(new XElement("Sprite", + new XAttribute("texture", texturePathBox.Text), + new XAttribute("sourcerect", "0,0,100,100"), + new XAttribute("origin", "0.5,0.5"))); - LevelObjectPrefab.List.Add(newPrefab); + // Create a new mod for the purpose of providing this new prefab + #warning TODO: add a clear way to tack it into an existing content package? + string modDir = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); + Directory.CreateDirectory(modDir); + + string fileListPath = Path.Combine(modDir, ContentPackage.FileListFileName); + string prefabFilePath = Path.Combine(modDir, $"{nameBox.Text}.xml"); + + var newMod = new ModProject { Name = nameBox.Text }; + var newFile = ModProject.File.FromPath(prefabFilePath); + newMod.AddFile(newFile); + + XDocument fileListDoc = newMod.ToXDocument(); + Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)); + using (XmlWriter writer = XmlWriter.Create(fileListPath, settings)) { fileListDoc.Save(writer); } + + XDocument prefabDoc = new XDocument(); + var prefabFileRoot = new XElement("LevelObjects"); + prefabFileRoot.Add(newElement); + prefabDoc.Add(prefabFileRoot); + using (XmlWriter writer = XmlWriter.Create(prefabFilePath, settings)) { prefabDoc.Save(writer); } + + ContentPackageManager.UpdateContentPackageList(); + + var newRegularList = ContentPackageManager.EnabledPackages.Regular.ToList(); + newRegularList.Add(ContentPackageManager.RegularPackages.First(p => p.Name == nameBox.Text)); + ContentPackageManager.EnabledPackages.SetRegular(newRegularList); + GameMain.LevelEditorScreen.UpdateLevelObjectsList(); box.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 5319a5ca1..0ff737f3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -9,11 +9,13 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; +using System.Data.Common; using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Net; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.Steam; @@ -21,7 +23,7 @@ namespace Barotrauma { class MainMenuScreen : Screen { - public enum Tab + private enum Tab { NewGame = 0, LoadGame = 1, @@ -38,20 +40,20 @@ namespace Barotrauma private readonly GUIComponent buttonsParent; - private readonly GUIFrame[] menuTabs; + private readonly Dictionary menuTabs; private SinglePlayerCampaignSetupUI campaignSetupUI; - private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; + private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private readonly GUIFrame downloadingModsContainer, enableModsContainer; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; private GUIImage playstyleBanner; private GUITextBlock playstyleDescription; - private GUIComponent remoteContentContainer; + private const string RemoteContentUrl = "http://www.barotraumagame.com/gamedata/"; + private readonly GUIComponent remoteContentContainer; private XDocument remoteContentDoc; private Tab selectedTab = Tab.Empty; @@ -62,28 +64,21 @@ namespace Barotrauma private readonly CreditsPlayer creditsPlayer; -#if OSX - private bool firstLoadOnMac = true; -#endif + public static readonly Queue WorkshopItemsToUpdate = new Queue(); -#region Creation + #region Creation public MainMenuScreen(GameMain game) { GameMain.Instance.ResolutionChanged += () => { - if (Selected == this && selectedTab == Tab.Settings) - { - GameMain.Config.ResetSettingsFrame(); - SelectTab(Tab.Settings); - } CreateHostServerFields(); CreateCampaignSetupUI(); if (remoteContentDoc?.Root != null) { remoteContentContainer.ClearChildren(); - foreach (XElement subElement in remoteContentDoc.Root.Elements()) + foreach (var subElement in remoteContentDoc.Root.Elements()) { - GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } }; @@ -118,9 +113,9 @@ namespace Barotrauma var doc = XMLExtensions.TryLoadXml("Content/UI/MenuContent.xml"); if (doc?.Root != null) { - foreach (XElement subElement in doc?.Root.Elements()) + foreach (var subElement in doc?.Root.Elements()) { - GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } #else @@ -144,7 +139,7 @@ namespace Barotrauma var campaignNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: campaignHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, labelHeight), campaignNavigation.RectTransform), - TextManager.Get("CampaignLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true }; + TextManager.Get("CampaignLabel"), textAlignment: Alignment.Left, font: GUIStyle.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = ForceUpperCase.Yes }; var campaignButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: campaignNavigation.RectTransform), style: "MainMenuGUIFrame"); @@ -156,7 +151,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("TutorialButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.Tutorials, OnClicked = (tb, userdata) => { @@ -167,7 +162,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("LoadGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.LoadGame, OnClicked = (tb, userdata) => { @@ -178,7 +173,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("NewGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.NewGame, OnClicked = (tb, userdata) => { @@ -201,7 +196,7 @@ namespace Barotrauma var multiplayerNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: multiplayerHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, labelHeight), multiplayerNavigation.RectTransform), - TextManager.Get("MultiplayerLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true }; + TextManager.Get("MultiplayerLabel"), textAlignment: Alignment.Left, font: GUIStyle.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = ForceUpperCase.Yes }; var multiplayerButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: multiplayerNavigation.RectTransform), style: "MainMenuGUIFrame"); @@ -213,7 +208,7 @@ namespace Barotrauma joinServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), multiplayerList.RectTransform), TextManager.Get("JoinServerButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.JoinServer, OnClicked = (tb, userdata) => { @@ -223,7 +218,7 @@ namespace Barotrauma }; hostServerButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), multiplayerList.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.HostServer, OnClicked = (tb, userdata) => { @@ -246,7 +241,7 @@ namespace Barotrauma var customizeNavigation = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.75f), parent: customizeHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.25f) }); new GUITextBlock(new RectTransform(new Vector2(1.0f, labelHeight), customizeNavigation.RectTransform), - TextManager.Get("CustomizeLabel"), textAlignment: Alignment.Left, font: GUI.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = true }; + TextManager.Get("CustomizeLabel"), textAlignment: Alignment.Left, font: GUIStyle.LargeFont, textColor: Color.Black, style: "MainMenuGUITextBlock") { ForceUpperCase = ForceUpperCase.Yes }; var customizeButtons = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), parent: customizeNavigation.RectTransform), style: "MainMenuGUIFrame"); @@ -257,36 +252,18 @@ namespace Barotrauma }; #if USE_STEAM - var steamWorkshopButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), style: null); - - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), steamWorkshopButtonContainer.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, - Enabled = false, + ForceUpperCase = ForceUpperCase.Yes, + Enabled = true, UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; - - downloadingModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), steamWorkshopButtonContainer.RectTransform, - Anchor.CenterRight, Pivot.CenterLeft) - { RelativeOffset = new Vector2(0.3f, 0.0f) }, - "MainMenuNotifBackground", Color.Yellow) - { - CanBeFocused = false, - UserData = "workshopnotif", - Visible = false - }; - new GUITextBlock(new RectTransform(Vector2.One * 0.9f, downloadingModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get("ModsDownloadingNotif"), Color.Black) - { - CanBeFocused = false, - }; - #endif - + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.SubmarineEditor, OnClicked = (tb, userdata) => { @@ -297,7 +274,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("CharacterEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.CharacterEditor, OnClicked = (tb, userdata) => { @@ -329,51 +306,37 @@ namespace Barotrauma new GUIButton(new RectTransform(Vector2.One, settingsButtonContainer.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.Settings, OnClicked = SelectTab }; - - enableModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), settingsButtonContainer.RectTransform, - Anchor.CenterRight, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.5f, 0.0f) }, - "MainMenuNotifBackground", Color.Yellow) - { - CanBeFocused = false, - UserData = "settingsnotif", - Visible = false - }; - new GUITextBlock(new RectTransform(Vector2.One * 0.9f, enableModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get("ModsInstalledNotif"), Color.Black) - { - CanBeFocused = false - }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("EditorDisclaimerWikiLink"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = (button, userData) => { - string url = TextManager.Get("EditorDisclaimerWikiUrl", returnNull: true) ?? "https://barotraumagame.com/wiki"; + string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value; GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice"); return true; } }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("CreditsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = Tab.Credits, OnClicked = SelectTab }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("QuitButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, OnClicked = QuitClicked }; //debug button for quickly starting a new round #if DEBUG new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 80) }, - "Quickstart (dev)", style: "GUIButtonLarge", color: GUI.Style.Red) + "Quickstart (dev)", style: "GUIButtonLarge", color: GUIStyle.Red) { IgnoreLayoutGroups = true, UserData = Tab.Empty, @@ -387,7 +350,7 @@ namespace Barotrauma } }; new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 130) }, - "Profiling", style: "GUIButtonLarge", color: GUI.Style.Red) + "Profiling", style: "GUIButtonLarge", color: GUIStyle.Red) { IgnoreLayoutGroups = true, UserData = Tab.Empty, @@ -404,7 +367,7 @@ namespace Barotrauma } }; new GUIButton(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 180) }, - "Join Localhost", style: "GUIButtonLarge", color: GUI.Style.Red) + "Join Localhost", style: "GUIButtonLarge", color: GUIStyle.Red) { IgnoreLayoutGroups = true, UserData = Tab.Empty, @@ -413,7 +376,7 @@ namespace Barotrauma { SelectTab(tb, userdata); - GameMain.Client = new GameClient(string.IsNullOrEmpty(GameMain.Config.PlayerName) ? SteamManager.GetUsername() : GameMain.Config.PlayerName, + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), IPAddress.Loopback.ToString(), 0, "localhost", 0, false); return true; @@ -430,18 +393,19 @@ namespace Barotrauma var pivot = Pivot.CenterRight; Vector2 relativeSpacing = new Vector2(0.05f, 0.0f); - menuTabs = new GUIFrame[Enum.GetValues(typeof(Tab)).Length + 1]; + menuTabs = new Dictionary(); - menuTabs[(int)Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, + menuTabs[Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, style: null); + menuTabs[Tab.Settings].CanBeFocused = false; - menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); - menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); CreateCampaignSetupUI(); var hostServerScale = new Vector2(0.7f, 1.2f); - menuTabs[(int)Tab.HostServer] = new GUIFrame(new RectTransform( + menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) { RelativeOffset = relativeSpacing }); @@ -449,36 +413,38 @@ namespace Barotrauma //---------------------------------------------------------------------- - menuTabs[(int)Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); //PLACEHOLDER var tutorialList = new GUIListBox( - new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[(int)Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); - foreach (Tutorial tutorial in Tutorial.Tutorials) + new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }); + var tutorialTypes = ReflectionUtils.GetDerivedNonAbstract(); + foreach (Type tutorialType in tutorialTypes) { - var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUI.LargeFont) + Tutorial tutorial = (Tutorial)Activator.CreateInstance(tutorialType); + var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { UserData = tutorial }; } tutorialList.OnSelected += (component, obj) => { - TutorialMode.StartTutorial(obj as Tutorial); + (obj as Tutorial).Start(); return true; }; this.game = game; - menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) + menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { CanBeFocused = false }; - new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[(int)Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker") { CanBeFocused = false }; - var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[(int)Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); + var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); } #endregion @@ -486,6 +452,14 @@ namespace Barotrauma #region Selection public override void Select() { + if (WorkshopItemsToUpdate.Any()) + { + while (WorkshopItemsToUpdate.TryDequeue(out ulong workshopId)) + { + SteamManager.Workshop.OnItemDownloadComplete(workshopId, forceInstall: true); + } + } + GUI.PreventPauseMenuToggle = false; base.Select(); @@ -500,30 +474,6 @@ namespace Barotrauma Submarine.Unload(); ResetButtonStates(null); - - if (GameMain.SteamWorkshopScreen != null) - { - CoroutineManager.StartCoroutine(GameMain.SteamWorkshopScreen.RefreshDownloadState()); - } - -#if OSX - // Hack for adjusting the viewport properly after splash screens on older Macs - if (firstLoadOnMac) - { - firstLoadOnMac = false; - - menuTabs[(int)Tab.Empty] = new GUIFrame(new RectTransform(new Vector2(1f, 1f), GUI.Canvas), "", Color.Transparent) - { - CanBeFocused = false - }; - var emptyList = new GUIListBox(new RectTransform(new Vector2(0.0f, 0.0f), menuTabs[(int)Tab.Empty].RectTransform)) - { - CanBeFocused = false - }; - - SelectTab(null, Tab.Empty); - } -#endif } public override void Deselect() @@ -549,44 +499,19 @@ namespace Barotrauma private bool SelectTab(Tab tab) { titleText.Visible = true; - if (GameMain.Config.UnsavedSettings) - { - var applyBox = new GUIMessageBox( - TextManager.Get("ApplySettingsLabel"), - TextManager.Get("ApplySettingsQuestion"), - new string[] { TextManager.Get("ApplySettingsYes"), TextManager.Get("ApplySettingsNo") }); - applyBox.Buttons[0].UserData = tab; - applyBox.Buttons[0].OnClicked = (tb, userdata) => - { - applyBox.Close(); - ApplySettings(); - SelectTab(tab); - return true; - }; - - applyBox.Buttons[1].UserData = tab; - applyBox.Buttons[1].OnClicked = (tb, userdata) => - { - applyBox.Close(); - DiscardSettings(); - SelectTab(tab); - return true; - }; - return false; - } - - GameMain.Config.ResetSettingsFrame(); + SettingsMenu.Instance?.Close(); + #warning TODO: reimplement settings confirmation dialog switch (tab) { case Tab.NewGame: - if (GameMain.Config.ShowTutorialSkipWarning) + if (GameSettings.CurrentConfig.TutorialSkipWarning) { selectedTab = Tab.Empty; ShowTutorialSkipWarning(Tab.NewGame); return true; } - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.NewGame); }); @@ -602,19 +527,16 @@ namespace Barotrauma campaignSetupUI.UpdateLoadMenu(); break; case Tab.Settings: - GameMain.MainMenuScreen?.SetEnableModsNotification(false); - menuTabs[(int)Tab.Settings].RectTransform.ClearChildren(); - GameMain.Config.SettingsFrame.RectTransform.Parent = menuTabs[(int)Tab.Settings].RectTransform; - GameMain.Config.SettingsFrame.RectTransform.RelativeSize = Vector2.One; + SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); break; case Tab.JoinServer: - if (GameMain.Config.ShowTutorialSkipWarning) + if (GameSettings.CurrentConfig.TutorialSkipWarning) { selectedTab = Tab.Empty; ShowTutorialSkipWarning(Tab.JoinServer); return true; } - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.JoinServer); }); @@ -623,19 +545,13 @@ namespace Barotrauma GameMain.ServerListScreen.Select(); break; case Tab.HostServer: - if (GameMain.Config.ContentPackageSelectionDirty) - { - new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("ServerRestartRequiredContentPackage", fallBackTag: "RestartRequiredGeneric")); - selectedTab = Tab.Empty; - return false; - } - if (GameMain.Config.ShowTutorialSkipWarning) + if (GameSettings.CurrentConfig.TutorialSkipWarning) { selectedTab = Tab.Empty; ShowTutorialSkipWarning(tab); return true; } - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); @@ -643,7 +559,7 @@ namespace Barotrauma } break; case Tab.Tutorials: - if (!GameMain.Config.CampaignDisclaimerShown) + if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) { selectedTab = Tab.Empty; GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.Tutorials); }); @@ -659,8 +575,9 @@ namespace Barotrauma CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SubEditorScreen)); break; case Tab.SteamWorkshop: - if (!Steam.SteamManager.IsInitialized) return false; - CoroutineManager.StartCoroutine(SelectScreenWithWaitCursor(GameMain.SteamWorkshopScreen)); + var settings = SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); + settings.SelectTab(SettingsMenu.Tab.Mods); + tab = Tab.Settings; break; case Tab.Credits: titleText.Visible = false; @@ -717,7 +634,7 @@ namespace Barotrauma } #endregion - public void QuickStart(bool fixedSeed = false, string sub = null, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) + public void QuickStart(bool fixedSeed = false, Identifier sub = default, float difficulty = 50, LevelGenerationParams levelGenerationParams = null) { if (fixedSeed) { @@ -726,11 +643,12 @@ namespace Barotrauma } SubmarineInfo selectedSub = null; - string subName = sub ?? GameMain.Config.QuickStartSubmarineName; - if (!string.IsNullOrEmpty(subName)) + Identifier subName = sub.IfEmpty(GameSettings.CurrentConfig.QuickStartSub); + if (!subName.IsEmpty) { DebugConsole.NewMessage($"Loading the predefined quick start sub \"{subName}\"", Color.White); - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLowerInvariant() == subName.ToLowerInvariant()); + + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); if (selectedSub == null) { DebugConsole.NewMessage($"Cannot find a sub that matches the name \"{subName}\".", Color.Red); @@ -755,7 +673,7 @@ namespace Barotrauma { var jobPrefab = JobPrefab.Get(job); var variant = Rand.Range(0, jobPrefab.Variants); - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant); if (characterInfo.Job == null) { DebugConsole.ThrowError("Failed to find the job \"" + job + "\"!"); @@ -765,40 +683,29 @@ namespace Barotrauma gamesession.CrewManager.InitSinglePlayerRound(); } - public void SetEnableModsNotification(bool visible) - { - if (enableModsContainer != null) { enableModsContainer.Visible = visible; } - } - - public void SetDownloadingModsNotification(bool visible) - { - if (downloadingModsContainer != null) { downloadingModsContainer.Visible = visible; } - } - private void ShowTutorialSkipWarning(Tab tabToContinueTo) { - var tutorialSkipWarning = new GUIMessageBox("", TextManager.Get("tutorialskipwarning"), new string[] { TextManager.Get("tutorialwarningskiptutorials"), TextManager.Get("tutorialwarningplaytutorials") }); - tutorialSkipWarning.Buttons[0].OnClicked += (btn, userdata) => - { - GameMain.Config.ShowTutorialSkipWarning = false; - GameMain.Config.SaveNewPlayerConfig(); - tutorialSkipWarning.Close(); - SelectTab(tabToContinueTo); - return true; - }; - tutorialSkipWarning.Buttons[1].OnClicked += (btn, userdata) => - { - GameMain.Config.ShowTutorialSkipWarning = false; - GameMain.Config.SaveNewPlayerConfig(); - tutorialSkipWarning.Close(); - SelectTab(Tab.Tutorials); - return true; - }; + var tutorialSkipWarning = new GUIMessageBox("", TextManager.Get("tutorialskipwarning"), new LocalizedString[] { TextManager.Get("tutorialwarningskiptutorials"), TextManager.Get("tutorialwarningplaytutorials") }); + + GUIButton.OnClickedHandler proceedToTab(Tab tab) + => (btn, userdata) => + { + var config = GameSettings.CurrentConfig; + config.TutorialSkipWarning = false; + GameSettings.SetCurrentConfig(config); + GameSettings.SaveCurrentConfig(); + tutorialSkipWarning.Close(); + SelectTab(tab); + return true; + }; + + tutorialSkipWarning.Buttons[0].OnClicked += proceedToTab(tabToContinueTo); + tutorialSkipWarning.Buttons[1].OnClicked += proceedToTab(Tab.Tutorials); } private void UpdateTutorialList() { - var tutorialList = menuTabs[(int)Tab.Tutorials].GetChild(); + var tutorialList = menuTabs[Tab.Tutorials].GetChild(); int completedTutorials = 0; @@ -814,7 +721,7 @@ namespace Barotrauma { if (i < completedTutorials + 1) { - (tutorialList.Content.GetChild(i) as GUITextBlock).TextColor = GUI.Style.Green; + (tutorialList.Content.GetChild(i) as GUITextBlock).TextColor = GUIStyle.Green; #if !DEBUG (tutorialList.Content.GetChild(i) as GUITextBlock).CanBeFocused = true; #endif @@ -829,37 +736,6 @@ namespace Barotrauma } } - public void ResetSettingsFrame(GameSettings.Tab selectedTab = GameSettings.Tab.Graphics) - { - menuTabs[(int)Tab.Settings].RectTransform.ClearChildren(); - GameMain.Config.ResetSettingsFrame(); - GameMain.Config.CreateSettingsFrame(selectedTab); - GameMain.Config.SettingsFrame.RectTransform.Parent = menuTabs[(int)Tab.Settings].RectTransform; - GameMain.Config.SettingsFrame.RectTransform.RelativeSize = Vector2.One; - } - - private bool ApplySettings() - { - GameMain.Config.SaveNewPlayerConfig(); - - if (GameMain.GraphicsWidth != GameMain.Config.GraphicsWidth || - GameMain.GraphicsHeight != GameMain.Config.GraphicsHeight) - { - new GUIMessageBox( - TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredGeneric")); - } - - return true; - } - - private bool DiscardSettings() - { - GameMain.Config.LoadPlayerConfig(); - - return true; - } - private bool ChangeMaxPlayers(GUIButton button, object obj) { int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); @@ -872,7 +748,7 @@ namespace Barotrauma { if (SubmarineInfo.SavedSubmarines.Any(s => s.CalculatingHash)) { - var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new string[] { TextManager.Get("cancel") }); + var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new LocalizedString[] { TextManager.Get("cancel") }); var waitCoroutine = CoroutineManager.StartCoroutine(WaitForSubmarineHashCalculations(waitBox), "WaitForSubmarineHashCalculations"); waitBox.Buttons[0].OnClicked += (btn, userdata) => { @@ -888,7 +764,7 @@ namespace Barotrauma private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) { - string originalText = messageBox.Text.Text; + LocalizedString originalText = messageBox.Text.Text; int doneCount = 0; do { @@ -905,16 +781,10 @@ namespace Barotrauma { string name = serverNameBox.Text; - GameMain.NetLobbyScreen?.Release(); - GameMain.NetLobbyScreen = new NetLobbyScreen(); + GameMain.ResetNetLobbyScreen(); try { - string exeName = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.ServerExecutable)?.FirstOrDefault()?.Path; - if (string.IsNullOrEmpty(exeName)) - { - DebugConsole.ThrowError("No server executable defined in the selected content packages. Attempting to use the default executable..."); - exeName = "DedicatedServer.exe"; - } + string exeName = "DedicatedServer.exe"; string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -962,7 +832,8 @@ namespace Barotrauma ChildServerRelay.Start(processInfo); Thread.Sleep(1000); //wait until the server is ready before connecting - GameMain.Client = new GameClient(string.IsNullOrEmpty(GameMain.Config.PlayerName) ? name : GameMain.Config.PlayerName, + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty( + SteamManager.GetUsername().FallbackNullOrEmpty(name)), System.Net.IPAddress.Loopback.ToString(), Steam.SteamManager.GetSteamID(), name, ownerKey, true); } catch (Exception e) @@ -980,9 +851,9 @@ namespace Barotrauma public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - if (selectedTab < Tab.Empty && menuTabs[(int)selectedTab] != null) + if (selectedTab < Tab.Empty && menuTabs.TryGetValue(selectedTab, out GUIFrame tab) && tab != null) { - menuTabs[(int)selectedTab].AddToGUIUpdateList(); + tab.AddToGUIUpdateList(); switch (selectedTab) { case Tab.NewGame: @@ -995,9 +866,9 @@ namespace Barotrauma public override void Update(double deltaTime) { #if !DEBUG && USE_STEAM - if (GameMain.Config.UseSteamMatchmaking) + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { - hostServerButton.Enabled = Steam.SteamManager.IsInitialized; + hostServerButton.Enabled = Steam.SteamManager.IsInitialized; } steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; #elif USE_STEAM @@ -1020,7 +891,7 @@ namespace Barotrauma #if UNSTABLE backgroundSprite = new Sprite("Content/UnstableBackground.png", sourceRectangle: null); #endif - backgroundSprite ??= LocationType.List.Where(l => l.UseInMainMenu).GetRandom()?.GetPortrait(0); + backgroundSprite ??= (LocationType.Prefabs.Where(l => l.UseInMainMenu).GetRandomUnsynced())?.GetPortrait(0); } if (backgroundSprite != null) @@ -1029,7 +900,7 @@ namespace Barotrauma aberrationStrength: 0.0f); } - var vignette = GUI.Style.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); + var vignette = GUIStyle.GetComponentStyle("mainmenuvignette")?.GetDefaultSprite(); if (vignette != null) { spriteBatch.Begin(blendState: BlendState.NonPremultiplied); @@ -1039,9 +910,9 @@ namespace Barotrauma } } - readonly string[] legalCrap = new string[] + readonly LocalizedString[] legalCrap = new LocalizedString[] { - TextManager.Get("privacypolicy", returnNull: true) ?? "Privacy policy", + TextManager.Get("privacypolicy").Fallback("Privacy policy"), "© " + DateTime.Now.Year + " Undertow Games & FakeFish. All rights reserved.", "© " + DateTime.Now.Year + " Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved." }; @@ -1054,28 +925,27 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - if (selectedTab != Tab.Credits) { #if !UNSTABLE string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"; - GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); + GUIStyle.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUIStyle.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); #endif - string gameAnalyticsStatus = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); - Vector2 textSize = GUI.SmallFont.MeasureString(gameAnalyticsStatus).ToPoint().ToVector2(); - GUI.SmallFont.DrawString(spriteBatch, gameAnalyticsStatus, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.LineHeight * 2 - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); + LocalizedString gameAnalyticsStatus = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); + Vector2 textSize = GUIStyle.SmallFont.MeasureString(gameAnalyticsStatus).ToPoint().ToVector2(); + GUIStyle.SmallFont.DrawString(spriteBatch, gameAnalyticsStatus, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUIStyle.SmallFont.LineHeight * 2 - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); Vector2 textPos = new Vector2(GameMain.GraphicsWidth - HUDLayoutSettings.Padding, GameMain.GraphicsHeight - HUDLayoutSettings.Padding * 0.75f); for (int i = legalCrap.Length - 1; i >= 0; i--) { - textSize = GUI.SmallFont.MeasureString(legalCrap[i]) + textSize = GUIStyle.SmallFont.MeasureString(legalCrap[i]) .ToPoint().ToVector2(); bool mouseOn = i == 0 && - PlayerInput.MousePosition.X > textPos.X - textSize.X && PlayerInput.MousePosition.X < textPos.X && - PlayerInput.MousePosition.Y > textPos.Y - textSize.Y && PlayerInput.MousePosition.Y < textPos.Y; + PlayerInput.MousePosition.X > textPos.X - textSize.X && PlayerInput.MousePosition.X < textPos.X && + PlayerInput.MousePosition.Y > textPos.Y - textSize.Y && PlayerInput.MousePosition.Y < textPos.Y; - GUI.SmallFont.DrawString(spriteBatch, + GUIStyle.SmallFont.DrawString(spriteBatch, legalCrap[i], textPos - textSize, mouseOn ? Color.White : Color.White * 0.7f); @@ -1163,10 +1033,10 @@ namespace Barotrauma #region UI Methods private void CreateCampaignSetupUI() { - menuTabs[(int)Tab.NewGame].ClearChildren(); - menuTabs[(int)Tab.LoadGame].ClearChildren(); + menuTabs[Tab.NewGame].ClearChildren(); + menuTabs[Tab.LoadGame].ClearChildren(); - var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center)) + var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.NewGame].RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f @@ -1174,7 +1044,7 @@ namespace Barotrauma var newGameContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.95f), innerNewGame.RectTransform, Anchor.Center), style: "InnerFrame"); - var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, + var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame, SubmarineInfo.SavedSubmarines) @@ -1186,7 +1056,7 @@ namespace Barotrauma private void CreateHostServerFields() { - menuTabs[(int)Tab.HostServer].ClearChildren(); + menuTabs[Tab.HostServer].ClearChildren(); string name = ""; string password = ""; @@ -1226,14 +1096,14 @@ namespace Barotrauma Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.07f); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[(int)Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.9f), menuTabs[Tab.HostServer].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { RelativeSpacing = 0.02f, Stretch = true }; GUIComponent parent = content; - new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUI.LargeFont) { ForceUpperCase = true }; + new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { ForceUpperCase = ForceUpperCase.Yes }; //play style ----------------------------------------------------- @@ -1250,7 +1120,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(Vector2.One, playstyleBanner.RectTransform), "InnerGlow", color: Color.Black); new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), playstyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.03f) }, - "playstyle name goes here", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + "playstyle name goes here", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), playstyleContainer.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, @@ -1278,10 +1148,10 @@ namespace Barotrauma } }; - string longestPlayStyleStr = ""; + LocalizedString longestPlayStyleStr = ""; foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { - string playStyleStr = TextManager.Get("servertagdescription." + playStyle); + LocalizedString playStyleStr = TextManager.Get("servertagdescription." + playStyle); if (playStyleStr.Length > longestPlayStyleStr.Length) { longestPlayStyleStr = playStyleStr; } } @@ -1289,7 +1159,7 @@ namespace Barotrauma longestPlayStyleStr, style: null, wrap: true) { Color = Color.Black * 0.8f, - TextColor = GUI.Style.GetComponentStyle("GUITextBlock").TextColor + TextColor = GUIStyle.GetComponentStyle("GUITextBlock").TextColor }; playstyleDescription.Padding = Vector4.One * 10.0f * GUI.Scale; playstyleDescription.CalculateHeightFromText(padding: (int)(15 * GUI.Scale)); @@ -1399,8 +1269,8 @@ namespace Barotrauma if (isPublicBox.Selected && ForbiddenWordFilter.IsForbidden(name, out string forbiddenWord)) { var msgBox = new GUIMessageBox("", - TextManager.GetWithVariables("forbiddenservernameverification", new string[] { "[forbiddenword]", "[servername]" }, new string[] { forbiddenWord, name }), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + TextManager.GetWithVariables("forbiddenservernameverification", ("[forbiddenword]", forbiddenWord), ("[servername]", name)), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked += (_, __) => { TryStartServer(); @@ -1437,10 +1307,10 @@ namespace Barotrauma private void FetchRemoteContent() { - if (string.IsNullOrEmpty(GameMain.Config.RemoteContentUrl)) { return; } + if (string.IsNullOrEmpty(RemoteContentUrl)) { return; } try { - var client = new RestClient(GameMain.Config.RemoteContentUrl); + var client = new RestClient(RemoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); client.ExecuteAsync(request, RemoteContentReceived); CoroutineManager.StartCoroutine(WairForRemoteContentReceived()); @@ -1482,9 +1352,9 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(xml)) { remoteContentDoc = XDocument.Parse(xml); - foreach (XElement subElement in remoteContentDoc?.Root.Elements()) + foreach (var subElement in remoteContentDoc?.Root.Elements()) { - GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs new file mode 100644 index 000000000..478571b01 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -0,0 +1,296 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.IO; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Steamworks.Data; +using Color = Microsoft.Xna.Framework.Color; +using ServerContentPackage = Barotrauma.Networking.ClientPeer.ServerContentPackage; + +namespace Barotrauma +{ + class ModDownloadScreen : Screen + { + private readonly Queue pendingDownloads = + new Queue(); + private ServerContentPackage? currentDownload; + + private readonly List downloadedPackages = new List(); + + private bool confirmDownload; + + private void Reset() + { + pendingDownloads.Clear(); + downloadedPackages.Clear(); + currentDownload = null; + confirmDownload = false; + } + + public override void Select() + { + base.Select(); + Reset(); + + Frame.ClearChildren(); + + var mainVisibleFrame = new GUIFrame(new RectTransform((0.6f, 0.8f), Frame.RectTransform, Anchor.Center)); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.93f, mainVisibleFrame.RectTransform, Anchor.Center)); + + void mainLayoutSpacing() + => new GUIFrame(new RectTransform((1.0f, 0.02f), mainLayout.RectTransform), style: null); + + var serverName = new GUITextBlock(new RectTransform((1.0f, 0.08f), mainLayout.RectTransform), + "", font: GUIStyle.LargeFont, + textAlignment: Alignment.CenterLeft) + { + TextGetter = () => GameMain.NetLobbyScreen.ServerName.Text + }; + mainLayoutSpacing(); + var downloadList = new GUIListBox(new RectTransform((1.0f, 0.76f), mainLayout.RectTransform)); + mainLayoutSpacing(); + var disconnectButton = new GUIButton(new RectTransform((0.3f, 0.1f), mainLayout.RectTransform), + TextManager.Get("Disconnect")) + { + OnClicked = (guiButton, o) => + { + GameMain.Client.Disconnect(); + GameMain.MainMenuScreen.Select(); + return false; + } + }; + + var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages + .Where(sp => sp.ContentPackage is null).ToArray(); + if (!missingPackages.Any()) + { + GameMain.NetLobbyScreen.Select(); + return; + } + + GUIMessageBox msgBox = new GUIMessageBox( + TextManager.Get("WorkshopItemDownloadTitle"), + "", + Array.Empty(), + relativeSize: (0.5f, 0.75f)); + + GUILayoutGroup innerLayout = msgBox.Content; + innerLayout.Stretch = true; + + void innerLayoutSpacing(float height) + => new GUIFrame(new RectTransform((1.0f, height), innerLayout.RectTransform), style: null); + + GUITextBlock textBlock(LocalizedString str, GUIFont font, Alignment alignment = Alignment.CenterLeft) + { + var tb = new GUITextBlock(new RectTransform(Point.Zero, innerLayout.RectTransform), str, + wrap: true, textAlignment: alignment, font: font); + new GUICustomComponent(new RectTransform(Vector2.Zero, tb.RectTransform), onUpdate: + (deltaTime, component) => + { + if (tb.RectTransform.NonScaledSize.X != innerLayout.Rect.Width) + { + tb.RectTransform.NonScaledSize = (innerLayout.Rect.Width, 0); + tb.RectTransform.NonScaledSize = (innerLayout.Rect.Width, + (int)tb.Font.MeasureString(tb.WrappedText).Y); + } + }); + return tb; + } + + var title = textBlock(TextManager.Get("ModDownloadTitle"), GUIStyle.SubHeadingFont, Alignment.Center); + innerLayoutSpacing(0.05f); + var header = textBlock(TextManager.Get("ModDownloadHeader"), GUIStyle.Font); + innerLayoutSpacing(0.05f); + + var msgBoxModList = new GUIListBox(new RectTransform(Vector2.One, innerLayout.RectTransform)); + + innerLayoutSpacing(0.05f); + var footer = textBlock(TextManager.Get("ModDownloadFooter"), GUIStyle.Font, Alignment.Center); + + innerLayoutSpacing(0.05f); + GUILayoutGroup buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true); + + void buttonContainerSpacing(float width) + => new GUIFrame(new RectTransform((width, 1.0f), buttonContainer.RectTransform), style: null); + + void button(LocalizedString text, Action action) + => new GUIButton(new RectTransform((0.3f, 1.0f), buttonContainer.RectTransform), text) + { + OnClicked = (_, __) => + { + action(); + msgBox.Close(); + return false; + } + }; + + buttonContainerSpacing(0.1f); + button(TextManager.Get("Yes"), () => confirmDownload = true); + buttonContainerSpacing(0.2f); + button(TextManager.Get("No"), () => + { + GameMain.Client.Disconnect(); + GameMain.MainMenuScreen.Select(); + }); + buttonContainerSpacing(0.1f); + + foreach (var p in missingPackages) + { + pendingDownloads.Enqueue(p); + + //Message box frame + new GUITextBlock(new RectTransform((1.0f, 0.1f), msgBoxModList.Content.RectTransform), p.Name) + { + CanBeFocused = false + }; + + //Download progress frame + var downloadFrame = new GUIFrame(new RectTransform((1.0f, 0.06f), downloadList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = p, + CanBeFocused = false + }; + new GUITextBlock(new RectTransform((0.5f, 1.0f), downloadFrame.RectTransform), p.Name) + { + CanBeFocused = false + }; + var downloadProgress = new GUIProgressBar( + new RectTransform((0.5f, 0.75f), downloadFrame.RectTransform, Anchor.CenterRight), + 0.0f, color: GUIStyle.Green); + downloadProgress.ProgressGetter = () => + { + if (currentDownload == p) + { + FileReceiver.FileTransferIn? getTransfer() => GameMain.Client.FileReceiver.ActiveTransfers.FirstOrDefault(t => t.FileType == FileTransferType.Mod); + + if (downloadProgress.GetAnyChild() is null) + { + GUILayoutGroup progressBarLayout + = new GUILayoutGroup(new RectTransform(Vector2.One, downloadProgress.RectTransform), isHorizontal: true); + + void progressBarText(float width, Alignment textAlignment, Func getter) + { + var textContainer = new GUIFrame(new RectTransform((width, 1.0f), progressBarLayout.RectTransform), + style: null); + var textShadow = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, "", + textColor: Color.Black, textAlignment: textAlignment); + var text = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), "", + textAlignment: textAlignment); + new GUICustomComponent(new RectTransform(Vector2.Zero, textContainer.RectTransform), onUpdate: + (f, component) => + { + string str = getter(); + if (text.Text?.SanitizedValue != str) + { + text.Text = str; + textShadow.Text = str; + } + }); + } + progressBarText(0.475f, Alignment.CenterRight, () => MathUtils.GetBytesReadable(getTransfer()?.Received ?? 0)); + progressBarText(0.05f, Alignment.Center, () => "/"); + progressBarText(0.475f, Alignment.CenterLeft, () => MathUtils.GetBytesReadable(getTransfer()?.FileSize ?? 0)); + } + + return getTransfer()?.Progress ?? 0.0f; + } + + if (!pendingDownloads.Contains(p)) + { + downloadProgress.ClearChildren(); + return 1.0f; + } + + return 0.0f; + }; + } + } + + public override void Deselect() + { + Reset(); + base.Deselect(); + } + + public override void Update(double deltaTime) + { + base.Update(deltaTime); + if (GameMain.Client is null) { return; } + if (!confirmDownload) { return; } + if (currentDownload is null) + { + if (pendingDownloads.TryDequeue(out currentDownload)) + { + GameMain.Client.RequestFile(FileTransferType.Mod, currentDownload.Name, currentDownload.Hash.StringRepresentation); + } + else + { + var serverPackages = GameMain.Client.ClientPeer.ServerContentPackages; + CorePackage corePackage + = downloadedPackages.FirstOrDefault(p => p is CorePackage) as CorePackage + ?? serverPackages.FirstOrDefault(p => p.CorePackage != null) + ?.CorePackage + ?? throw new Exception($"Failed to find core package to enable"); + RegularPackage[] regularPackages + = serverPackages.Where(p => p.CorePackage is null) + .Select(p => + p.RegularPackage + ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) + ?? throw new Exception($"Could not find regular package \"{p.Name}\"")) + .Cast() + .ToArray(); + foreach (var regularPackage in regularPackages) + { + DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime); + } + + ContentPackageManager.EnabledPackages.BackUp(); + ContentPackageManager.EnabledPackages.SetCore(corePackage); + ContentPackageManager.EnabledPackages.SetRegular(regularPackages); + + GameMain.NetLobbyScreen.Select(); + } + } + } + + public void CurrentDownloadFinished(FileReceiver.FileTransferIn transfer) + { + if (currentDownload is null) { throw new Exception("Current download is null"); } + + string path = transfer.FilePath; + if (!path.EndsWith(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase)) + { + return; + } + string dir = path.RemoveFromEnd(ModReceiver.Extension, StringComparison.OrdinalIgnoreCase); + + SaveUtil.DecompressToDirectory(path, dir, file => { }); + ContentPackage newPackage + = ContentPackage.TryLoad($"{dir}/{ContentPackage.FileListFileName}") + ?? throw new Exception($"Failed to load downloaded mod \"{currentDownload.Name}\""); + if (!currentDownload.Hash.Equals(newPackage.Hash)) + { + throw new Exception($"Hash mismatch for downloaded mod \"{currentDownload.Name}\" (expected {currentDownload.Hash}, got {newPackage.Hash})"); + } + downloadedPackages.Add(newPackage); + + currentDownload = null; + + } + + public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) + { + GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); //wtf + + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); + + GUI.Draw(Cam, spriteBatch); + + spriteBatch.End(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index da26a5575..e035855de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -264,7 +264,7 @@ namespace Barotrauma } } - public List> JobPreferences + public List JobPreferences { get { @@ -272,13 +272,13 @@ namespace Barotrauma // (e.g. the player has a pre-existing campaign character) if (JobList?.Content == null) { - return new List>(); + return new List(); } - List> jobPreferences = new List>(); + List jobPreferences = new List(); foreach (GUIComponent child in JobList.Content.Children) { - if (!(child.UserData is Pair jobPrefab)) { continue; } + if (!(child.UserData is JobVariant jobPrefab)) { continue; } jobPreferences.Add(jobPrefab); } return jobPreferences; @@ -384,14 +384,14 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.05f }; - FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUI.SmallFont); + FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", - font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => @@ -527,7 +527,7 @@ namespace Barotrauma chatInput = new GUITextBox(new RectTransform(new Vector2(0.95f, 1.0f), chatRow.RectTransform)) { MaxTextLength = ChatMessage.MaxLength, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, DeselectAfterMessage = false }; @@ -578,7 +578,7 @@ namespace Barotrauma serverLogFilter = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.07f), serverLogHolder.RectTransform)) { MaxTextLength = ChatMessage.MaxLength, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), @@ -618,7 +618,7 @@ namespace Barotrauma //autorestart ------------------------------------------------------------------ - autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUI.SmallFont, style: "TextFrame", textAlignment: Alignment.Center); + autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUIStyle.SmallFont, style: "TextFrame", textAlignment: Alignment.Center); GUIFrame autoRestartBoxContainer = new GUIFrame(new RectTransform(Vector2.One, bottomBarMid.RectTransform), style: "TextFrame"); autoRestartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), autoRestartBoxContainer.RectTransform, Anchor.Center), TextManager.Get("AutoRestart")) { @@ -704,13 +704,13 @@ namespace Barotrauma HideElementsOutsideFrame = true }; new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), serverBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.04f) }, - "", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") { CanBeFocused = false }; publicOrPrivate = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), serverBanner.RectTransform, Anchor.BottomRight, Pivot.BottomRight), - "", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") + "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") { CanBeFocused = false }; @@ -719,7 +719,7 @@ namespace Barotrauma ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform), style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft); var serverMessageHint = new GUITextBlock(new RectTransform(Vector2.One, ServerMessage.RectTransform), - textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUI.Style.Font, text: TextManager.Get("ClickToWriteServerMessage")); + textColor: Color.DarkGray * 0.6f, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, text: TextManager.Get("ClickToWriteServerMessage")); void updateServerMessageScrollBasedOnCaret() { @@ -786,7 +786,7 @@ namespace Barotrauma Stretch = true }; - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUI.SubHeadingFont); + var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUIStyle.SubHeadingFont); SubVisibilityButton = new GUIButton( @@ -806,8 +806,8 @@ namespace Barotrauma { Stretch = true }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); + subSearchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; @@ -895,7 +895,7 @@ namespace Barotrauma Stretch = true }; - var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode"), font: GUI.SubHeadingFont); + var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode"), font: GUIStyle.SubHeadingFont); voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) { @@ -922,8 +922,8 @@ namespace Barotrauma Stretch = true }; - var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUI.SubHeadingFont); - var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUI.SmallFont, wrap: true); + var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); + var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; @@ -953,7 +953,7 @@ namespace Barotrauma Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), - TextManager.Get("gamemode.multiplayercampaign"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center); + TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); ContinueCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("campaigncontinue"), textAlignment: Alignment.Center) { @@ -981,9 +981,9 @@ namespace Barotrauma { Stretch = true }; - + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("MissionType"), font: GUI.SubHeadingFont); + TextManager.Get("MissionType"), font: GUIStyle.SubHeadingFont); missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) { OnSelected = (component, obj) => @@ -1020,7 +1020,7 @@ namespace Barotrauma TextManager.Get("MissionType." + missionType.ToString())) { UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), OnSelected = (tickbox) => { int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; @@ -1045,7 +1045,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, - TextManager.Get("Settings"), font: GUI.SubHeadingFont); + TextManager.Get("Settings"), font: GUIStyle.SubHeadingFont); var settingsFrame = new GUIFrame(new RectTransform(Vector2.One, settingsHolder.RectTransform), style: "InnerFrame"); var settingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), settingsFrame.RectTransform, Anchor.Center)) { @@ -1089,11 +1089,11 @@ namespace Barotrauma }; levelDifficultyScrollBar.OnMoved = (scrollbar, value) => { - if (EventManagerSettings.List.Count == 0) { return true; } + if (!EventManagerSettings.Prefabs.Any()) { return true; } difficultyName.Text = - EventManagerSettings.List[Math.Min((int)Math.Floor(value * EventManagerSettings.List.Count), EventManagerSettings.List.Count - 1)].Name + EventManagerSettings.GetByDifficultyPercentile(value).Name + " (" + ((int)Math.Round(scrollbar.BarScrollValue)) + " %)"; - difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + difficultyName.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return true; }; @@ -1214,8 +1214,8 @@ namespace Barotrauma public IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); - string headerText = TextManager.Get("RoundStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), new string[0]); + LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait"); + var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty()); if (startButton != null) { @@ -1405,7 +1405,7 @@ namespace Barotrauma if (characterInfo == null || CampaignCharacterDiscarded) { characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); - characterInfo.RecreateHead(GameMain.Config.PlayerCharacterCustomization); + characterInfo.RecreateHead(MultiplayerPreferences.Instance); GameMain.Client.CharacterInfo = characterInfo; characterInfo.OmitJobInPortraitClothing = false; } @@ -1517,20 +1517,20 @@ namespace Barotrauma for (int i = 0; i < 3; i++) { - Pair jobPrefab = null; - while (i < GameMain.Config.JobPreferences.Count) + JobVariant jobPrefab = null; + while (i < MultiplayerPreferences.Instance.JobPreferences.Count) { - var jobIdentifier = GameMain.Config.JobPreferences[i]; - if (!JobPrefab.Prefabs.ContainsKey(jobIdentifier.First)) + var jobPreference = MultiplayerPreferences.Instance.JobPreferences[i]; + if (!JobPrefab.Prefabs.ContainsKey(jobPreference.JobIdentifier)) { - GameMain.Config.JobPreferences.RemoveAt(i); + MultiplayerPreferences.Instance.JobPreferences.RemoveAt(i); continue; } // The old job variant system used one-based indexing // so let's make sure no one get to pick a variant which doesn't exist - var prefab = JobPrefab.Prefabs[jobIdentifier.First]; - var variant = Math.Min(jobIdentifier.Second, prefab.Variants - 1); - jobPrefab = new Pair(prefab, variant); + var prefab = JobPrefab.Prefabs[jobPreference.JobIdentifier]; + var variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); + jobPrefab = new JobVariant(prefab, variant); break; } @@ -1553,20 +1553,20 @@ namespace Barotrauma { characterInfo.CreateIcon(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, SelectedColor = Color.Transparent }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); foreach (Skill skill in characterInfo.Job.Skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), textColor, - font: GUI.SmallFont); + font: GUIStyle.SmallFont); } // Spacing @@ -1644,15 +1644,15 @@ namespace Barotrauma SelectedTextColor = Color.White }; - TeamPreferenceListBox.Select(GameMain.Config.TeamPreference); + TeamPreferenceListBox.Select(MultiplayerPreferences.Instance.TeamPreference); TeamPreferenceListBox.OnSelected += (component, obj) => { - if ((CharacterTeamType)obj == GameMain.Config.TeamPreference) { return true; } + if ((CharacterTeamType)obj == MultiplayerPreferences.Instance.TeamPreference) { return true; } - GameMain.Config.TeamPreference = (CharacterTeamType)obj; + MultiplayerPreferences.Instance.TeamPreference = (CharacterTeamType)obj; GameMain.Client.ForceNameAndJobUpdate(); - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); return true; }; @@ -1685,7 +1685,7 @@ namespace Barotrauma IgnoreLayoutGroups = true }; var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), - TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null); + TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null); changesPendingText.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.2f), (int)(text.TextSize.Y * 2.0f)); } @@ -1698,7 +1698,7 @@ namespace Barotrauma Color = Color.Black }; new GUITextBlock(new RectTransform(Vector2.One, changesPendingFrame.RectTransform, Anchor.Center), - TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null) + TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null) { AutoScaleHorizontal = true }; @@ -1709,7 +1709,7 @@ namespace Barotrauma jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), style: "GUIToolTip") { - UserData = new Pair(jobPrefab, variant) + UserData = new JobVariant(jobPrefab, variant) }; jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.Right, parentSlot.Rect.Y); @@ -1718,8 +1718,8 @@ namespace Barotrauma Stretch = true, AbsoluteSpacing = (int)(15 * GUI.Scale) }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUI.SubHeadingFont, textAlignment: Alignment.Center); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); var itemIdentifiers = jobPrefab.PreviewItems[variant] .Where(it => it.ShowPreview) @@ -1730,7 +1730,7 @@ namespace Barotrauma int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1); new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomCenter), - onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new Pair(jobPrefab, variant), itemsPerRow); }); + onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new JobVariant(jobPrefab, variant), itemsPerRow); }); jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); } @@ -1818,12 +1818,12 @@ namespace Barotrauma int buttonSize = (int)(frame.Rect.Height * 0.8f); var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft) /*{ AbsoluteOffset = new Point(buttonSize + 5, 0) }*/, - ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) + ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation); if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) @@ -1831,7 +1831,7 @@ namespace Barotrauma subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); frame.ToolTip = TextManager.Get("SubNotFound"); } - else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.Hash != sub.MD5Hash?.Hash) + else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.StringRepresentation != sub.MD5Hash?.StringRepresentation) { subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); frame.ToolTip = TextManager.Get("SubDoesntMatch"); @@ -1847,7 +1847,7 @@ namespace Barotrauma if (!sub.RequiredContentPackagesInstalled) { subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); - frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; + frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.ToolTip.SanitizedString; } CreateSubmarineClassText( @@ -1866,10 +1866,10 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, - TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip, + ToolTip = subTextBlock.ToolTip?.SanitizedString, CanBeFocused = false }; //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) @@ -1885,11 +1885,11 @@ namespace Barotrauma else { var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip, + ToolTip = subTextBlock.ToolTip, CanBeFocused = false }; } @@ -1911,7 +1911,7 @@ namespace Barotrauma selectedSub.RequiredContentPackages.Any() ? TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)) : TextManager.Get("ContentPackageMismatchWarningGeneric"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => @@ -1941,14 +1941,14 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.SelectMode)) { - string presetName = ((GameModePreset)component.UserData).Identifier; + Identifier presetName = ((GameModePreset)component.UserData).Identifier; //display a verification prompt when switching away from the campaign if (HighlightedModeIndex == SelectedModeIndex && (GameMain.NetLobbyScreen.ModeList.SelectedData as GameModePreset) == GameModePreset.MultiPlayerCampaign && presetName != GameModePreset.MultiPlayerCampaign.Identifier) { - var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); verificationBox.Buttons[0].OnClicked += (btn, userdata) => { GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); @@ -1962,7 +1962,7 @@ namespace Barotrauma GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); - if (presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) + if (presetName == "multiplayercampaign") { GUI.SetCursorWaiting(endCondition: () => { @@ -1970,7 +1970,7 @@ namespace Barotrauma }); } - return !presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase); + return presetName != "multiplayercampaign"; } return false; } @@ -1994,7 +1994,7 @@ namespace Barotrauma public void AddPlayer(Client client) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), PlayerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, - client.Name, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, style: null) + client.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, style: null) { Padding = Vector4.One * 10.0f * GUI.Scale, Color = Color.White * 0.25f, @@ -2006,7 +2006,7 @@ namespace Barotrauma UserData = client }; var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, - sprite: GUI.Style.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) + sprite: GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { UserData = new Pair("soundicon", 0.0f), CanBeFocused = false, @@ -2170,7 +2170,7 @@ namespace Barotrauma { permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => { - string label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, rank.Name }); + LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = delegate @@ -2257,7 +2257,7 @@ namespace Barotrauma }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), - text: selectedClient.Name, font: GUI.LargeFont); + text: selectedClient.Name, font: GUIStyle.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); if (hasManagePermissions) @@ -2265,7 +2265,7 @@ namespace Barotrauma PlayerFrame.UserData = selectedClient; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), - TextManager.Get("Rank"), font: GUI.SubHeadingFont); + TextManager.Get("Rank"), font: GUIStyle.SubHeadingFont); var rankDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank")) { @@ -2303,9 +2303,9 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.05f }; - var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUI.SubHeadingFont); + var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUIStyle.SubHeadingFont); var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), - TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUI.SubHeadingFont); + TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(permissionLabel, consoleCommandLabel); var permissionContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedPlayerFrame.RectTransform), isHorizontal: true) @@ -2320,7 +2320,7 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerLeft.RectTransform), TextManager.Get("all", fallBackTag: "clientpermission.all")) + new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerLeft.RectTransform), TextManager.Get("all", "clientpermission.all")) { Enabled = !myClient, OnSelected = (tickbox) => @@ -2351,7 +2351,7 @@ namespace Barotrauma if (permission == ClientPermissions.None || permission == ClientPermissions.All) continue; var permissionTick = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), permissionsBox.Content.RectTransform), - TextManager.Get("ClientPermission." + permission), font: GUI.SmallFont) + TextManager.Get("ClientPermission." + permission), font: GUIStyle.SmallFont) { UserData = permission, Selected = selectedClient.HasPermission(permission), @@ -2387,7 +2387,7 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerRight.RectTransform), TextManager.Get("all", fallBackTag: "clientpermission.all")) + new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerRight.RectTransform), TextManager.Get("all", "clientpermission.all")) { Enabled = !myClient, OnSelected = (tickbox) => @@ -2415,7 +2415,7 @@ namespace Barotrauma foreach (DebugConsole.Command command in DebugConsole.Commands) { var commandTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), commandList.Content.RectTransform), - command.names[0], font: GUI.SmallFont) + command.names[0], font: GUIStyle.SmallFont) { Selected = selectedClient.PermittedConsoleCommands.Contains(command), Enabled = !myClient, @@ -2521,7 +2521,7 @@ namespace Barotrauma viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; viewSteamProfileButton.OnClicked = (bt, userdata) => { - Steamworks.SteamFriends.OpenWebOverlay("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); + SteamManager.OverlayCustomURL("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); return true; }; } @@ -2605,26 +2605,22 @@ namespace Barotrauma if (GameMain.Client == null) { return; } - string currMicStyle = micIcon.Style.Element.Name.LocalName; + Identifier currMicStyle = micIcon.Style.Element.NameAsIdentifier(); - string targetMicStyle = "GUIMicrophoneEnabled"; - if (GameMain.Config.CaptureDeviceNames == null) + Identifier targetMicStyle = "GUIMicrophoneEnabled".ToIdentifier(); + var voipCaptureDeviceNames = VoipCapture.CaptureDeviceNames; + if (voipCaptureDeviceNames.Count == 0) { - GameMain.Config.CaptureDeviceNames = OpenAL.Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + targetMicStyle = "GUIMicrophoneUnavailable".ToIdentifier(); + } + else if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) + { + targetMicStyle = "GUIMicrophoneDisabled".ToIdentifier(); } - if (GameMain.Config.CaptureDeviceNames.Count == 0) + if (targetMicStyle != currMicStyle) { - targetMicStyle = "GUIMicrophoneUnavailable"; - } - else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) - { - targetMicStyle = "GUIMicrophoneDisabled"; - } - - if (!targetMicStyle.Equals(currMicStyle, StringComparison.OrdinalIgnoreCase)) - { - GUI.Style.Apply(micIcon, targetMicStyle); + GUIStyle.Apply(micIcon, targetMicStyle); } foreach (GUIComponent child in PlayerList.Content.Children) @@ -2672,12 +2668,11 @@ namespace Barotrauma JobSelectionFrame.Visible = false; } - if (GUI.MouseOn?.UserData is Pair jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") + if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") { - var prevVisibleVariant = jobVariantTooltip?.UserData as Pair; - if (jobVariantTooltip == null || prevVisibleVariant.First != jobPrefab.First || prevVisibleVariant.Second != jobPrefab.Second) + if (!(jobVariantTooltip?.UserData is JobVariant prevVisibleVariant) || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) { - CreateJobVariantTooltip(jobPrefab.First, jobPrefab.Second, GUI.MouseOn.Parent); + CreateJobVariantTooltip(jobPrefab.Prefab, jobPrefab.Variant, GUI.MouseOn.Parent); } } if (jobVariantTooltip != null) @@ -2730,9 +2725,9 @@ namespace Barotrauma publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint(); } - private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, Pair jobPrefab, int itemsPerRow) + private void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow) { - var itemIdentifiers = jobPrefab.First.PreviewItems[jobPrefab.Second] + var itemIdentifiers = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant] .Where(it => it.ShowPreview) .Select(it => it.ItemIdentifier) .Distinct(); @@ -2753,8 +2748,8 @@ namespace Barotrauma } int i = 0; Rectangle tooltipRect = Rectangle.Empty; - string tooltip = null; - foreach (var itemIdentifier in itemIdentifiers) + LocalizedString tooltip = null; + foreach (Identifier itemIdentifier in itemIdentifiers) { if (!(MapEntityPrefab.Find(null, identifier: itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)) { continue; } @@ -2769,15 +2764,15 @@ namespace Barotrauma scale: slotSize.X / (float)Inventory.SlotSpriteSmall.SourceRect.Width, color: slotRect.Contains(PlayerInput.MousePosition) ? Color.White : Color.White * 0.6f); - Sprite icon = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + Sprite icon = itemPrefab.InventoryIcon ?? itemPrefab.Sprite; float iconScale = Math.Min(Math.Min(slotSize.X / icon.size.X, slotSize.Y / icon.size.Y), 2.0f) * 0.9f; icon.Draw(spriteBatch, slotPos + slotSize.ToVector2() * 0.5f, scale: iconScale); - int count = jobPrefab.First.PreviewItems[jobPrefab.Second].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier); + int count = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier); if (count > 1) { string itemCountText = "x" + count; - GUI.Font.DrawString(spriteBatch, itemCountText, slotPos + slotSize.ToVector2() - GUI.Font.MeasureString(itemCountText) - Vector2.UnitX * 5, Color.White); + GUIStyle.Font.DrawString(spriteBatch, itemCountText, slotPos + slotSize.ToVector2() - GUIStyle.Font.MeasureString(itemCountText) - Vector2.UnitX * 5, Color.White); } if (slotRect.Contains(PlayerInput.MousePosition)) @@ -2787,7 +2782,7 @@ namespace Barotrauma } i++; } - if (!string.IsNullOrEmpty(tooltip)) + if (!tooltip.IsNullOrEmpty()) { GUIComponent.DrawToolTip(spriteBatch, tooltip, tooltipRect); } @@ -2803,11 +2798,10 @@ namespace Barotrauma } GUITextBlock msg = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), chatBox.Content.RectTransform), - text: ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender, + text: RichString.Rich(ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender), textColor: message.Color, color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, - wrap: true, font: GUI.SmallFont, - parseRichText: true) + wrap: true, font: GUIStyle.SmallFont) { UserData = message, CanBeFocused = false @@ -2876,14 +2870,25 @@ namespace Barotrauma }, OnSliderReleased = SaveHead }; - return false; } private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); private bool StoreHead(bool save) { - GameMain.Config.PlayerCharacterCustomization = GameMain.Client.CharacterInfo.Head; + var info = GameMain.Client.CharacterInfo; + + var characterConfig = MultiplayerPreferences.Instance; + + characterConfig.TagSet.Clear(); characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); + characterConfig.HairIndex = info.Head.HairIndex; + characterConfig.BeardIndex = info.Head.BeardIndex; + characterConfig.MoustacheIndex = info.Head.MoustacheIndex; + characterConfig.FaceAttachmentIndex = info.Head.FaceAttachmentIndex; + characterConfig.HairColor = info.Head.HairColor; + characterConfig.FacialHairColor = info.Head.FacialHairColor; + characterConfig.SkinColor = info.Head.SkinColor; + if (save) { if (GameMain.GameSession?.IsRunning ?? false) @@ -2891,14 +2896,14 @@ namespace Barotrauma TabMenu.PendingChanges = true; CreateChangesPendingText(); } - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); } return true; } private bool SwitchJob(GUIButton _, object obj) { - if (JobList == null) { return false; } + if (JobList == null || GameMain.Client == null) { return false; } int childIndex = JobList.SelectedIndex; var child = JobList.SelectedComponent; @@ -2906,11 +2911,11 @@ namespace Barotrauma bool moveToNext = obj != null; - var jobPrefab = (obj as Pair)?.First; + var jobPrefab = (obj as JobVariant)?.Prefab; var prevObj = child.UserData; - var existingChild = JobList.Content.FindChild(d => (d.UserData is Pair prefab) && (prefab.First == jobPrefab)); + var existingChild = JobList.Content.FindChild(d => (d.UserData is JobVariant prefab) && (prefab.Prefab == jobPrefab)); if (existingChild != null && obj != null) { existingChild.UserData = prevObj; @@ -2981,13 +2986,13 @@ namespace Barotrauma GUIButton jobButton = null; var availableJobs = JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is Pair prefab) || prefab.First != jobPrefab) - ).Select(j => new Pair(j, 0)); + jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is JobVariant prefab) || prefab.Prefab != jobPrefab) + ).Select(j => new JobVariant(j, 0)); availableJobs = availableJobs.Concat( JobPrefab.Prefabs.Where(jobPrefab => - jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is Pair prefab) && prefab.First == jobPrefab) - ).Select(j => JobList.Content.FindChild(c => (c.UserData is Pair prefab) && prefab.First == j).UserData as Pair)); + jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab) + ).Select(j => (JobVariant)JobList.Content.FindChild(c => (c.UserData is JobVariant prefab) && prefab.Prefab == j).UserData)); availableJobs = availableJobs.ToList(); @@ -3012,11 +3017,11 @@ namespace Barotrauma }; itemsInRow++; - var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.First, selectedByPlayer: false); + var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.Prefab, selectedByPlayer: false); if (images != null && images.Length > 1) { - jobPrefab.Second = Math.Min(jobPrefab.Second, images.Length); - int currVisible = jobPrefab.Second; + jobPrefab.Variant = Math.Min(jobPrefab.Variant, images.Length); + int currVisible = jobPrefab.Variant; GUIButton currSelected = null; for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { @@ -3029,7 +3034,7 @@ namespace Barotrauma variantButton.OnClicked = (btn, obj) => { if (currSelected != null) { currSelected.Selected = false; } - int k = ((Pair)obj).Second; + int k = ((JobVariant)obj).Variant; btn.Parent.UserData = obj; for (int j = 0; j < images.Length; j++) { @@ -3062,14 +3067,14 @@ namespace Barotrauma private GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer) { GUIFrame innerFrame = null; - List outfitPreviews = jobPrefab.GetJobOutfitSprites(Gender.Male, useInventoryIcon: true, out var maxDimensions); + List outfitPreviews = jobPrefab.GetJobOutfitSprites(CharacterPrefab.HumanPrefab.CharacterInfoPrefab, useInventoryIcon: true, out var maxDimensions); innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.85f, parent.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - GUIImage[][] retVal = new GUIImage[0][]; + GUIImage[][] retVal = Array.Empty(); if (outfitPreviews != null && outfitPreviews.Any()) { retVal = new GUIImage[outfitPreviews.Count][]; @@ -3212,7 +3217,7 @@ namespace Barotrauma { CampaignSetupFrame.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), - TextManager.Get("campaignstarting"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + TextManager.Get("campaignstarting"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); } } } @@ -3284,9 +3289,9 @@ namespace Barotrauma private bool ViewJobInfo(GUIButton button, object obj) { - if (!(button.UserData is Pair jobPrefab)) { return false; } + if (!(button.UserData is JobVariant jobPrefab)) { return false; } - JobInfoFrame = jobPrefab.First.CreateInfoFrame(out GUIComponent buttonContainer); + JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(out GUIComponent buttonContainer); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { @@ -3315,7 +3320,7 @@ namespace Barotrauma /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ - List> jobNamePreferences = new List>(); + List jobPreferences = new List(); bool disableNext = false; for (int i = 0; i < listBox.Content.CountChildren; i++) @@ -3325,15 +3330,15 @@ namespace Barotrauma slot.ClearChildren(); slot.CanBeFocused = !disableNext; - if (slot.UserData is Pair jobPrefab) + if (slot.UserData is JobVariant jobPrefab) { - var images = AddJobSpritesToGUIComponent(slot, jobPrefab.First, selectedByPlayer: true); + var images = AddJobSpritesToGUIComponent(slot, jobPrefab.Prefab, selectedByPlayer: true); for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { foreach (GUIImage image in images[variantIndex]) { //jobPreferenceSprites.Add(image.Sprite); - int selectedVariantIndex = Math.Min(jobPrefab.Second, images.Length); + int selectedVariantIndex = Math.Min(jobPrefab.Variant, images.Length); image.Visible = images.Length == 1 || selectedVariantIndex == variantIndex; } if (images.Length > 1) @@ -3372,14 +3377,14 @@ namespace Barotrauma } }; - jobNamePreferences.Add(new Pair(jobPrefab.First.Identifier, jobPrefab.Second)); + jobPreferences.Add(new MultiplayerPreferences.JobPreference(jobPrefab.Prefab.Identifier, jobPrefab.Variant)); } else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), slot.RectTransform), (i + 1).ToString(), textColor: Color.White * (disableNext ? 0.15f : 0.5f), textAlignment: Alignment.Center, - font: GUI.LargeFont) + font: GUIStyle.LargeFont) { CanBeFocused = false }; @@ -3387,7 +3392,7 @@ namespace Barotrauma if (!disableNext) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), slot.RectTransform, Anchor.BottomCenter), TextManager.Get("clicktoselectjob"), - font: GUI.SmallFont, + font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center) { @@ -3400,7 +3405,7 @@ namespace Barotrauma } GameMain.Client.ForceNameAndJobUpdate(); - if (!GameMain.Config.AreJobPreferencesEqual(jobNamePreferences)) + if (!MultiplayerPreferences.Instance.AreJobPreferencesEqual(jobPreferences)) { if (GameMain.GameSession?.IsRunning ?? false) { @@ -3408,12 +3413,13 @@ namespace Barotrauma CreateChangesPendingText(); } - GameMain.Config.JobPreferences = jobNamePreferences; - GameMain.Config.SaveNewPlayerConfig(); + MultiplayerPreferences.Instance.JobPreferences.Clear(); + MultiplayerPreferences.Instance.JobPreferences.AddRange(jobPreferences); + GameSettings.SaveCurrentConfig(); } } - private GUIButton CreateJobVariantButton(Pair jobPrefab, int variantIndex, int variantCount, GUIComponent slot) + private GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { float relativeSize = 0.15f; @@ -3421,8 +3427,8 @@ namespace Barotrauma { RelativeOffset = new Vector2(relativeSize * 1.3f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, (variantIndex + 1).ToString(), style: "JobVariantButton") { - Selected = jobPrefab.Second == variantIndex, - UserData = new Pair(jobPrefab.First, variantIndex), + Selected = jobPrefab.Variant == variantIndex, + UserData = new JobVariant(jobPrefab.Prefab, variantIndex), }; return btn; @@ -3440,6 +3446,7 @@ namespace Barotrauma public static bool operator ==(FailedSubInfo a, FailedSubInfo b) => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash); + public static bool operator !=(FailedSubInfo a, FailedSubInfo b) => !(a == b); } @@ -3462,7 +3469,7 @@ namespace Barotrauma } SubmarineInfo sub = subList.Content.Children - .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.Hash == md5Hash)? + .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.StringRepresentation == md5Hash)? .UserData as SubmarineInfo; //matching sub found and already selected, all good @@ -3473,7 +3480,7 @@ namespace Barotrauma CreateSubPreview(sub); } - if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) + if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.StringRepresentation == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) { return true; } @@ -3507,7 +3514,7 @@ namespace Barotrauma FailedSelectedShuttle = null; //hashes match, all good - if (sub.MD5Hash?.Hash == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) + if (sub.MD5Hash?.StringRepresentation == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { return true; } @@ -3525,21 +3532,23 @@ namespace Barotrauma FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash); } - string errorMsg = ""; + LocalizedString errorMsg = ""; if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub)) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " "; } - else if (sub.MD5Hash?.Hash == null) + else if (sub.MD5Hash?.StringRepresentation == null) { errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", subName) + " "; GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); - if (textBlock != null) { textBlock.TextColor = GUI.Style.Red; } + if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; } } else { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]" , "[myhash]", "[serverhash]" }, - new string[3] { sub.Name, sub.MD5Hash.ShortHash, Md5Hash.GetShortHash(md5Hash) }) + " "; + errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", + ("[subname]", sub.Name), + ("[myhash]", sub.MD5Hash.ShortRepresentation), + ("[serverhash]", Md5Hash.GetShortHash(md5Hash))) + " "; } //already showing a message about the same sub @@ -3553,7 +3562,7 @@ namespace Barotrauma errorMsg += TextManager.Get("DownloadSubQuestion"); var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "request" + subName }; @@ -3590,7 +3599,7 @@ namespace Barotrauma return false; } - SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.Hash == serverSubmarine.MD5Hash?.Hash); + SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.StringRepresentation == serverSubmarine.MD5Hash?.StringRepresentation); if (purchasableSub != null) { return true; @@ -3598,21 +3607,23 @@ namespace Barotrauma purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name); - string errorMsg = ""; + LocalizedString errorMsg = ""; if (purchasableSub == null) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", serverSubmarine.Name) + " "; } - else if (purchasableSub.MD5Hash?.Hash == null) + else if (purchasableSub.MD5Hash?.StringRepresentation == null) { errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", serverSubmarine.Name) + " "; /*GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); - if (textBlock != null) { textBlock.TextColor = GUI.Style.Red; }*/ + if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; }*/ } else { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]", "[myhash]", "[serverhash]" }, - new string[3] { purchasableSub.Name, purchasableSub.MD5Hash.ShortHash, Md5Hash.GetShortHash(serverSubmarine.MD5Hash.Hash) }) + " "; + errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", + ("[subname]", purchasableSub.Name), + ("[myhash]", purchasableSub.MD5Hash.ShortRepresentation), + ("[serverhash]", Md5Hash.GetShortHash(serverSubmarine.MD5Hash.StringRepresentation))) + " "; } errorMsg += TextManager.Get("DownloadSubQuestion"); @@ -3624,11 +3635,11 @@ namespace Barotrauma } var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }) + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) { UserData = "request" + serverSubmarine.Name }; - requestFileBox.Buttons[0].UserData = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.Hash); + requestFileBox.Buttons[0].UserData = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.StringRepresentation); requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => { @@ -3675,7 +3686,7 @@ namespace Barotrauma private void CreateSubmarineVisibilityMenu() { var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "", - buttons: Array.Empty(), + buttons: Array.Empty(), relativeSize: new Vector2(0.75f, 0.75f)); messageBox.Content.ChildAnchor = Anchor.TopCenter; var columns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), messageBox.Content.RectTransform), isHorizontal: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 728752cca..418ee66ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using Barotrauma.Particles; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; using System.Text; using Barotrauma.Extensions; @@ -110,7 +111,7 @@ namespace Barotrauma }; var emitterListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), paddedRightPanel.RectTransform)); - new SerializableEntityEditor(emitterListBox.Content.RectTransform, emitterProperties, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); + new SerializableEntityEditor(emitterListBox.Content.RectTransform, emitterProperties, false, true, elementHeight: 20, titleFont: GUIStyle.SubHeadingFont); var listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), paddedRightPanel.RectTransform)); @@ -120,8 +121,8 @@ namespace Barotrauma UserData = "filterarea" }; - filterLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font) { IgnoreLayoutGroups = true }; - filterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font); + filterLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font) { IgnoreLayoutGroups = true }; + filterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font); filterBox.OnTextChanged += (textBox, text) => { FilterEmitters(text); return true; }; new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), filterArea.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton") { @@ -136,7 +137,7 @@ namespace Barotrauma emitterPrefab = new ParticleEmitterPrefab(selectedPrefab, emitterProperties); emitter = new ParticleEmitter(emitterPrefab); listBox.ClearChildren(); - new SerializableEntityEditor(listBox.Content.RectTransform, selectedPrefab, false, true, elementHeight: 20, titleFont: GUI.SubHeadingFont); + new SerializableEntityEditor(listBox.Content.RectTransform, selectedPrefab, false, true, elementHeight: 20, titleFont: GUIStyle.SubHeadingFont); //listBox.Content.RectTransform.NonScaledSize = particlePrefabEditor.RectTransform.NonScaledSize; //listBox.UpdateScrollBarSize(); return true; @@ -167,7 +168,7 @@ namespace Barotrauma foreach (ParticlePrefab particlePrefab in particlePrefabs) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), prefabList.Content.RectTransform) { MinSize = new Point(0, 20) }, - particlePrefab.DisplayName) + particlePrefab.Name) { Padding = Vector4.Zero, UserData = particlePrefab @@ -196,7 +197,7 @@ namespace Barotrauma private void SerializeAll() { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -218,7 +219,7 @@ namespace Barotrauma NewLineOnAttributes = true }; - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -265,7 +266,7 @@ namespace Barotrauma }; XElement originalElement = null; - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) + foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -273,7 +274,7 @@ namespace Barotrauma var prefabList = GameMain.ParticleManager.GetPrefabList(); foreach (ParticlePrefab otherPrefab in prefabList) { - foreach (XElement subElement in doc.Root.Elements()) + foreach (var subElement in doc.Root.Elements()) { if (!subElement.Name.ToString().Equals(prefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(prefab, subElement, true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs index 5aeea7263..3c63217cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs @@ -9,7 +9,7 @@ namespace Barotrauma { private Sprite backgroundSprite; private RoundSummary roundSummary; - private string loadText; + private LocalizedString loadText; private RectTransform prevGuiElementParent; @@ -47,9 +47,9 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - string loadingText = loadText + new string('.', (int)Timing.TotalTime % 3 + 1); - Vector2 textSize = GUI.LargeFont.MeasureString(loadText); - GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.95f) - textSize / 2, loadingText, Color.White, font: GUI.LargeFont); + LocalizedString loadingText = loadText + new string('.', (int)Timing.TotalTime % 3 + 1); + Vector2 textSize = GUIStyle.LargeFont.MeasureString(loadText); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, GameMain.GraphicsHeight * 0.95f) - textSize / 2, loadingText, Color.White, font: GUIStyle.LargeFont); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 3093092c7..356b8878c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Barotrauma { - partial class Screen + abstract partial class Screen { private GUIFrame frame; public GUIFrame Frame @@ -70,6 +70,7 @@ namespace Barotrauma public virtual void Release() { + if (frame is null) { return; } frame.RectTransform.Parent = null; frame = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 461a8ba29..5ec47b586 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -44,34 +44,6 @@ namespace Barotrauma private GUIButton friendsDropdownButton; private GUIListBox friendsDropdown; - //Workshop downloads - public struct PendingWorkshopDownload - { - public readonly string ExpectedHash; - public readonly ulong Id; - public readonly Steamworks.Ugc.Item? Item; - - public PendingWorkshopDownload(string expectedHash, Steamworks.Ugc.Item item) - { - ExpectedHash = expectedHash; - Item = item; - Id = item.Id; - } - - public PendingWorkshopDownload(string expectedHash, ulong id) - { - ExpectedHash = expectedHash; - Item = null; - Id = id; - } - } - - private GUIFrame workshopDownloadsFrame = null; - private Steamworks.Ugc.Item? currentlyDownloadingWorkshopItem = null; - private Dictionary pendingWorkshopDownloads = null; - private string autoConnectName; - private string autoConnectEndpoint; - private enum TernaryOption { Any, @@ -84,7 +56,7 @@ namespace Barotrauma public UInt64 SteamID; public string Name; public Sprite Sprite; - public string StatusText; + public LocalizedString StatusText; public bool PlayingThisGame; public bool PlayingAnotherGame; public string ConnectName; @@ -95,7 +67,7 @@ namespace Barotrauma { get { - return PlayingThisGame && !string.IsNullOrWhiteSpace(StatusText) && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); + return PlayingThisGame && !StatusText.IsNullOrWhiteSpace() && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); } } } @@ -182,10 +154,10 @@ namespace Barotrauma private GUITickBox filterFull; private GUITickBox filterEmpty; private GUITickBox filterWhitelisted; - private Dictionary ternaryFilters; - private Dictionary filterTickBoxes; - private Dictionary playStyleTickBoxes; - private Dictionary gameModeTickBoxes; + private Dictionary ternaryFilters; + private Dictionary filterTickBoxes; + private Dictionary playStyleTickBoxes; + private Dictionary gameModeTickBoxes; private GUITickBox filterOffensive; //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. @@ -212,7 +184,7 @@ namespace Barotrauma CreateUI(); } - private void AddTernaryFilter(RectTransform parent, float elementHeight, string tag, Action valueSetter) + private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) { var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) { @@ -239,7 +211,7 @@ namespace Barotrauma { UserData = TextManager.Get("servertag." + tag + ".label") }; - GUI.Style.Apply(filterLabel, "GUITextBlock", null); + GUIStyle.Apply(filterLabel, "GUITextBlock", null); var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); @@ -249,6 +221,7 @@ namespace Barotrauma dropDown.OnSelected = (_, data) => { valueSetter((TernaryOption)data); FilterServers(); + StoreServerFilters(); return true; }; @@ -271,10 +244,10 @@ namespace Barotrauma var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUI.LargeFont) + var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont) { Padding = Vector4.Zero, - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, AutoScaleHorizontal = true }; @@ -282,10 +255,10 @@ namespace Barotrauma var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont); ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") { - Text = GameMain.Config.PlayerName, + Text = MultiplayerPreferences.Instance.PlayerName, MaxTextLength = Client.MaxNameLength, OverflowClip = true }; @@ -296,7 +269,7 @@ namespace Barotrauma } ClientNameBox.OnTextChanged += (textbox, text) => { - GameMain.Config.PlayerName = text; + MultiplayerPreferences.Instance.PlayerName = text; return true; }; @@ -366,7 +339,7 @@ namespace Barotrauma }; float elementHeight = 0.05f; - var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUI.SubHeadingFont) + var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, AutoScaleHorizontal = true, @@ -405,115 +378,79 @@ namespace Barotrauma }; filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - ternaryFilters = new Dictionary(); - filterTickBoxes = new Dictionary(); + ternaryFilters = new Dictionary(); + filterTickBoxes = new Dictionary(); - filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) + GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { - UserData = TextManager.Get("FilterSameVersion"), - Selected = true, - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterSameVersion", filterSameVersion); + text ??= TextManager.Get(key); + var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) + { + UserData = text, + Selected = defaultState, + ToolTip = addTooltip ? text : null, + OnSelected = (tickBox) => + { + FilterServers(); + StoreServerFilters(); + return true; + } + }; + filterTickBoxes.Add(key, tickBox); + return tickBox; + } - filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterPassword")) - { - UserData = TextManager.Get("FilterPassword"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterPassword", filterPassword); - - filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterIncompatibleServers")) - { - UserData = TextManager.Get("FilterIncompatibleServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterIncompatibleServers", filterIncompatible); - - filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterFullServers")) - { - UserData = TextManager.Get("FilterFullServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterFullServers", filterFull); - - filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterEmptyServers")) - { - UserData = TextManager.Get("FilterEmptyServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterEmptyServers", filterEmpty); - - filterWhitelisted = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterWhitelistedServers")) - { - UserData = TextManager.Get("FilterWhitelistedServers"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterWhitelistedServers", filterWhitelisted); - - filterOffensive = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterOffensiveServers")) - { - UserData = TextManager.Get("FilterOffensiveServers"), - ToolTip = TextManager.Get("FilterOffensiveServersToolTip"), - OnSelected = (tickBox) => { FilterServers(); return true; } - }; - filterTickBoxes.Add("FilterOffensiveServers", filterOffensive); + filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); + filterPassword = addTickBox("FilterPassword".ToIdentifier()); + filterIncompatible = addTickBox("FilterIncompatibleServers".ToIdentifier()); + filterFull = addTickBox("FilterFullServers".ToIdentifier()); + filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); + filterWhitelisted = addTickBox("FilterWhitelistedServers".ToIdentifier()); + filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); // Filter Tags - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma", (value) => { filterKarmaValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors", (value) => { filterTraitorValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire", (value) => { filterFriendlyFireValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip", (value) => { filterVoipValue = value; }); - AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded", (value) => { filterModdedValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); + AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); // Play Style Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - playStyleTickBoxes = new Dictionary(); + playStyleTickBoxes = new Dictionary(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag." + playStyle)) - { - ToolTip = TextManager.Get("servertag." + playStyle), - Selected = true, - OnSelected = (tickBox) => { FilterServers(); return true; }, - UserData = playStyle - }; - playStyleTickBoxes.Add("servertag." + playStyle, selectionTick); - filterTickBoxes.Add("servertag." + playStyle, selectionTick); + var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); + selectionTick.UserData = playStyle; + playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); } // Game mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUI.SubHeadingFont) { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; - gameModeTickBoxes = new Dictionary(); + gameModeTickBoxes = new Dictionary(); foreach (GameModePreset mode in GameModePreset.List) { - if (mode.IsSinglePlayer) continue; + if (mode.IsSinglePlayer) { continue; } - var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), mode.Name) - { - ToolTip = mode.Name, - Selected = true, - OnSelected = (tickBox) => { FilterServers(); return true; }, - UserData = mode.Identifier - }; + var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); + selectionTick.UserData = mode.Identifier; gameModeTickBoxes.Add(mode.Identifier, selectionTick); - filterTickBoxes.Add(mode.Identifier, selectionTick); } filters.Content.RectTransform.SizeChanged += () => { filters.Content.RectTransform.RecalculateChildren(true, true); - filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData as string); + filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); GUITextBlock.AutoScaleAndNormalize( @@ -543,7 +480,7 @@ namespace Barotrauma text: TextManager.Get(columnLabel[i]), textAlignment: Alignment.Center, style: "GUIButtonSmall") { ToolTip = TextManager.Get(columnLabel[i]), - ForceUpperCase = true, + ForceUpperCase = ForceUpperCase.Yes, UserData = columnLabel[i], OnClicked = SortList }; @@ -734,7 +671,7 @@ namespace Barotrauma XDocument playStylesDoc = XMLExtensions.TryLoadXml("Content/UI/Server/PlayStyles.xml"); - XElement rootElement = playStylesDoc.Root; + var rootElement = playStylesDoc.Root.FromPackage(ContentPackageManager.VanillaCorePackage); foreach (var element in rootElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) @@ -839,7 +776,7 @@ namespace Barotrauma info.LobbyID = SteamManager.CurrentLobbyID; info.IP = ip; info.Port = port; - info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? ""; + info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty; info.GameStarted = Screen.Selected != GameMain.NetLobbyScreen; info.GameVersion = GameMain.Version.ToString(); info.MaxPlayers = serverSettings.MaxPlayers; @@ -1018,21 +955,21 @@ namespace Barotrauma { base.Select(); - ContentPackagesByWorkshopId = ContentPackage.AllPackages + ContentPackagesByWorkshopId = ContentPackageManager.AllPackages .Select(p => new KeyValuePair(p.SteamWorkshopId, p)) .Where(p => p.Key != 0) .GroupBy(x => x.Key).Select(g => g.First()) .ToImmutableDictionary(); - ContentPackagesByHash = ContentPackage.AllPackages - .Select(p => new KeyValuePair(p.MD5hash.Hash, p)) + ContentPackagesByHash = ContentPackageManager.AllPackages + .Select(p => new KeyValuePair(p.Hash.StringRepresentation, p)) .GroupBy(x => x.Key).Select(g => g.First()) .ToImmutableDictionary(); SelectedTab = ServerListTab.All; - LoadServerFilters(GameMain.Config.ServerFilterElement); - if (GameSettings.ShowOffensiveServerPrompt) + GameMain.ServerListScreen.LoadServerFilters(); + if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) { - var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => { filterOffensive.Selected = true; @@ -1040,7 +977,10 @@ namespace Barotrauma return true; }; filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; - GameSettings.ShowOffensiveServerPrompt = false; + + var config = GameSettings.CurrentConfig; + config.ShowOffensiveServerPrompt = false; + GameSettings.SetCurrentConfig(config); } Steamworks.SteamMatchmaking.ResetActions(); @@ -1060,10 +1000,7 @@ namespace Barotrauma ContentPackagesByHash = ImmutableDictionary.Empty; base.Deselect(); - GameMain.Config.SaveNewPlayerConfig(); - - pendingWorkshopDownloads?.Clear(); - workshopDownloadsFrame = null; + GameSettings.SaveCurrentConfig(); } public override void Update(double deltaTime) @@ -1083,62 +1020,6 @@ namespace Barotrauma friendsDropdown.Visible = false; } } - - if (currentlyDownloadingWorkshopItem == null) - { - if (pendingWorkshopDownloads?.Any() ?? false) - { - Steamworks.Ugc.Item? item = pendingWorkshopDownloads.Values.FirstOrDefault(it => it.Item != null).Item; - if (item != null) - { - ulong itemId = item.Value.Id; - currentlyDownloadingWorkshopItem = item; - SteamManager.ForceRedownload(item.Value.Id, () => - { - if (!(item?.IsSubscribed ?? false)) - { - TaskPool.Add("SubscribeToServerMod", item?.Subscribe(), (t) => { }); - } - PendingWorkshopDownload clearedDownload = pendingWorkshopDownloads[itemId]; - pendingWorkshopDownloads.Remove(itemId); - currentlyDownloadingWorkshopItem = null; - - void onInstall(ContentPackage resultingPackage) - { - if (!resultingPackage.MD5hash.Hash.Equals(clearedDownload.ExpectedHash)) - { - workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Red); - CancelWorkshopDownloads(); - GameMain.Client?.Disconnect(); - GameMain.Client = null; - new GUIMessageBox( - TextManager.Get("ConnectionLost"), - TextManager.GetWithVariable("DisconnectMessage.MismatchedWorkshopMod", "[incompatiblecontentpackage]", $"\"{resultingPackage.Name}\" (hash {resultingPackage.MD5hash.ShortHash})")); - } - } - - if (SteamManager.CheckWorkshopItemInstalled(item)) - { - SteamManager.UninstallWorkshopItem(item, false, out _); - } - if (SteamManager.InstallWorkshopItem(item, out string errorMsg, enableContentPackage: false, suppressInstallNotif: true, onInstall: onInstall)) - { - workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Green); - } - else - { - workshopDownloadsFrame?.FindChild((c) => c.UserData is ulong l && l == itemId, true)?.Flash(GUI.Style.Red); - DebugConsole.ThrowError(errorMsg); - } - }); - } - } - else if (!string.IsNullOrEmpty(autoConnectEndpoint)) - { - JoinServer(autoConnectEndpoint, autoConnectName); - autoConnectEndpoint = null; - } - } } private void FilterServers() @@ -1207,8 +1088,8 @@ namespace Barotrauma foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { - var gameMode = (string)tickBox.UserData; - if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) + var gameMode = (Identifier)tickBox.UserData; + if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode == gameMode) { child.Visible = false; break; @@ -1253,7 +1134,7 @@ namespace Barotrauma private void ShowDirectJoinPrompt() { var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", - new string[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, + new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); msgBox.Content.ChildAnchor = Anchor.TopCenter; @@ -1597,7 +1478,7 @@ namespace Barotrauma { friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") { - Font = GUI.GlobalFont, + Font = GUIStyle.GlobalFont, OnClicked = (button, udt) => { friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); @@ -1680,10 +1561,10 @@ namespace Barotrauma var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; - if (friend.PlayingThisGame) { textBlock.TextColor = GUI.Style.Green; } - if (friend.PlayingAnotherGame) { textBlock.TextColor = GUI.Style.Blue; } + if (friend.PlayingThisGame) { textBlock.TextColor = GUIStyle.Green; } + if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } if (friend.InServer) { @@ -1744,7 +1625,7 @@ namespace Barotrauma } recentServers.Concat(favoriteServers).ForEach(si => si.OwnerVerified = false); - if (GameMain.Config.UseSteamMatchmaking) + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { serverList.ClearChildren(); if (!SteamManager.GetServers(AddToServerList, ServerQueryFinished)) @@ -1768,11 +1649,6 @@ namespace Barotrauma scanServersButton.Enabled = true; } } - else - { - CoroutineManager.StartCoroutine(SendMasterServerRequest()); - waitingForRefresh = false; - } refreshDisableTimer = DateTime.Now + AllowedRefreshInterval; @@ -1998,14 +1874,14 @@ namespace Barotrauma if (serverInfo.LobbyID == 0 && (string.IsNullOrWhiteSpace(serverInfo.IP) || string.IsNullOrWhiteSpace(serverInfo.Port))) { - string toolTip = TextManager.Get("ServerOffline"); + LocalizedString toolTip = TextManager.Get("ServerOffline"); serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } - else if (GameMain.Config.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) + else if (GameSettings.CurrentConfig.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) { - string toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); + LocalizedString toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); compatibleBox.Selected = false; serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.8f; @@ -2014,7 +1890,7 @@ namespace Barotrauma else if (string.IsNullOrEmpty(serverInfo.GameVersion) || !serverInfo.ContentPackageHashes.Any()) { compatibleBox.Selected = false; - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUI.Style.Orange * 0.85f, textAlignment: Alignment.Center) + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) { ToolTip = TextManager.Get(string.IsNullOrEmpty(serverInfo.GameVersion) ? "ServerListUnknownVersion" : @@ -2023,27 +1899,30 @@ namespace Barotrauma } else if (!compatibleBox.Selected) { - string toolTip = ""; + LocalizedString toolTip = ""; if (serverInfo.GameVersion != GameMain.Version.ToString()) + { toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion); + } for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { bool listAsIncompatible = false; if (serverInfo.ContentPackageWorkshopIds[i] == 0) { - listAsIncompatible = !GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i]); + listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i]); } else { - listAsIncompatible = GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash != serverInfo.ContentPackageHashes[i] && - cp.SteamWorkshopId == serverInfo.ContentPackageWorkshopIds[i]); + listAsIncompatible = ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation != serverInfo.ContentPackageHashes[i] && + contentPackage.SteamWorkshopId == serverInfo.ContentPackageWorkshopIds[i]); } if (listAsIncompatible) { if (toolTip != "") toolTip += "\n"; - toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", new string[2] { "[contentpackage]", "[hash]" }, - new string[2] { serverInfo.ContentPackageNames[i], Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]) }); + toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", + ("[contentpackage]", serverInfo.ContentPackageNames[i]), + ("[hash]", Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]))); } } @@ -2054,12 +1933,12 @@ namespace Barotrauma } else { - string toolTip = ""; + LocalizedString toolTip = ""; for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { - if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i])) + if (!ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i])) { - if (toolTip != "") toolTip += "\n"; + if (toolTip != "") { toolTip += "\n"; } toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", serverInfo.ContentPackageNames[i]); break; } @@ -2116,163 +1995,12 @@ namespace Barotrauma waitingForRefresh = false; } - private IEnumerable SendMasterServerRequest() - { - RestClient client = null; - try - { - client = new RestClient(NetConfig.MasterServerUrl); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error while connecting to master server", e); - } - - if (client == null) yield return CoroutineStatus.Success; - - var request = new RestRequest("masterserver2.php", Method.GET); - request.AddParameter("gamename", "barotrauma"); - request.AddParameter("action", "listservers"); - - // execute the request - masterServerResponded = false; - var restRequestHandle = client.ExecuteAsync(request, response => MasterServerCallBack(response)); - - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 8); - while (!masterServerResponded) - { - if (DateTime.Now > timeOut) - { - serverList.ClearChildren(); - restRequestHandle.Abort(); - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.Get("MasterServerTimeOutError")); - yield return CoroutineStatus.Success; - } - yield return CoroutineStatus.Running; - } - - if (masterServerResponse.ErrorException != null) - { - serverList.ClearChildren(); - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariable("MasterServerErrorException", "[error]", masterServerResponse.ErrorException.ToString())); - } - else if (masterServerResponse.StatusCode != HttpStatusCode.OK) - { - serverList.ClearChildren(); - - switch (masterServerResponse.StatusCode) - { - case HttpStatusCode.NotFound: - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), - TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", NetConfig.MasterServerUrl)); - break; - case HttpStatusCode.ServiceUnavailable: - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), - TextManager.Get("MasterServerErrorUnavailable")); - break; - default: - new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), - TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" }, - new string[2] { masterServerResponse.StatusCode.ToString(), masterServerResponse.StatusDescription })); - break; - } - - } - else - { - UpdateServerList(masterServerResponse.Content); - } - - yield return CoroutineStatus.Success; - - } - private void MasterServerCallBack(IRestResponse response) { masterServerResponse = response; masterServerResponded = true; } - public void DownloadWorkshopItems(IEnumerable downloads, string serverName, string endPointString) - { - if (workshopDownloadsFrame != null) { return; } - int rowCount = downloads.Count() + 2; - - autoConnectName = serverName; autoConnectEndpoint = endPointString; - - workshopDownloadsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), null, Color.Black * 0.5f); - currentlyDownloadingWorkshopItem = null; - pendingWorkshopDownloads = new Dictionary(); - - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f + 0.03f * rowCount), workshopDownloadsFrame.RectTransform, Anchor.Center, Pivot.Center)); - var innerLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, (float)rowCount / (float)(rowCount + 3)), innerFrame.RectTransform, Anchor.Center, Pivot.Center)); - - foreach (PendingWorkshopDownload entry in downloads) - { - pendingWorkshopDownloads.Add(entry.Id, entry); - - var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f / rowCount), innerLayout.RectTransform), true, Anchor.CenterLeft) - { - UserData = entry.Id - }; - TaskPool.Add("RetrieveWorkshopItemData", Steamworks.SteamUGC.QueryFileAsync(entry.Id), (t) => - { - if (t.IsFaulted) - { - TaskPool.PrintTaskExceptions(t, $"Failed to retrieve Workshop item info (ID {entry.Id})"); - return; - } - t.TryGetResult(out Steamworks.Ugc.Item? item); - - if (!item.HasValue) - { - DebugConsole.ThrowError($"Failed to find a Steam Workshop item with the ID {entry.Id}."); - return; - } - - if (pendingWorkshopDownloads.ContainsKey(entry.Id)) - { - pendingWorkshopDownloads[entry.Id] = new PendingWorkshopDownload(entry.ExpectedHash, item.Value); - - new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.67f), itemLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), item.Value.Title); - - new GUIProgressBar(new RectTransform(new Vector2(0.6f, 0.67f), itemLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), 0f, Color.Lime) - { - ProgressGetter = () => - { - if (item.Value.IsInstalled) { return 1.0f; } - else if (!item.Value.IsDownloading) { return 0.0f; } - return item.Value.DownloadAmount; - } - }; - } - }); - } - - var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 2.0f / rowCount), innerLayout.RectTransform), true, Anchor.CenterLeft) - { - UserData = "buttons" - }; - - new GUIButton(new RectTransform(new Vector2(0.3f, 0.67f), buttonLayout.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), TextManager.Get("Cancel")) - { - OnClicked = (btn, obj) => - { - CancelWorkshopDownloads(); - return true; - } - }; - } - - public void CancelWorkshopDownloads() - { - autoConnectEndpoint = null; - autoConnectName = null; - pendingWorkshopDownloads.Clear(); - currentlyDownloadingWorkshopItem = null; - workshopDownloadsFrame = null; - } - private bool JoinServer(string endpoint, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) @@ -2283,8 +2011,8 @@ namespace Barotrauma return false; } - GameMain.Config.PlayerName = ClientNameBox.Text; - GameMain.Config.SaveNewPlayerConfig(); + MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; + GameSettings.SaveCurrentConfig(); CoroutineManager.StartCoroutine(ConnectToServer(endpoint, serverName), "ConnectToServer"); @@ -2301,7 +2029,7 @@ namespace Barotrauma try { #endif - GameMain.Client = new GameClient(GameMain.Config.PlayerName, serverIP, serverSteamID, serverName); + GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(SteamManager.GetUsername()), serverIP, serverSteamID, serverName); #if !DEBUG } catch (Exception e) @@ -2345,7 +2073,7 @@ namespace Barotrauma private Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } - return ToolBox.GradientLerp(ping / 200.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } public async Task PingServerAsync(string ip, int timeOut) @@ -2425,35 +2153,35 @@ namespace Barotrauma menu.AddToGUIUpdateList(); friendPopup?.AddToGUIUpdateList(); friendsDropdown?.AddToGUIUpdateList(); - workshopDownloadsFrame?.AddToGUIUpdateList(); } - public void SaveServerFilters(XElement element) + public void StoreServerFilters() { - element.RemoveAttributes(); - foreach (KeyValuePair filterBox in filterTickBoxes) + foreach (KeyValuePair filterBox in filterTickBoxes) { - element.Add(new XAttribute(filterBox.Key, filterBox.Value.Selected.ToString())); + ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); } - foreach (KeyValuePair ternaryFilter in ternaryFilters) + foreach (KeyValuePair ternaryFilter in ternaryFilters) { - element.Add(new XAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString())); + ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } } - public void LoadServerFilters(XElement element) + public void LoadServerFilters() { - if (element == null) { return; } - - foreach (KeyValuePair filterBox in filterTickBoxes) + XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); + ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); + foreach (KeyValuePair filterBox in filterTickBoxes) { - filterBox.Value.Selected = element.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); + filterBox.Value.Selected = + ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); } - foreach (KeyValuePair ternaryFilter in ternaryFilters) + foreach (KeyValuePair ternaryFilter in ternaryFilters) { - string valueStr = element.GetAttributeString(ternaryFilter.Key, ""); - TernaryOption ternaryOption = (TernaryOption)ternaryFilter.Value.SelectedData; - Enum.TryParse(valueStr, true, out ternaryOption); + TernaryOption ternaryOption = + ServerListFilters.Instance.GetAttributeEnum( + ternaryFilter.Key, + (TernaryOption)ternaryFilter.Value.SelectedData); var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 13936faa4..95f6f37c6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -14,7 +14,7 @@ using Barotrauma.IO; namespace Barotrauma { - class SpriteEditorScreen : Screen + class SpriteEditorScreen : EditorScreen { private GUIListBox textureList, spriteList; @@ -30,22 +30,19 @@ namespace Barotrauma private GUITextBlock texturePathText; private GUITextBlock xmlPathText; private GUIScrollBar zoomBar; - private List selectedSprites = new List(); - private List dirtySprites = new List(); - private Texture2D selectedTexture; - private Sprite lastSelected; + private readonly List selectedSprites = new List(); + private readonly List dirtySprites = new List(); + private Sprite selectedTexture; private Rectangle textureRect; private float zoom = 1; - private float minZoom = 0.25f; - private float maxZoom; - private int spriteCount; + private const float MinZoom = 0.25f, MaxZoom = 10.0f; private GUITextBox filterSpritesBox; private GUITextBlock filterSpritesLabel; private GUITextBox filterTexturesBox; private GUITextBlock filterTexturesLabel; - private string originLabel, positionLabel, sizeLabel; + private LocalizedString originLabel, positionLabel, sizeLabel; private bool editBackgroundColor; private Color backgroundColor = new Color(0.051f, 0.149f, 0.271f, 1.0f); @@ -92,8 +89,8 @@ namespace Barotrauma RefreshLists(); textureList.Select(firstSelected.Texture, autoScroll: false); selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); - texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath); - texturePathText.TextColor = GUI.Style.Green; + texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); + texturePathText.TextColor = GUIStyle.Green; return true; } }; @@ -107,7 +104,7 @@ namespace Barotrauma if (selectedTexture == null) { return false; } foreach (Sprite sprite in loadedSprites) { - if (sprite.Texture != selectedTexture) { continue; } + if (sprite.FullPath != selectedTexture.FullPath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } // Not all sprites have a sourcerect defined, in which case we'll want to use the current source rect instead of an empty rect. @@ -116,7 +113,7 @@ namespace Barotrauma } ResetWidgets(); xmlPathText.Text = TextManager.Get("spriteeditor.resetsuccessful"); - xmlPathText.TextColor = GUI.Style.Green; + xmlPathText.TextColor = GUIStyle.Green; return true; } }; @@ -153,7 +150,7 @@ namespace Barotrauma Step = 0.01f, OnMoved = (scrollBar, value) => { - zoom = MathHelper.Lerp(minZoom, maxZoom, value); + zoom = MathHelper.Lerp(MinZoom, MaxZoom, value); viewAreaOffset = Point.Zero; return true; } @@ -200,8 +197,8 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ; - filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ; + filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true); filterArea.RectTransform.MinSize = filterTexturesBox.RectTransform.MinSize; filterTexturesBox.OnTextChanged += (textBox, text) => { FilterTextures(text); return true; }; @@ -209,9 +206,9 @@ namespace Barotrauma { OnSelected = (listBox, userData) => { - var previousTexture = selectedTexture; - selectedTexture = userData as Texture2D; - if (previousTexture != selectedTexture) + var previousSprite = selectedTexture; + selectedTexture = userData as Sprite; + if (previousSprite != selectedTexture) { ResetZoom(); } @@ -219,12 +216,12 @@ namespace Barotrauma { var textBlock = (GUITextBlock)child; var sprite = (Sprite)textBlock.UserData; - textBlock.TextColor = new Color(textBlock.TextColor, sprite.Texture == selectedTexture ? 1.0f : 0.4f); - if (sprite.Texture == selectedTexture) { textBlock.Visible = true; } + textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexture.FilePath ? 1.0f : 0.4f); + if (sprite.FilePath == selectedTexture.FilePath) { textBlock.Visible = true; } } - if (selectedSprites.None(s => s.Texture == selectedTexture)) + if (selectedSprites.None(s => s.FilePath == selectedTexture.FilePath)) { - spriteList.Select(loadedSprites.First(s => s.Texture == selectedTexture), autoScroll: false); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexture.FilePath), autoScroll: false); UpdateScrollBar(spriteList); } texturePathText.TextColor = Color.LightGray; @@ -245,8 +242,8 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; - filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; + filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true); filterArea.RectTransform.MinSize = filterSpritesBox.RectTransform.MinSize; filterSpritesBox.OnTextChanged += (textBox, text) => { FilterSprites(text); return true; }; @@ -282,7 +279,7 @@ namespace Barotrauma RelativeSpacing = 0.01f }; var fields = new GUIComponent[4]; - string[] colorComponentLabels = { TextManager.Get("spriteeditor.colorcomponentr"), TextManager.Get("spriteeditor.colorcomponentg"), TextManager.Get("spriteeditor.colorcomponentb") }; + LocalizedString[] colorComponentLabels = { TextManager.Get("spriteeditor.colorcomponentr"), TextManager.Get("spriteeditor.colorcomponentg"), TextManager.Get("spriteeditor.colorcomponentb") }; for (int i = 2; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.2f, 1), inputArea.RectTransform) @@ -291,23 +288,23 @@ namespace Barotrauma MaxSize = new Point(100, 50) }, 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: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 255; - numberInput.Font = GUI.SmallFont; + numberInput.Font = GUIStyle.SmallFont; switch (i) { case 0: - colorLabel.TextColor = GUI.Style.Red; + colorLabel.TextColor = GUIStyle.Red; numberInput.IntValue = backgroundColor.R; numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)(numInput.IntValue); break; case 1: - colorLabel.TextColor = GUI.Style.Green; + colorLabel.TextColor = GUIStyle.Green; numberInput.IntValue = backgroundColor.G; numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)(numInput.IntValue); break; @@ -325,7 +322,7 @@ namespace Barotrauma { loadedSprites.ForEach(s => s.Remove()); loadedSprites.Clear(); - var contentPackages = GameMain.Config.AllEnabledPackages.ToList(); + var contentPackages = ContentPackageManager.EnabledPackages.All.ToList(); #if !DEBUG var vanilla = GameMain.VanillaContent; @@ -343,16 +340,15 @@ namespace Barotrauma XDocument doc = XMLExtensions.TryLoadXml(file.Path); if (doc != null) { - LoadSprites(doc.Root); + LoadSprites(doc.Root.FromPackage(file.Path.ContentPackage)); } } } } - void LoadSprites(XElement element) + void LoadSprites(ContentXElement element) { - string[] spriteElementNames = new string[] - { + string[] spriteElementNames = { "Sprite", "DeformableSprite", "BackgroundSprite", @@ -371,34 +367,33 @@ namespace Barotrauma foreach (string spriteElementName in spriteElementNames) { - element.Elements(spriteElementName).ForEach(s => CreateSprite(s)); - element.Elements(spriteElementName.ToLowerInvariant()).ForEach(s => CreateSprite(s)); + element.GetChildElements(spriteElementName).ForEach(s => CreateSprite(s)); } element.Elements().ForEach(e => LoadSprites(e)); } - void CreateSprite(XElement element) + void CreateSprite(ContentXElement element) { string spriteFolder = ""; - string textureElement = ""; + ContentPath texturePath = null; if (element.Attribute("texture") != null) { - textureElement = element.GetAttributeString("texture", ""); + texturePath = element.GetAttributeContentPath("texture"); } else { if (element.Name.ToString().ToLower() == "vinesprite") { - textureElement = element.Parent.GetAttributeString("vineatlas", ""); + texturePath = element.Parent.GetAttributeContentPath("vineatlas"); } } - if (string.IsNullOrEmpty(textureElement)) { return; } + if (texturePath.IsNullOrEmpty()) { return; } // TODO: parse and create? - if (textureElement.Contains("[GENDER]") || textureElement.Contains("[HEADID]") || textureElement.Contains("[RACE]") || textureElement.Contains("[VARIANT]")) { return; } - if (!textureElement.Contains("/")) + if (texturePath.Value.Contains("[GENDER]") || texturePath.Value.Contains("[HEADID]") || texturePath.Value.Contains("[RACE]") || texturePath.Value.Contains("[VARIANT]")) { return; } + if (!texturePath.Value.Contains("/")) { var parsedPath = element.ParseContentPathFromUri(); spriteFolder = Path.GetDirectoryName(parsedPath); @@ -409,7 +404,7 @@ namespace Barotrauma //{ // loadedSprites.Add(new Sprite(element, spriteFolder)); //} - loadedSprites.Add(new Sprite(element, spriteFolder, textureElement)); + loadedSprites.Add(new Sprite(element, spriteFolder, texturePath.Value, lazyLoad: true)); } } @@ -420,7 +415,7 @@ namespace Barotrauma HashSet docsToSave = new HashSet(); foreach (Sprite sprite in sprites) { - if (sprite.Texture != selectedTexture) { continue; } + if (sprite.FullPath != selectedTexture.FullPath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } element.SetAttributeValue("sourcerect", XMLExtensions.RectToString(sprite.SourceRect)); @@ -448,7 +443,7 @@ namespace Barotrauma doc.SaveSafe(xmlPath); #endif } - xmlPathText.TextColor = GUI.Style.Green; + xmlPathText.TextColor = GUIStyle.Green; return true; } #endregion @@ -478,7 +473,7 @@ namespace Barotrauma { foreach (Sprite sprite in loadedSprites) { - if (sprite.Texture != selectedTexture) continue; + if (sprite.FullPath != selectedTexture.FullPath) { continue; } if (PlayerInput.PrimaryMouseButtonClicked()) { var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); @@ -498,7 +493,7 @@ namespace Barotrauma { if (PlayerInput.ScrollWheelSpeed != 0) { - zoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, minZoom, maxZoom); + zoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, MinZoom, MaxZoom); zoomBar.BarScroll = GetBarScrollValue(); } widgets.Values.ForEach(w => w.Update((float)deltaTime)); @@ -645,17 +640,17 @@ namespace Barotrauma if (selectedTexture != null) { textureRect = new Rectangle( - (int)(viewArea.Center.X - selectedTexture.Bounds.Width / 2f * zoom), - (int)(viewArea.Center.Y - selectedTexture.Bounds.Height / 2f * zoom), - (int)(selectedTexture.Bounds.Width * zoom), - (int)(selectedTexture.Bounds.Height * zoom)); + (int)(viewArea.Center.X - selectedTexture.Texture.Bounds.Width / 2f * zoom), + (int)(viewArea.Center.Y - selectedTexture.Texture.Bounds.Height / 2f * zoom), + (int)(selectedTexture.Texture.Bounds.Width * zoom), + (int)(selectedTexture.Texture.Bounds.Height * zoom)); - spriteBatch.Draw(selectedTexture, + spriteBatch.Draw(selectedTexture.Texture, viewArea.Center.ToVector2(), sourceRectangle: null, color: Color.White, rotation: 0.0f, - origin: new Vector2(selectedTexture.Bounds.Width / 2.0f, selectedTexture.Bounds.Height / 2.0f), + origin: new Vector2(selectedTexture.Texture.Bounds.Width / 2.0f, selectedTexture.Texture.Bounds.Height / 2.0f), scale: zoom, effects: SpriteEffects.None, layerDepth: 0); @@ -670,10 +665,8 @@ namespace Barotrauma foreach (GUIComponent element in spriteList.Content.Children) { - Sprite sprite = element.UserData as Sprite; - if (sprite == null) { continue; } - if (sprite.Texture != selectedTexture) continue; - spriteCount++; + if (!(element.UserData is Sprite sprite)) { continue; } + if (sprite.FullPath != selectedTexture.FullPath) { continue; } Rectangle sourceRect = new Rectangle( textureRect.X + (int)(sprite.SourceRect.X * zoom), @@ -682,10 +675,10 @@ namespace Barotrauma (int)(sprite.SourceRect.Height * zoom)); bool isSelected = selectedSprites.Contains(sprite); - GUI.DrawRectangle(spriteBatch, sourceRect, isSelected ? GUI.Style.Orange : GUI.Style.Red * 0.5f, thickness: isSelected ? 2 : 1); + GUI.DrawRectangle(spriteBatch, sourceRect, isSelected ? GUIStyle.Orange : GUIStyle.Red * 0.5f, thickness: isSelected ? 2 : 1); - string id = sprite.ID; - if (!string.IsNullOrEmpty(id)) + Identifier id = sprite.Identifier; + if (!id.IsEmpty) { int widgetSize = 10; Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2(); @@ -759,8 +752,6 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); - spriteCount = 0; - spriteBatch.End(); } @@ -829,7 +820,7 @@ namespace Barotrauma foreach (GUIComponent child in textureList.Content.Children) { if (!(child is GUITextBlock textBlock)) { continue; } - textBlock.Visible = textBlock.Text.ToLower().Contains(text); + textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase); } } private void FilterSprites(string text) @@ -845,7 +836,7 @@ namespace Barotrauma foreach (GUIComponent child in spriteList.Content.Children) { if (!(child is GUITextBlock textBlock)) { continue; } - textBlock.Visible = textBlock.Text.ToLower().Contains(text); + textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase); } } @@ -854,25 +845,7 @@ namespace Barotrauma base.Select(); LoadSprites(); RefreshLists(); - // Store the reference, because lastSelected is reassigned when the texture is selected. - Sprite lastSprite = lastSelected; - // Select the last selected texture if any. - // TODO: Does not work if the texture has been disposed. This happens when it's not used by any sprite -> is there a better way to identify the textures? id or something? - if (selectedTexture != null && textureList.Content.Children.Any(c => c.UserData as Texture2D == selectedTexture)) - { - textureList.Select(selectedTexture, autoScroll: false); - UpdateScrollBar(textureList); - // Select the last selected sprite if any - if (lastSprite != null && spriteList.Content.Children.FirstOrDefault(c => c.UserData is Sprite s && s.ID == lastSprite.ID)?.UserData is Sprite sprite) - { - spriteList.Select(sprite, autoScroll: false); - UpdateScrollBar(spriteList); - } - } - else - { - spriteList.Select(0, autoScroll: false); - } + spriteList.Select(0, autoScroll: false); } public override void Deselect() @@ -887,7 +860,7 @@ namespace Barotrauma { foreach (var s in Sprite.LoadedSprites) { - if (s.Texture == sprite.Texture && !reloadedSprites.Contains(s)) + if (s.FullPath == sprite.FullPath && !reloadedSprites.Contains(s)) { s.ReloadXML(); reloadedSprites.Add(s); @@ -907,7 +880,7 @@ namespace Barotrauma RefreshLists(); } - if (selectedSprites.Any(s => s.Texture != selectedTexture)) + if (selectedSprites.Any(s => s.FullPath != selectedTexture.FullPath)) { ResetWidgets(); } @@ -921,7 +894,6 @@ namespace Barotrauma { selectedSprites.Add(sprite); dirtySprites.Add(sprite); - lastSelected = sprite; } } else @@ -929,9 +901,8 @@ namespace Barotrauma selectedSprites.Clear(); selectedSprites.Add(sprite); dirtySprites.Add(sprite); - lastSelected = sprite; } - if (selectedTexture != sprite.Texture) + if (selectedTexture?.FullPath != sprite.FullPath) { textureList.Select(sprite.Texture, autoScroll: false); UpdateScrollBar(textureList); @@ -939,7 +910,7 @@ namespace Barotrauma xmlPathText.Text = string.Empty; foreach (var s in selectedSprites) { - texturePathText.Text = s.FilePath; + texturePathText.Text = s.FilePath.Value; var element = s.SourceElement; if (element != null) { @@ -962,18 +933,18 @@ namespace Barotrauma ResetWidgets(); HashSet textures = new HashSet(); // Create texture list - foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath))) + foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath.Value))) { //ignore sprites that don't have a file path (e.g. submarine pics) - if (string.IsNullOrEmpty(sprite.FilePath)) continue; - string normalizedFilePath = Path.GetFullPath(sprite.FilePath); + if (sprite.FilePath.IsNullOrEmpty()) continue; + string normalizedFilePath = sprite.FilePath.FullPath; if (!textures.Contains(normalizedFilePath)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textureList.Content.RectTransform) { MinSize = new Point(0, 20) }, - Path.GetFileName(sprite.FilePath)) + Path.GetFileName(sprite.FilePath.Value)) { - ToolTip = sprite.FilePath, - UserData = sprite.Texture + ToolTip = sprite.FilePath.Value, + UserData = sprite }; textures.Add(normalizedFilePath); } @@ -981,7 +952,7 @@ namespace Barotrauma // Create sprite list // TODO: allow the user to choose whether to sort by file name or by texture sheet //foreach (Sprite sprite in loadedSprites.OrderBy(s => GetSpriteName(s))) - foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeString("texture", string.Empty))) + foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeContentPath("texture")?.Value ?? string.Empty)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), spriteList.Content.RectTransform) { MinSize = new Point(0, 20) }, GetSpriteName(sprite) + " (" + sprite.SourceRect.X + ", " + sprite.SourceRect.Y + ", " + sprite.SourceRect.Width + ", " + sprite.SourceRect.Height + ")") @@ -996,9 +967,8 @@ namespace Barotrauma { if (selectedTexture == null) { return; } var viewArea = GetViewArea; - float width = viewArea.Width / (float)selectedTexture.Width; - float height = viewArea.Height / (float)selectedTexture.Height; - maxZoom = 10; // TODO: user-definable? + float width = viewArea.Width / (float)selectedTexture.Texture.Width; + float height = viewArea.Height / (float)selectedTexture.Texture.Height; zoom = Math.Min(1, Math.Min(width, height)); zoomBar.BarScroll = GetBarScrollValue(); viewAreaOffset = Point.Zero; @@ -1017,7 +987,7 @@ namespace Barotrauma } } - private float GetBarScrollValue() => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(minZoom, maxZoom, zoom)); + private float GetBarScrollValue() => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(MinZoom, MaxZoom, zoom)); private string GetSpriteName(Sprite sprite) { @@ -1032,7 +1002,7 @@ namespace Barotrauma { name = sourceElement.Parent.GetAttributeString("name", string.Empty); } - return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath) : name; + return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath.Value) : name; } private void UpdateScrollBar(GUIListBox listBox) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs deleted file mode 100644 index 60cdf08a0..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ /dev/null @@ -1,1923 +0,0 @@ -using Barotrauma.IO; -using Barotrauma.Steam; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using RestSharp; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Barotrauma -{ - class SteamWorkshopScreen : Screen - { - private GUIFrame menu; - private GUIListBox subscribedItemList, topItemList; - private GUITextBox subscribedItemFilter, topItemFilter; - - private GUIListBox publishedItemList, myItemList; - - //shows information of a selected workshop item - private GUIFrame modsPreviewFrame, browsePreviewFrame; - - //menu for creating new items - private GUIFrame createItemFrame; - //listbox that shows the files included in the item being created - private GUIListBox createItemFileList; - - private GUIImage previewIcon; - - private System.IO.FileSystemWatcher createItemWatcher; - - private readonly List tabButtons = new List(); - - private class PendingPreviewImageDownload - { - /// - /// Was the image downloaded - /// - public bool Downloaded = false; - - /// - /// How many tasks are looking to create a preview image based on this download - /// - public int PendingLoads = 1; - } - private readonly Dictionary pendingPreviewImageDownloads = new Dictionary(); - private readonly Dictionary itemPreviewSprites = new Dictionary(); - - private enum Tab - { - Mods, - Browse, - Publish - } - - private GUIComponent[] tabs; - - private ContentPackage itemContentPackage; - private Steamworks.Ugc.Editor? itemEditor; - - private enum VisibilityType - { - Public, - FriendsOnly, - Private - } - - public SteamWorkshopScreen() - { - GameMain.Instance.ResolutionChanged += CreateUI; - CreateUI(); - - Steamworks.SteamUGC.GlobalOnItemInstalled += OnItemInstalled; - } - - private void CreateUI() - { - tabs = new GUIComponent[Enum.GetValues(typeof(Tab)).Length]; - menu = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); - - var container = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), menu.RectTransform, Anchor.Center)) { Stretch = true }; - var topButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), container.RectTransform), isHorizontal: true); - - foreach (Tab tab in Enum.GetValues(typeof(Tab))) - { - GUIButton tabButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), topButtonContainer.RectTransform), - TextManager.Get(tab.ToString() + "Tab"), style: "GUITabButton") - { - UserData = tab, - OnClicked = (btn, userData) => - { - SelectTab((Tab)userData); return true; - } - }; - tabButtons.Add(tabButton); - } - topButtonContainer.RectTransform.MinSize = new Point(0, topButtonContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - topButtonContainer.RectTransform.MaxSize = new Point(int.MaxValue, topButtonContainer.RectTransform.MinSize.Y); - - var tabContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.7f), container.RectTransform), style: "InnerFrame"); - - var bottomButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), container.RectTransform), isHorizontal: true); - GUIButton backButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.9f), bottomButtonContainer.RectTransform) { MinSize = new Point(150, 0) }, - TextManager.Get("Back")) - { - OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu - }; - backButton.SelectedColor = backButton.Color; - topButtonContainer.RectTransform.MinSize = new Point(0, backButton.RectTransform.MinSize.Y); - topButtonContainer.RectTransform.MaxSize = new Point(int.MaxValue, backButton.RectTransform.MinSize.Y); - - //------------------------------------------------------------------------------- - //Subscribed Mods tab - //------------------------------------------------------------------------------- - - tabs[(int)Tab.Mods] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var modsContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Mods].RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - subscribedItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), modsContainer.RectTransform)) - { - ScrollBarVisible = true, - OnSelected = (GUIComponent component, object userdata) => - { - if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } - ShowItemPreview(userdata as Steamworks.Ugc.Item?, modsPreviewFrame); - return true; - } - }; - - subscribedItemFilter = CreateFilterBox(modsContainer, subscribedItemList); - - modsPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Mods].RectTransform, Anchor.TopRight), style: null); - - //------------------------------------------------------------------------------- - //Popular Mods tab - //------------------------------------------------------------------------------- - - tabs[(int)Tab.Browse] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var listContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Browse].RectTransform), childAnchor: Anchor.TopCenter) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - topItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.9f), listContainer.RectTransform)) - { - ScrollBarVisible = true, - OnSelected = (GUIComponent component, object userdata) => - { - ShowItemPreview(userdata as Steamworks.Ugc.Item?, browsePreviewFrame); - return true; - } - }; - - topItemFilter = CreateFilterBox(listContainer, topItemList); - - new GUIButton(new RectTransform(new Vector2(1.0f, 0.02f), listContainer.RectTransform), TextManager.Get("FindModsButton"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - SteamManager.OverlayCustomURL("steam://url/SteamWorkshopPage/" + SteamManager.AppID); - return true; - } - }; - - browsePreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Browse].RectTransform, Anchor.TopRight), style: null); - - //------------------------------------------------------------------------------- - //Publish tab - //------------------------------------------------------------------------------- - - tabs[(int)Tab.Publish] = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.95f), tabContainer.RectTransform, Anchor.Center), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), tabs[(int)Tab.Publish].RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("PublishedWorkshopItems"), font: GUI.SubHeadingFont); - publishedItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), leftColumn.RectTransform)) - { - OnSelected = (component, userdata) => - { - if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } - if (GUI.MouseOn is GUITickBox || GUI.MouseOn?.Parent is GUITickBox) { return false; } - myItemList.Deselect(); - if (userdata is Steamworks.Ugc.Item?) - { - var item = userdata as Steamworks.Ugc.Item?; - if (!(item?.IsInstalled ?? false)) { return false; } - if (CreateWorkshopItem(item)) { ShowCreateItemFrame(); } - } - return true; - } - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), TextManager.Get("YourWorkshopItems"), font: GUI.SubHeadingFont); - myItemList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), leftColumn.RectTransform)) - { - OnSelected = (component, userdata) => - { - if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } - publishedItemList.Deselect(); - if (userdata is SubmarineInfo sub) - { - CreateWorkshopItem(sub); - } - else if (userdata is ContentPackage contentPackage) - { - CreateWorkshopItem(contentPackage); - } - ShowCreateItemFrame(); - return true; - } - }; - - createItemFrame = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), tabs[(int)Tab.Publish].RectTransform, Anchor.TopRight), style: null); - - SelectTab(Tab.Mods); - - CoroutineManager.StartCoroutine(PollSubscribedItems()); - } - - private GUITextBox CreateFilterBox(GUIComponent parent, GUIListBox listbox) - { - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) - { - Stretch = true - }; - filterContainer.RectTransform.SetAsFirstChild(); - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => - { - foreach (GUIComponent child in listbox.Content.Children) - { - if (!(child.UserData is Steamworks.Ugc.Item item)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : (item.Title?.ToLower().Contains(text.ToLower()) ?? false); - } - return true; - }; - - return searchBox; - } - - public override void Select() - { - base.Select(); - - modsPreviewFrame.ClearChildren(); - browsePreviewFrame.ClearChildren(); - createItemFrame.ClearChildren(); - itemContentPackage = null; - itemEditor = null; - - SelectTab(Tab.Mods); - } - - public override void OnFileDropped(string filePath, string extension) - { - switch (extension) - { - case ".png": // workshop preview - case ".jpg": - case ".jpeg": - if (previewIcon == null || itemContentPackage == null) { break; } - - OnPreviewImageSelected(previewIcon, filePath); - break; - - default: - DebugConsole.ThrowError($"Could not drag and drop the file. \"{extension}\" is not a valid file extension! (expected .png, .jpg or .jpeg)"); - break; - } - } - - private void OnItemInstalled(ulong itemId) - { - RefreshSubscribedItems(); - } - - float subscribePollAdditionalWait = 0.0f; - - private IEnumerable PollSubscribedItems() - { - if (!SteamManager.IsInitialized) { yield return CoroutineStatus.Success; } - - uint numSubscribed = 0; - while (true) - { - while (CoroutineManager.IsCoroutineRunning("Load")) { yield return new WaitForSeconds(1.0f); } - while (subscribePollAdditionalWait > 0.01f) - { - subscribePollAdditionalWait = Math.Min(subscribePollAdditionalWait, 3.0f); - float wait = subscribePollAdditionalWait; - yield return new WaitForSeconds(wait); - subscribePollAdditionalWait -= wait; - } - uint newNumSubscribed = Steamworks.SteamUGC.NumSubscribedItems; - if (newNumSubscribed != numSubscribed) - { - RefreshSubscribedItems(); - numSubscribed = newNumSubscribed; - } - - yield return new WaitForSeconds(1.0f); - } - } - - private void SelectTab(Tab tab) - { - for (int i = 0; i < tabs.Length; i++) - { - tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; - } - - if (createItemFrame.CountChildren == 0) - { - new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.9f), createItemFrame.RectTransform, Anchor.Center), - TextManager.Get("WorkshopItemCreateHelpText"), wrap: true) - { - CanBeFocused = false - }; - } - - createItemWatcher?.Dispose(); createItemWatcher = null; - if (Screen.Selected == this) - { - switch (tab) - { - case Tab.Mods: - RefreshSubscribedItems(); - break; - case Tab.Browse: - RefreshPopularItems(); - break; - case Tab.Publish: - RefreshPublishedItems(); - break; - } - } - } - - public IEnumerable RefreshDownloadState() - { - bool isDownloading = true; - while (true) - { - SteamManager.GetSubscribedWorkshopItems((items) => - { - isDownloading = items.Any(it => it.IsDownloading || it.IsDownloadPending); - - GameMain.MainMenuScreen.SetDownloadingModsNotification(isDownloading); - }); - - if (!isDownloading) { break; } - - yield return new WaitForSeconds(0.5f); - } - yield return CoroutineStatus.Success; - } - - private void RefreshSubscribedItems() - { - SteamManager.GetSubscribedWorkshopItems((items) => - { - //filter out the items published by the player (they're shown in the publish tab) - var mySteamID = SteamManager.GetSteamID(); - OnItemsReceived(GetVisibleItems(items.Where(it => it.Owner.Id != mySteamID)), subscribedItemList); - - GameMain.MainMenuScreen.SetDownloadingModsNotification(items.Any(it => it.IsDownloading || it.IsDownloadPending)); - }); - } - - private void RefreshPopularItems() - { - SteamManager.GetPopularWorkshopItems((items) => { OnItemsReceived(GetVisibleItems(items), topItemList); }, 20); - } - - private void RefreshPublishedItems() - { - SteamManager.GetPublishedWorkshopItems((items) => { OnItemsReceived(items, publishedItemList); }); - RefreshMyItemList(); - } - - private IEnumerable GetVisibleItems(IEnumerable items) - { -#if UNSTABLE - //show everything in Unstable - return items; -#else - //hide Unstable items in normal version - return items.Where(it => !it.HasTag("unstable")); -#endif - } - - private void RefreshMyItemList() - { - myItemList.ClearChildren(); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelSubmarines"), - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - CanBeFocused = false - }; - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { - if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } - string subPath = Path.GetFullPath(sub.FilePath); - - //ignore files that are part of the vanilla content package - if (GameMain.VanillaContent != null && - GameMain.VanillaContent.Files.Any(s => Path.GetFullPath(s.Path) == subPath)) - { - continue; - } - //ignore subs that are part of a workshop content package - if (ContentPackage.AllPackages.Any(cp => cp.SteamWorkshopId != 0 && - cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) - { - continue; - } - //ignore subs that are defined in a content package with more files than just the sub - //(these will be listed in the "content packages" section) - if (ContentPackage.AllPackages.Any(cp => cp.Files.Count > 1 && - cp.Files.Any(f => f.Type == ContentType.Submarine && Path.GetFullPath(f.Path) == subPath))) - { - continue; - } - - CreateMyItemFrame(sub, myItemList); - } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), myItemList.Content.RectTransform), TextManager.Get("WorkshopLabelContentPackages"), - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - CanBeFocused = false - }; - foreach (ContentPackage contentPackage in ContentPackage.AllPackages) - { - if (contentPackage.SteamWorkshopId != 0 || contentPackage.HideInWorkshopMenu) { continue; } - if (contentPackage == GameMain.VanillaContent) { continue; } - //don't list content packages that only define one sub (they're visible in the "Submarines" section) - if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } - CreateMyItemFrame(contentPackage, myItemList); - } - } - - private void OnItemsReceived(IEnumerable itemDetails, GUIListBox listBox) - { - CrossThread.RequestExecutionOnMainThread(() => - { - listBox.ClearChildren(); - foreach (var item in itemDetails) - { - CreateWorkshopItemFrame(item, listBox); - } - - if (itemDetails.Count() == 0 && listBox == subscribedItemList) - { - new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.9f), listBox.Content.RectTransform, Anchor.Center), TextManager.Get("NoSubscribedMods"), wrap: true) - { - CanBeFocused = false - }; - } - }); - } - - private void CreateWorkshopItemFrame(Steamworks.Ugc.Item? item, GUIListBox listBox) - { - if (string.IsNullOrEmpty(item?.Title)) - { - return; - } - - string text = string.Empty; - if (listBox == subscribedItemList) - { - text = subscribedItemFilter.Text; - } - else if (listBox == topItemList) - { - text = topItemFilter.Text; - } - - bool visible = string.IsNullOrEmpty(text) || (item?.Title?.ToLower().Contains(text.ToLower()) ?? false); - - int prevIndex = -1; - var existingFrame = listBox.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); - if (existingFrame != null) - { - prevIndex = listBox.Content.GetChildIndex(existingFrame); - listBox.Content.RemoveChild(existingFrame); - } - - var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), - style: "ListBoxElement") - { - UserData = item, - Visible = visible - }; - if (prevIndex > -1) - { - itemFrame.RectTransform.RepositionChildInHierarchy(prevIndex); - } - - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) - { - CanBeFocused = false, - Stretch = true - }; - - int iconSize = innerFrame.Rect.Height; - if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) - { - new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), itemPreviewSprites[item?.PreviewImageUrl], scaleToFit: true) - { - UserData = "previewimage", - CanBeFocused = false - }; - } - else if (Screen.Selected == this) - { - new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true) - { - UserData = "previewimage", - CanBeFocused = false - }; - try - { - if (!string.IsNullOrEmpty(item?.PreviewImageUrl)) - { - string imagePreviewPath = Path.Combine(SteamManager.WorkshopItemPreviewImageFolder, item?.Id + ".png"); - - bool isNewImage; - lock (pendingPreviewImageDownloads) - { - isNewImage = !pendingPreviewImageDownloads.ContainsKey(item.Value.Id); - if (isNewImage) - { - if (File.Exists(imagePreviewPath)) - { - File.Delete(imagePreviewPath); - } - - pendingPreviewImageDownloads.Add(item.Value.Id, new PendingPreviewImageDownload()); - } - } - - if (isNewImage) - { - Directory.CreateDirectory(SteamManager.WorkshopItemPreviewImageFolder); - - Uri baseAddress = new Uri(item?.PreviewImageUrl); - Uri directory = new Uri(baseAddress, "."); // "." == current dir, like MS-DOS - string fileName = Path.GetFileName(baseAddress.LocalPath); - - IRestClient client = new RestClient(directory); - var request = new RestRequest(fileName, Method.GET); - client.ExecuteAsync(request, response => - { - OnPreviewImageDownloaded(response, imagePreviewPath, - () => - { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[item.Value.Id].Downloaded = true; - } - CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); - }); - }); - } - else - { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[item.Value.Id].PendingLoads++; - } - CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); - } - } - } - catch (Exception e) - { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads.Remove(item.Value.Id); - } - DebugConsole.ThrowError("Downloading the preview image of the Workshop item \"" + item?.Title + "\" failed.", e); - } - } - - var rightColumn = new GUILayoutGroup(new RectTransform(new Point(innerFrame.Rect.Width - iconSize, innerFrame.Rect.Height), innerFrame.RectTransform), childAnchor: Anchor.CenterLeft) - { - IsHorizontal = true, - Stretch = true, - RelativeSpacing = 0.05f, - CanBeFocused = false - }; - - var titleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), rightColumn.RectTransform), item?.Title, textAlignment: Alignment.CenterLeft, wrap: true) - { - UserData = "titletext", - CanBeFocused = false - }; - - if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false) && Directory.Exists(item?.Directory)) - { - bool installed = SteamManager.CheckWorkshopItemInstalled(item); - - if (!installed) - { - bool? compatible = SteamManager.CheckWorkshopItemCompatibility(item); - if (compatible.HasValue && !compatible.Value) - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.3f), rightColumn.RectTransform), - TextManager.Get("WorkshopItemIncompatible"), textColor: GUI.Style.Red) - { - ToolTip = TextManager.Get("WorkshopItemIncompatibleTooltip") - }; - } - else - { - installed = SteamManager.InstallWorkshopItem(item, out string errorMsg, Selected == this); - if (!installed) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - titleText.TextColor = Color.Red; - titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); - } - } - } - - if (installed) - { - bool upToDate = SteamManager.CheckWorkshopItemUpToDate(item); - - if (!upToDate) - { - if (!SteamManager.UpdateWorkshopItem(item, out string errorMsg)) - { - DebugConsole.NewMessage(errorMsg, Color.Red); - titleText.TextColor = Color.Red; - titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg }); - } - } - } - - } - else if (item?.IsDownloading ?? false) - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloading")); - } - else if (item?.IsDownloadPending ?? false) - { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloadPending")); - } - else if (!(item?.IsSubscribed ?? false) && (listBox != subscribedItemList)) - { - var downloadBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIPlusButton") - { - ToolTip = TextManager.Get("DownloadButton"), - ForceUpperCase = true, - UserData = item - }; - downloadBtn.OnClicked = (btn, userdata) => { DownloadItem(itemFrame, downloadBtn, item); return true; }; - } - - if ((item?.IsSubscribed ?? false) && listBox == subscribedItemList) - { - var reinstallBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIReloadButton") - { - ToolTip = TextManager.Get("WorkshopItemReinstall"), - ForceUpperCase = true, - UserData = "reinstall" - }; - reinstallBtn.OnClicked = (btn, userdata) => - { - var elem = subscribedItemList.Content.GetChildByUserData(item); - try - { - bool reselect = GameMain.Config.AllEnabledPackages.Any(cp => cp.SteamWorkshopId != 0 && cp.SteamWorkshopId == item?.Id); - if (!SteamManager.UninstallWorkshopItem(item, false, out string errorMsg)) - { - DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); - elem.Flash(GUI.Style.Red); - return true; - } - - SteamManager.ForceRedownload(item?.Id ?? 0, () => - { - if (!SteamManager.InstallWorkshopItem(item, out string errorMsg, reselect, true)) - { - DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); - elem.Flash(GUI.Style.Red); - } - RefreshSubscribedItems(); - }); - RefreshSubscribedItems(); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\"", e, true); - elem.Flash(GUI.Style.Red); - } - return true; - }; - reinstallBtn.Enabled = !item.Value.IsDownloading && !item.Value.IsDownloadPending; - var unsubBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIMinusButton") - { - ToolTip = TextManager.Get("WorkshopItemUnsubscribe"), - ForceUpperCase = true, - UserData = "unsubscribe" - }; - unsubBtn.OnClicked = (btn, userdata) => - { - subscribePollAdditionalWait += 1.0f; - item?.Unsubscribe(); - SteamManager.UninstallWorkshopItem(item, true, out _); - subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); - return true; - }; - } - - innerFrame.Recalculate(); - listBox.RecalculateChildren(); - } - - public void SetReinstallButtonStatus(Steamworks.Ugc.Item? item, bool enabled, Color? flashColor) - { - var child = subscribedItemList.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); - if (child != null) - { - var reinstallBtn = child.FindChild("reinstall", true); - if (reinstallBtn != null) { reinstallBtn.Enabled = enabled; } - var unsubBtn = child.FindChild("unsubscribe", true); - if (unsubBtn != null) { unsubBtn.Enabled = enabled; } - if (flashColor.HasValue) { child.Flash(flashColor); } - } - } - - private void RemoveItemFromLists(ulong itemID) - { - RemoveItemFromList(publishedItemList); - RemoveItemFromList(subscribedItemList); - RemoveItemFromList(topItemList); - - void RemoveItemFromList(GUIListBox listBox) - { - listBox.Content.RemoveChild( - listBox.Content.Children.FirstOrDefault(c => c.UserData is Steamworks.Ugc.Item? && (c.UserData as Steamworks.Ugc.Item?)?.Id == itemID)); - } - } - - private void CreateMyItemFrame(SubmarineInfo submarine, GUIListBox listBox) - { - var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), - style: "ListBoxElement") - { - UserData = submarine - }; - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.1f, - Stretch = true - }; - if (submarine.PreviewImage != null) - { - new GUIImage(new RectTransform(new Point(innerFrame.Rect.Height), innerFrame.RectTransform), submarine.PreviewImage, scaleToFit: true); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), submarine.Name, textAlignment: Alignment.CenterLeft); - } - private void CreateMyItemFrame(ContentPackage contentPackage, GUIListBox listBox) - { - var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), - style: "ListBoxElement") - { - UserData = contentPackage - }; - var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), itemFrame.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.1f, - Stretch = true - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), contentPackage.Name, textAlignment: Alignment.CenterLeft); - } - - private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath, Action action) - { - if (response.ResponseStatus == ResponseStatus.Completed) - { - TaskPool.Add("WritePreviewImageAsync", WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); - } - } - - private async Task WritePreviewImageAsync(IRestResponse response, string previewImagePath) - { - await Task.Yield(); - try - { - File.WriteAllBytes(previewImagePath, response.RawBytes); - } - catch (Exception e) - { - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsManager.ErrorSeverity.Error, "Failed to save workshop item preview image.\n" + e.Message); - return; - } - } - - private IEnumerable WaitForItemPreviewDownloaded(Steamworks.Ugc.Item? item, GUIListBox listBox, string previewImagePath) - { - while (true) - { - lock (pendingPreviewImageDownloads) - { - if (pendingPreviewImageDownloads[item.Value.Id].Downloaded){ break; } - } - - yield return new WaitForSeconds(0.2f); - } - - if (File.Exists(previewImagePath)) - { - TaskPool.Add("LoadPreviewImageAsync", LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), - new Tuple(item, listBox), - (task, tuple) => - { - //must be done in the main thread because creating/removing GUI elements is not thread-safe - CrossThread.RequestExecutionOnMainThread(() => - { - (var it, var lb) = tuple; - if (lb.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) - { - if (task.TryGetResult(out Sprite sprite)) { previewImage.Sprite = sprite; } - } - else - { - CreateWorkshopItemFrame(it, lb); - } - - if (modsPreviewFrame.FindChild(it) != null) - { - ShowItemPreview(it, modsPreviewFrame); - } - if (browsePreviewFrame.FindChild(item) != null) - { - ShowItemPreview(it, browsePreviewFrame); - } - - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; - if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } - } - }); - }); - } - - yield return CoroutineStatus.Success; - } - - private async Task LoadPreviewImageAsync(string previewImageUrl, string previewImagePath) - { - await Task.Yield(); - lock (itemPreviewSprites) - { - if (itemPreviewSprites.ContainsKey(previewImageUrl)) - { - return itemPreviewSprites[previewImageUrl]; - } - else - { - Sprite newSprite = new Sprite(previewImagePath, sourceRectangle: null); - itemPreviewSprites.Add(previewImageUrl, newSprite); - return newSprite; - } - } - } - - private bool DownloadItem(GUIComponent frame, GUIButton downloadButton, Steamworks.Ugc.Item? item) - { - if (item == null) { return false; } - - var parentElement = downloadButton.Parent; - parentElement.RemoveChild(downloadButton); - var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), parentElement.RectTransform), TextManager.Get("WorkshopItemDownloading")); - - SteamManager.SubscribeToWorkshopItem(item.Value.Id, () => - { - if (SteamManager.InstallWorkshopItem(item, out _)) - { - textBlock.Text = TextManager.Get("workshopiteminstalled"); - frame.Flash(GUI.Style.Green); - } - else - { - frame.Flash(GUI.Style.Red); - } - RefreshSubscribedItems(); - }); - - return true; - } - - private void ShowItemPreview(Steamworks.Ugc.Item? item, GUIFrame itemPreviewFrame) - { - itemPreviewFrame.ClearChildren(); - - if (item == null) { return; } - - var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), itemPreviewFrame.RectTransform, Anchor.Center)) - { - Stretch = true, - UserData = item - }; - - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), content.RectTransform)) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerArea.RectTransform), item?.Title, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont, wrap: true); - - new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.0f), headerArea.RectTransform), item?.Owner.Name, textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont); - - var btn = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), headerArea.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => - { - SteamManager.OverlayCustomURL("steam://url/CommunityFilePage/" + item?.Id); - return true; - } - }; - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.005f), content.RectTransform), style: "HorizontalLine"); - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); - - //--------------- - - var centerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) - { - new GUIImage(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), itemPreviewSprites[item?.PreviewImageUrl], scaleToFit: true); - } - else - { - new GUIImage(new RectTransform(new Vector2(0.5f, 0.0f), centerArea.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); - } - - var statsFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), centerArea.RectTransform), style: "GUIFrameListBox"); - var statsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), statsFrame.RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - //score ------------------------------------- - var scoreContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.0f), scoreContainer.RectTransform), TextManager.Get("WorkshopItemScore"), font: GUI.SubHeadingFont); - int starCount = (int)Math.Round((item?.Score ?? 0.0f) * 5); - for (int i = 0; i < 5; i++) - { - new GUIImage(new RectTransform(new Point(scoreContainer.Rect.Height), scoreContainer.RectTransform), - i < starCount ? "GUIStarIconBright" : "GUIStarIconDark"); - } - new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.0f), scoreContainer.RectTransform), - TextManager.GetWithVariable("WorkshopItemVotes", "[votecount]", (item.Value.VotesUp + item.Value.VotesDown).ToString()), - textAlignment: Alignment.CenterRight); - - //tags ------------------------------------ - - List tags = new List(); - for (int i = 0; i < item?.Tags.Length && i < 5; i++) - { - if (string.IsNullOrEmpty(item?.Tags[i])) { continue; } - string tag = TextManager.Get("Workshop.ContentTag." + item?.Tags[i].Replace(" ", ""), true); - if (string.IsNullOrEmpty(tag)) { tag = item?.Tags[i].CapitaliseFirstInvariant(); } - tags.Add(tag); - } - if (tags.Count > 0) - { - var tagContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f, - CanBeFocused = true - }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), tagContainer.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUI.SubHeadingFont); - - var t = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), tagContainer.RectTransform, Anchor.TopRight), string.Join(", ", tags), textAlignment: Alignment.CenterRight); - t.RectTransform.SizeChanged += () => - { - t.TextScale = 1.0f; - t.AutoScaleHorizontal = true; - }; - } - - var fileSize = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemFileSize"), font: GUI.SubHeadingFont); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), fileSize.RectTransform, Anchor.TopRight), MathUtils.GetBytesReadable(item?.IsInstalled ?? false ? (long)item.Value.SizeBytes : item.Value.DownloadBytesDownloaded), textAlignment: Alignment.CenterRight); - - //var dateContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), isHorizontal: true); - - var creationDate = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemCreationDate"), font: GUI.SubHeadingFont); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), creationDate.RectTransform, Anchor.CenterRight), item?.Created.ToString("dd.MM.yyyy"), textAlignment: Alignment.CenterRight); - - var modificationDate = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), statsContent.RectTransform), TextManager.Get("WorkshopItemModificationDate"), font: GUI.SubHeadingFont); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), modificationDate.RectTransform, Anchor.CenterRight), item?.Updated.ToString("dd.MM.yyyy"), textAlignment: Alignment.CenterRight); - - if (item?.IsSubscribed ?? false) - { - var buttonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), statsContent.RectTransform), style: null); - var unsubscribeButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.95f), buttonContainer.RectTransform, Anchor.Center), TextManager.Get("WorkshopItemUnsubscribe"), style: "GUIButtonSmall") - { - UserData = item, - OnClicked = (btn, userdata) => - { - subscribePollAdditionalWait += 1.0f; - item?.Unsubscribe(); - SteamManager.UninstallWorkshopItem(item, true, out _); - subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); - itemPreviewFrame.ClearChildren(); - return true; - } - }; - buttonContainer.RectTransform.MinSize = unsubscribeButton.RectTransform.MinSize; - statsContent.Recalculate(); - } - - //------------------ - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), content.RectTransform), style: null); - - var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)) { ScrollBarVisible = true }; - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform) { MinSize = new Point(0, 5) }, style: null); - - string description = item?.Description; - description = ToolBox.RemoveBBCodeTags(description); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionContainer.Content.RectTransform), description, wrap: true) - { - CanBeFocused = false - }; - } - - private void CreateWorkshopItem(SubmarineInfo sub) - { - string destinationFolder = Path.Combine("Mods", sub.Name.Trim()); - itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); - SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); - - bool fileMoved = false; - string submarineDir = Path.GetDirectoryName(sub.FilePath); - if (submarineDir != Path.GetDirectoryName(destinationFolder)) - { - string destinationPath = Path.Combine(destinationFolder, Path.GetFileName(sub.FilePath)); - if (!File.Exists(destinationPath)) - { - File.Move(sub.FilePath, destinationPath); - } - fileMoved = true; - sub.FilePath = destinationPath; - } - - itemContentPackage.AddFile(sub.FilePath, ContentType.Submarine); - itemContentPackage.Name = sub.Name; - itemContentPackage.Save(itemContentPackage.Path); - - if (fileMoved) - { - GameMain.Config.EnableRegularPackage(itemContentPackage); - } - - itemEditor = itemEditor?.WithTitle(sub.Name).WithTag("Submarine").WithDescription(sub.Description); - - if (sub.PreviewImage != null) - { - string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); - try - { - using (System.IO.Stream s = File.Create(previewImagePath)) - { - sub.PreviewImage.Texture.SaveAsPng(s, (int)sub.PreviewImage.size.X, (int)sub.PreviewImage.size.Y); - itemEditor = itemEditor?.WithPreviewFile(previewImagePath); - } - if (new FileInfo(previewImagePath).Length > 1024 * 1024) - { - new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); - itemEditor = itemEditor?.WithPreviewFile(SteamManager.DefaultPreviewImagePath); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving submarine preview image failed.", e); - itemEditor = itemEditor?.WithPreviewFile(null); - } - } - } - private void CreateWorkshopItem(ContentPackage contentPackage) - { - //SteamManager.CreateWorkshopItemStaging(new List(), out itemEditor, out itemContentPackage); - - itemContentPackage = contentPackage; - SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); - itemEditor = itemEditor?.WithTitle(contentPackage.Name); - - /*string modDirectory = ""; - foreach (ContentFile file in contentPackage.Files) - { - itemContentPackage.AddFile(file.Path, file.Type); - //if some of the content files are in a subdirectory of the Mods folder, - //assume that directory contains mod files for this package and copy them to the staging folder - if (modDirectory == "" && ContentPackage.IsModFilePathAllowed(file.Path)) - { - string directoryName = Path.GetDirectoryName(file.Path); - string[] splitPath = directoryName.Split(Path.DirectorySeparatorChar); - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - modDirectory = splitPath[1]; - } - } - } - - if (!string.IsNullOrEmpty(modDirectory)) - { - SaveUtil.CopyFolder(Path.Combine("Mods", modDirectory), Path.Combine(SteamManager.WorkshopItemStagingFolder, "Mods", modDirectory), copySubDirs: true); - }*/ - - } - - private bool CreateWorkshopItem(Steamworks.Ugc.Item? item) - { - if (!(item?.IsInstalled ?? false)) - { - new GUIMessageBox(TextManager.Get("Error"), - TextManager.GetWithVariable("WorkshopErrorInstallRequiredToEdit", "[itemname]", (item?.Title ?? "[NULL]"))); - return false; - } - if (!SteamManager.CreateWorkshopItemStaging(item, out itemEditor, out itemContentPackage)) - { - return false; - } - var tickBox = publishedItemList.Content.GetChildByUserData(item)?.GetAnyChild(); - if (tickBox != null) { tickBox.Selected = true; } - return true; - } - - private void ShowCreateItemFrame() - { - createItemFrame.ClearChildren(); - - if (itemEditor == null) { return; } - - if (itemContentPackage == null) - { - string errorMsg = "Failed to edit workshop item (content package null)\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.ShowCreateItemFrame:ContentPackageNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return; - } - - var createItemContent = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), createItemFrame.RectTransform, Anchor.Center)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - var topPanel = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), createItemContent.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.01f - }; - - var topLeftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), topPanel.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - var topRightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), topPanel.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.02f - }; - - // top right column -------------------------------------------------------------------------------------- - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemTitle"), font: GUI.SubHeadingFont); - var titleBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.15f), topRightColumn.RectTransform), itemEditor?.Title); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemDescription"), font: GUI.SubHeadingFont); - - var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), topRightColumn.RectTransform)); - var descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform), itemEditor?.Description, - textAlignment: Alignment.TopLeft, style: "GUITextBoxNoBorder", font: GUI.SmallFont, wrap: true); - descriptionBox.OnTextChanged += (textBox, text) => - { - Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText); - textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10)); - descriptionContainer.UpdateScrollBarSize(); - descriptionContainer.BarScroll = 1.0f; - itemEditor = itemEditor?.WithDescription(text); - return true; - }; - descriptionContainer.RectTransform.SizeChanged += () => { descriptionBox.Text = descriptionBox.Text; }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topRightColumn.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUI.SubHeadingFont); - var tagHolder = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.17f), topRightColumn.RectTransform) { MinSize = new Point(0, 50) }, isHorizontal: true) - { - Spacing = 5 - }; - - HashSet availableTags = new HashSet(); - foreach (string tag in itemEditor?.Tags ?? Enumerable.Empty()) - { - if (!string.IsNullOrEmpty(tag)) { availableTags.Add(tag.ToLowerInvariant()); } - } - foreach (string tag in SteamManager.PopularTags) - { - if (!string.IsNullOrEmpty(tag)) { availableTags.Add(tag.ToLowerInvariant()); } - if (availableTags.Count > 10) { break; } - } - - foreach (string tag in availableTags) - { - var tagBtn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tagHolder.Content.RectTransform, anchor: Anchor.CenterLeft), - tag.CapitaliseFirstInvariant(), style: "GUIButtonRound"); - tagBtn.TextBlock.AutoScaleHorizontal = true; - tagBtn.Selected = itemEditor?.Tags?.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)) ?? false; - - tagBtn.OnClicked = (btn, userdata) => - { - if (!tagBtn.Selected) - { - if (!(itemEditor?.Tags?.Any(t => t.ToLowerInvariant() == tag) ?? false)) { itemEditor = itemEditor?.WithTag(tagBtn.Text); } - tagBtn.Selected = true; - } - else - { - itemEditor?.Tags?.RemoveAll(t => t.Equals(tagBtn.Text, StringComparison.OrdinalIgnoreCase)); - tagBtn.Selected = false; - } - return true; - }; - } - tagHolder.UpdateScrollBarSize(); - - // top left column -------------------------------------------------------------------------------------- - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemPreviewImage"), font: GUI.SubHeadingFont); - - previewIcon = new GUIImage(new RectTransform(new Vector2(1.0f, 0.7f), topLeftColumn.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true); - new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - FileSelection.OnFileSelected = (file) => - { - OnPreviewImageSelected(previewIcon, file); - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("PNG", "*.png"); - FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.png"); - FileSelection.Open = true; - return true; - } - }; - - //if preview image has not been set, but there's a PreviewImage file inside the mod folder, use that by default - if (string.IsNullOrEmpty(itemEditor?.PreviewFile)) - { - string previewImagePath = Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName); - if (File.Exists(previewImagePath)) - { - itemEditor = itemEditor?.WithPreviewFile(Path.GetFullPath(previewImagePath)); - } - } - if (!string.IsNullOrEmpty(itemEditor?.PreviewFile)) - { - itemEditor = itemEditor?.WithPreviewFile(Path.GetFullPath(itemEditor?.PreviewFile)); - if (itemPreviewSprites.ContainsKey(itemEditor?.PreviewFile)) - { - itemPreviewSprites[itemEditor?.PreviewFile].Remove(); - } - var newPreviewImage = new Sprite(itemEditor?.PreviewFile, sourceRectangle: null); - previewIcon.Sprite = newPreviewImage; - itemPreviewSprites[itemEditor?.PreviewFile] = newPreviewImage; - } - - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), topLeftColumn.RectTransform), TextManager.Get("WorkshopItemCorePackage")) - { - ToolTip = TextManager.Get("WorkshopItemCorePackageTooltip"), - Selected = itemContentPackage.IsCorePackage, - OnSelected = (tickbox) => - { - if (tickbox.Selected) - { - if (!itemContentPackage.ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("ContentPackageCantMakeCorePackage", new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { itemContentPackage.Name, string.Join(", ", missingContentTypes) }, new bool[2] { false, true })); - tickbox.Selected = false; - } - else - { - itemContentPackage.IsCorePackage = tickbox.Selected; - } - } - else - { - itemContentPackage.IsCorePackage = false; - } - return true; - } - }; - - // file list -------------------------------------------------------------------------------------- - - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), createItemContent.RectTransform), style: null); - - var fileListTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), TextManager.Get("WorkshopItemFiles"), font: GUI.SubHeadingFont); - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), fileListTitle.RectTransform, Anchor.CenterRight), TextManager.Get("WorkshopItemShowFolder"), style: "GUIButtonSmall") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { ToolBox.OpenFileWithShell(Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path))); return true; } - }; - createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); - createItemWatcher?.Dispose(); - createItemWatcher = new System.IO.FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) - { - Filter = "*", - NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName - }; - createItemWatcher.Created += OnFileSystemChanges; - createItemWatcher.Deleted += OnFileSystemChanges; - createItemWatcher.Renamed += OnFileSystemChanges; - createItemWatcher.EnableRaisingEvents = true; - RefreshCreateItemFileList(); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.02f - }; - - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemRefreshFileList"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("WorkshopItemRefreshFileListTooltip"), - OnClicked = (btn, userdata) => - { - itemContentPackage = new ContentPackage(itemContentPackage.Path); - RefreshCreateItemFileList(); - return true; - } - }; - new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform, Anchor.TopRight), TextManager.Get("WorkshopItemAddFiles"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - FileSelection.OnFileSelected = (file) => - { - OnAddFilesSelected(new string[] { file }); - }; - FileSelection.ClearFileTypeFilters(); - FileSelection.AddFileTypeFilter("PNG", "*.png"); - FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); - FileSelection.AddFileTypeFilter("OGG", "*.ogg"); - FileSelection.AddFileTypeFilter("XML", "*.xml"); - FileSelection.AddFileTypeFilter("TXT", "*.txt"); - FileSelection.AddFileTypeFilter("All files", "*.*"); - FileSelection.SelectFileTypeFilter("*.*"); - FileSelection.Open = true; - - return true; - } - }; - - //the item has been already published if it has a non-zero ID -> allow adding a changenote - if ((itemEditor?.FileId ?? 0) > 0) - { - var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), createItemContent.RectTransform), isHorizontal: true); - var changeNoteLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), bottomRow.RectTransform)); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), changeNoteLayout.RectTransform), TextManager.Get("WorkshopItemChangenote"), font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("WorkshopItemChangenoteTooltip") - }; - - var changenoteContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), changeNoteLayout.RectTransform)); - var changenoteBox = new GUITextBox(new RectTransform(Vector2.One, changenoteContainer.Content.RectTransform), "", - textAlignment: Alignment.TopLeft, style: "GUITextBoxNoBorder", wrap: true) - { - ToolTip = TextManager.Get("WorkshopItemChangenoteTooltip") - }; - changenoteBox.OnTextChanged += (textBox, text) => - { - Vector2 textSize = textBox.Font.MeasureString(changenoteBox.WrappedText); - textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(changenoteContainer.Content.Rect.Height, (int)textSize.Y + 10)); - changenoteContainer.UpdateScrollBarSize(); - changenoteContainer.BarScroll = 1.0f; - itemEditor = itemEditor?.WithChangeLog(text); - return true; - }; - } - - var bottomButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), createItemContent.RectTransform), - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - RelativeSpacing = 0.03f - }; - - var visibilityLabel = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), bottomButtonContainer.RectTransform), TextManager.Get("WorkshopItemVisibility"), - textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - ToolTip = TextManager.Get("WorkshopItemVisibilityTooltip") - }; - visibilityLabel.RectTransform.MaxSize = new Point((int)(visibilityLabel.TextSize.X * 1.1f), 0); - - var visibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.2f, 1.0f), bottomButtonContainer.RectTransform)); - foreach (VisibilityType visibilityType in Enum.GetValues(typeof(VisibilityType))) - { - visibilityDropDown.AddItem(TextManager.Get("WorkshopItemVisibility." + visibilityType), visibilityType); - } - visibilityDropDown.SelectItem(itemEditor.Value.IsPublic ? VisibilityType.Public : - itemEditor.Value.IsFriendsOnly ? VisibilityType.FriendsOnly : - VisibilityType.Private); - visibilityDropDown.OnSelected = (c, ud) => - { - if (!(ud is VisibilityType visibilityType)) { return false; } - switch (visibilityType) - { - case VisibilityType.Public: - itemEditor = itemEditor?.WithPublicVisibility(); - break; - case VisibilityType.FriendsOnly: - itemEditor = itemEditor?.WithFriendsOnlyVisibility(); - break; - case VisibilityType.Private: - itemEditor = itemEditor?.WithPrivateVisibility(); - break; - } - - return true; - }; - - if ((itemEditor?.FileId ?? 0) > 0) - { - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), bottomButtonContainer.RectTransform), - TextManager.Get("WorkshopItemDelete"), style: "GUIButtonSmall") - { - ToolTip = TextManager.Get("WorkshopItemDeleteTooltip"), - TextColor = GUI.Style.Red, - OnClicked = (btn, userData) => - { - if (itemEditor == null) { return false; } - var deleteVerification = new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemDeleteVerification", "[itemname]", itemEditor?.Title), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - deleteVerification.Buttons[0].OnClicked = (yesBtn, userdata) => - { - if (itemEditor == null) { return false; } - RemoveItemFromLists(itemEditor.Value.FileId); - TaskPool.Add("DeleteFileAsync", Steamworks.SteamUGC.DeleteFileAsync(itemEditor.Value.FileId), - (t) => - { - if (t.Status == TaskStatus.Faulted) - { - TaskPool.PrintTaskExceptions(t, "Failed to delete Workshop item " + (itemEditor?.Title ?? "[NULL]")); - return; - } - }); - itemEditor = null; - SelectTab(Tab.Browse); - deleteVerification.Close(); - createItemFrame.ClearChildren(); - itemContentPackage.SteamWorkshopId = 0; - itemContentPackage.Save(itemContentPackage.Path); - return true; - }; - deleteVerification.Buttons[1].OnClicked = deleteVerification.Close; - return true; - } - }; - } - var publishBtn = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomButtonContainer.RectTransform, Anchor.CenterRight), - TextManager.Get((itemEditor?.FileId ?? 0) > 0 ? "WorkshopItemUpdate" : "WorkshopItemPublish")) - { - IgnoreLayoutGroups = true, - ToolTip = TextManager.Get("WorkshopItemPublishTooltip"), - OnClicked = (btn, userData) => - { - itemEditor = itemEditor?.WithTitle(titleBox.Text); - itemEditor = itemEditor?.WithDescription(descriptionBox.Text); - if (string.IsNullOrWhiteSpace(itemEditor?.Title)) - { - titleBox.Flash(GUI.Style.Red); - return false; - } - if (string.IsNullOrWhiteSpace(itemEditor?.Description)) - { - descriptionBox.Flash(GUI.Style.Red); - return false; - } - if (createItemFileList.Content.CountChildren == 0) - { - createItemFileList.Flash(GUI.Style.Red); - } - - if (!itemContentPackage.CheckErrors(out List errorMessages)) - { - new GUIMessageBox( - TextManager.GetWithVariable("workshopitempublishfailed", "[itemname]", itemEditor?.Title), - string.Join("\n", errorMessages)); - return false; - } - - PublishWorkshopItem(); - return true; - } - }; - publishBtn.TextBlock.AutoScaleHorizontal = true; - } - - private void OnPreviewImageSelected(GUIImage previewImageElement, string filePath) - { - string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); - if (new FileInfo(filePath).Length > 1024 * 1024) - { - new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge")); - return; - } - - if (Path.GetFullPath(filePath) != previewImagePath) - { - try - { - File.Copy(filePath, previewImagePath, overwrite: true); - } - catch (System.IO.IOException e) - { - DebugConsole.ThrowError("Failed to copy the preview image \"{previewImagePath}\" to the mod folder.", e); - return; - } - } - - if (itemPreviewSprites.ContainsKey(previewImagePath)) - { - itemPreviewSprites[previewImagePath].Remove(); - } - var newPreviewImage = new Sprite(previewImagePath, sourceRectangle: null); - previewImageElement.Sprite = newPreviewImage; - itemPreviewSprites[previewImagePath] = newPreviewImage; - itemEditor?.WithPreviewFile(previewImagePath); - } - - private void OnAddFilesSelected(string[] fileNames) - { - if (fileNames == null) { return; } - for (int i = 0; i < fileNames.Length; i++) - { - string file = fileNames[i]?.Trim(); - if (string.IsNullOrEmpty(file) || !File.Exists(file)) { continue; } - - string modFolder = Path.GetDirectoryName(itemContentPackage.Path); - string filePathRelativeToModFolder = Path.GetRelativePath(Path.Combine(Environment.CurrentDirectory, modFolder), file); - - //file is not inside the mod folder, we need to move it - if (filePathRelativeToModFolder.StartsWith("..") || - Path.GetPathRoot(Environment.CurrentDirectory) != Path.GetPathRoot(file)) - { - string destinationPath = Path.Combine(modFolder, Path.GetFileName(file)); - //add a number to the filename if a file with the same name already exists - i = 2; - while (File.Exists(destinationPath)) - { - destinationPath = Path.Combine(modFolder, $"{Path.GetFileNameWithoutExtension(file)} ({i}){Path.GetExtension(file)}"); - i++; - } - try - { - File.Copy(file, destinationPath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Copying the file \"" + file + "\" to the mod folder failed.", e); - return; - } - } - } - RefreshCreateItemFileList(); - } - - volatile bool refreshFileList = false; - - private void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) - { - refreshFileList = true; - } - - private void RefreshCreateItemFileList() - { - createItemFileList.ClearChildren(); - if (itemContentPackage == null) return; - var contentTypes = Enum.GetValues(typeof(ContentType)); - - List files = itemContentPackage.FilesUnsaved.ToList(); - - for (int i = files.Count - 1; i >= 0; i--) - { - ContentFile contentFile = files[i]; - - bool fileExists = File.Exists(contentFile.Path); - - if (contentFile.Type == ContentType.ServerExecutable) - { - fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); - } - - if (!fileExists) - { - itemContentPackage.RemoveFile(contentFile); - files.RemoveAt(i); - } - } - - List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", System.IO.SearchOption.AllDirectories) - .Select(f => new ContentFile(f, ContentType.None)) - .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && - Path.GetFileName(file.Path) != SteamManager.PreviewImageName) - .ToList(); - for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), - Path.GetFullPath(file.Path).CleanUpPath(), - StringComparison.InvariantCultureIgnoreCase)); - if (otherFile != null) - { - //replace the generated ContentFile object with the one that's present in the - //content package to determine which tickboxes should already be checked - allFiles[i] = otherFile; - files.Remove(otherFile); - } - } - - allFiles.AddRange(files); - - foreach (ContentFile contentFile in allFiles) - { - bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); - bool fileExists = File.Exists(contentFile.Path); - - if (contentFile.Type == ContentType.ServerExecutable) - { - fileExists |= File.Exists(Path.GetFileNameWithoutExtension(contentFile.Path) + ".dll"); - } - - var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, - style: "ListBoxElement") - { - CanBeFocused = false, - UserData = contentFile - }; - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), fileFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - - var tickBox = new GUITickBox(new RectTransform(Vector2.One, content.RectTransform, scaleBasis: ScaleBasis.BothHeight), "") - { - Selected = itemContentPackage.FilesUnsaved.Contains(contentFile), - UserData = contentFile - }; - - tickBox.OnSelected = (tb) => - { - ContentFile f = tb.UserData as ContentFile; - if (tb.Selected) - { - if (!itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.AddFile(f); } - } - else - { - if (itemContentPackage.FilesUnsaved.Contains(f)) { itemContentPackage.RemoveFile(f); } - } - - return true; - }; - - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform, Anchor.CenterLeft), contentFile.Path, font: GUI.SmallFont) - { - ToolTip = contentFile.Path - }; - if (!fileExists) - { - nameText.TextColor = GUI.Style.Red; - tickBox.ToolTip = TextManager.Get("WorkshopItemFileNotFound"); - } - else if (illegalPath && !ContentPackage.AllPackages.Any(cp => cp.FilesUnsaved.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(contentFile.Path)))) - { - nameText.TextColor = GUI.Style.Red; - tickBox.ToolTip = TextManager.Get("WorkshopItemIllegalPath"); - } - - var contentTypeSelection = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), content.RectTransform, Anchor.CenterRight), - elementCount: contentTypes.Length) - { - UserData = contentFile, - }; - foreach (ContentType contentType in contentTypes) - { - contentTypeSelection.AddItem(contentType.ToString(), contentType); - } - contentTypeSelection.SelectItem(contentFile.Type); - - contentTypeSelection.OnSelected = (GUIComponent selected, object userdata) => - { - ((ContentFile)contentTypeSelection.UserData).Type = (ContentType)userdata; - itemContentPackage.Save(itemContentPackage.Path); - return true; - }; - - if (!files.Contains(contentFile)) //this prevents deletion of files not contained in the mod's path (i.e. vanilla content) - { - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") - { - OnClicked = (btn, userdata) => - { - var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), - TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) - { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (applyButton, obj) => - { - try - { - File.Delete(contentFile.Path); - if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); - } - //RefreshCreateItemFileList(); - RefreshMyItemList(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return true; - } - }; - } - - content.Recalculate(); - fileFrame.RectTransform.MinSize = - new Point(0, (int)(content.RectTransform.Children.Max(c => c.MinSize.Y) / content.RectTransform.RelativeSize.Y)); - nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, maxWidth: nameText.Rect.Width); - } - - itemContentPackage.Save(itemContentPackage.Path); - } - - private void PublishWorkshopItem() - { - if (itemContentPackage == null || itemEditor == null) { return; } - -#if UNSTABLE - var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("unstableworkshopitempublishwarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); - if (workshopPublishStatus != null) - { - if (!(itemEditor?.HasTag("unstable") ?? false)) { itemEditor = itemEditor?.WithTag("unstable"); } - CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); - } - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked += msgBox.Close; -#else - itemEditor = itemEditor?.WithoutTag("unstable"); - var workshopPublishStatus = SteamManager.StartPublishItem(itemContentPackage, itemEditor); - if (workshopPublishStatus == null) { return; } - CoroutineManager.StartCoroutine(WaitForPublish(workshopPublishStatus), "WaitForPublish"); -#endif - - } - - private IEnumerable WaitForPublish(SteamManager.WorkshopPublishStatus workshopPublishStatus) - { - var item = workshopPublishStatus.Item; - var coroutine = workshopPublishStatus.Coroutine; - - string pleaseWaitText = TextManager.Get("WorkshopPublishPleaseWait"); - var msgBox = new GUIMessageBox( - pleaseWaitText, - TextManager.GetWithVariable("WorkshopPublishInProgress", "[itemname]", item?.Title), - new string[] { TextManager.Get("Cancel") }); - - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - CoroutineManager.StopCoroutines("WaitForPublish"); - createItemFrame.ClearChildren(); - SelectTab(Tab.Browse); - msgBox.Close(); - return true; - }; - - yield return CoroutineStatus.Running; - while (CoroutineManager.IsCoroutineRunning(coroutine)) - { - msgBox.Header.Text = pleaseWaitText + new string('.', ((int)Timing.TotalTime % 3 + 1)); - yield return CoroutineStatus.Running; - } - msgBox.Close(); - - if (workshopPublishStatus.Success ?? false) - { - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemPublished", "[itemname]", item?.Title)); - } - else - { - string errorMsg = workshopPublishStatus.Result.HasValue ? - TextManager.GetWithVariable("WorkshopPublishError." + workshopPublishStatus.Result?.Result.ToString(), "[savepath]", SaveUtil.SaveFolder, returnNull: true) : - null; - - if (errorMsg == null) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariable("WorkshopItemPublishFailed", "[itemname]", item?.Title) + - (workshopPublishStatus?.TaskStatus != null ? - " Task ended with status " +workshopPublishStatus?.TaskStatus?.ToString() : - " Publish failed with result "+ workshopPublishStatus.Result?.Result.ToString())); - } - else - { - new GUIMessageBox(TextManager.Get("Error"), errorMsg); - } - } - - createItemFrame.ClearChildren(); - SelectTab(Tab.Browse); - } - -#region UI management - - public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) - { - graphics.Clear(Color.CornflowerBlue); - - GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); - - spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); - GUI.Draw(Cam, spriteBatch); - spriteBatch.End(); - } - - public override void AddToGUIUpdateList() - { - menu.AddToGUIUpdateList(); - } - - public override void Update(double deltaTime) - { - if (refreshFileList) - { - RefreshCreateItemFileList(); - refreshFileList = false; - } - } - -#endregion - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 6b6b947dc..09ab544ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Xml.Linq; @@ -16,12 +17,16 @@ using System.IO; using Barotrauma.IO; #endif -// ReSharper disable AccessToModifiedClosure, PossibleLossOfFraction, RedundantLambdaParameterType, UnusedVariable - namespace Barotrauma { class SubEditorScreen : EditorScreen { + private static Submarine MainSub + { + get => Submarine.MainSub; + set => Submarine.MainSub = value; + } + private enum LayerVisibility { Visible, @@ -53,13 +58,15 @@ namespace Barotrauma islinked = Linkage; } } - - private static readonly string[] crewExperienceLevels = + + #warning TODO: switch this to an enum? + private static readonly ImmutableArray crewExperienceLevels = new string[] { "CrewExperienceLow", "CrewExperienceMid", "CrewExperienceHigh" - }; + }.ToImmutableArray(); + public enum Mode { @@ -144,7 +151,7 @@ namespace Barotrauma private GUIDropDown linkedSubBox; private static GUIComponent autoSaveLabel; - private readonly static int maxAutoSaves = GameSettings.MaximumAutoSaves; + private static int maxAutoSaves => GameSettings.CurrentConfig.MaxAutoSaves; public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object(); @@ -231,22 +238,26 @@ namespace Barotrauma private static string GetSubDescription() { - string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Info.Name ?? ""), true); - if (localizedDescription != null) { return localizedDescription; } - return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Info.Description; + if (MainSub?.Info != null) + { + LocalizedString localizedDescription = TextManager.Get($"submarine.description.{MainSub.Info.Name ?? ""}"); + if (!localizedDescription.IsNullOrEmpty()) { return localizedDescription.Value; } + return MainSub.Info.Description?.Value ?? ""; + } + return ""; } - private static string GetTotalHullVolume() + private static LocalizedString GetTotalHullVolume() { - return TextManager.Get("TotalHullVolume") + ":\n" + Hull.hullList.Sum(h => h.Volume); + return $"{TextManager.Get("TotalHullVolume")}:\n{Hull.HullList.Sum(h => h.Volume)}"; } - private static string GetSelectedHullVolume() + private static LocalizedString GetSelectedHullVolume() { float buoyancyVol = 0.0f; float selectedVol = 0.0f; float neutralPercentage = SubmarineBody.NeutralBallastPercentage; - Hull.hullList.ForEach(h => + Hull.HullList.ForEach(h => { buoyancyVol += h.Volume; if (h.IsSelected) @@ -255,16 +266,16 @@ namespace Barotrauma } }); buoyancyVol *= neutralPercentage; - string retVal = TextManager.Get("SelectedHullVolume") + ":\n" + selectedVol; + string retVal = $"{TextManager.Get("SelectedHullVolume")}:\n{selectedVol}"; if (selectedVol > 0.0f && buoyancyVol > 0.0f) { if (buoyancyVol / selectedVol < 1.0f) { - retVal += " (" + TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.0000")) + ")"; + retVal += $" ({TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.0000"))})"; } else { - retVal += " (" + TextManager.Get("InsufficientBallast") + ")"; + retVal += $" ({TextManager.Get("InsufficientBallast")})"; } } return retVal; @@ -340,7 +351,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton") { - ToolTip = TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖"), OnClicked = (btn, data) => { loadFrame = null; @@ -419,7 +430,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); subNameLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.9f), paddedTopPanel.RectTransform, Anchor.CenterLeft), - TextManager.Get("unspecifiedsubfilename"), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); + TextManager.Get("unspecifiedsubfilename"), font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft); linkedSubBox = new GUIDropDown(new RectTransform(new Vector2(0.15f, 0.9f), paddedTopPanel.RectTransform), TextManager.Get("AddSubButton"), elementCount: 20) @@ -452,7 +463,7 @@ namespace Barotrauma defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton") { - ToolTip = TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖"), OnSelected = tBox => { if (!lockMode) @@ -468,7 +479,7 @@ namespace Barotrauma wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton") { - ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖"), OnSelected = tBox => { if (!lockMode) @@ -496,7 +507,7 @@ namespace Barotrauma { if (GenerateWaypoints()) { - GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green); } WayPoint.ShowWayPoints = true; generateWaypointsVerification.Close(); @@ -508,7 +519,7 @@ namespace Barotrauma { if (GenerateWaypoints()) { - GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green); } WayPoint.ShowWayPoints = true; @@ -654,9 +665,9 @@ namespace Barotrauma Color = Color.Black, Visible = false }; - new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUI.SubHeadingFont) + new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUIStyle.SubHeadingFont) { - TextColor = GUI.Style.Orange + TextColor = GUIStyle.Orange }; UpdateUndoHistoryPanel(); @@ -742,7 +753,7 @@ namespace Barotrauma }; showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); - var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUI.SubHeadingFont); + var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUIStyle.SubHeadingFont); subcategoryHeader.RectTransform.MinSize = new Point(0, (int)(subcategoryHeader.Rect.Height * 1.5f)); var subcategoryList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform) { MinSize = new Point(0, showEntitiesPanel.Rect.Height / 3) }); @@ -757,7 +768,7 @@ namespace Barotrauma foreach (string subcategory in availableSubcategories) { var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), - TextManager.Get("subcategory." + subcategory, returnNull: true) ?? subcategory, font: GUI.SmallFont) + TextManager.Get("subcategory." + subcategory).Fallback(subcategory), font: GUIStyle.SmallFont) { UserData = subcategory, Selected = !IsSubcategoryHidden(subcategory), @@ -766,7 +777,7 @@ namespace Barotrauma if (tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f) { tb.ToolTip = tb.Text; - tb.Text = ToolBox.LimitString(tb.Text, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); + tb.Text = ToolBox.LimitString(tb.Text.Value, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); } } @@ -780,7 +791,7 @@ namespace Barotrauma //----------------------------------------------- - float longestTextWidth = GUI.SmallFont.MeasureString(TextManager.Get("SubEditorShadowCastingLights")).X; + float longestTextWidth = GUIStyle.SmallFont.MeasureString(TextManager.Get("SubEditorShadowCastingLights")).X; entityCountPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) { MinSize = new Point(Math.Max(170, (int)(longestTextWidth * 1.5f)), 0), @@ -794,35 +805,35 @@ namespace Barotrauma }; var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => { - itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return Item.ItemList.Count.ToString(); }; var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => { - int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.hullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); - structureCount.TextColor = ToolBox.GradientLerp(count / 1000.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); + structureCount.TextColor = ToolBox.GradientLerp(count / 1000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return count.ToString(); }; var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); wallCount.TextGetter = () => { - wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return Structure.WallList.Count.ToString(); }; var lightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont); var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); lightCountText.TextGetter = () => { @@ -832,11 +843,11 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(); } - lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return lightCount.ToString(); }; var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), - textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, wrap: true); + textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, wrap: true); var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); shadowCastingLightCountText.TextGetter = () => { @@ -846,7 +857,7 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(l => l.CastShadows); } - shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return lightCount.ToString(); }; entityCountPanel.RectTransform.NonScaledSize = @@ -861,11 +872,11 @@ namespace Barotrauma { Visible = false }; - GUITextBlock totalHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform), "", font: GUI.SmallFont) + GUITextBlock totalHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform), "", font: GUIStyle.SmallFont) { TextGetter = GetTotalHullVolume }; - GUITextBlock selectedHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.5f) }, "", font: GUI.SmallFont) + GUITextBlock selectedHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.5f) }, "", font: GUIStyle.SmallFont) { TextGetter = GetSelectedHullVolume }; @@ -889,7 +900,7 @@ namespace Barotrauma { Visible = false }; - var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", fallBackTag: "spriteeditor.snaptogrid")); + var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", "spriteeditor.snaptogrid")); saveStampButton.TextBlock.AutoScaleHorizontal = true; saveStampButton.OnClicked += (btn, userdata) => { @@ -906,7 +917,7 @@ namespace Barotrauma toggleEntityMenuButton = new GUIButton(new RectTransform(new Vector2(0.15f, 0.08f), EntityMenu.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") { - ToolTip = TextManager.Get("EntityMenuToggleTooltip") + "‖color:125,125,125‖\nQ‖color:end‖", + ToolTip = RichString.Rich(TextManager.Get("EntityMenuToggleTooltip") + "‖color:125,125,125‖\nQ‖color:end‖"), OnClicked = (btn, userdata) => { entityMenuOpen = !entityMenuOpen; @@ -934,11 +945,11 @@ namespace Barotrauma { CanBeFocused = false }; - selectedCategoryText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("MapEntityCategory.All"), font: GUI.LargeFont); + selectedCategoryText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("MapEntityCategory.All"), font: GUIStyle.LargeFont); - var filterText = new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.SubHeadingFont); + var filterText = new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont); filterText.RectTransform.MaxSize = new Point((int)(filterText.TextSize.X * 1.5f), int.MaxValue); - entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.17f, 1.0f), entityMenuTop.RectTransform), font: GUI.Font, createClearButton: true); + entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.17f, 1.0f), entityMenuTop.RectTransform), font: GUIStyle.Font, createClearButton: true); entityFilterBox.OnTextChanged += (textBox, text) => { if (text == lastFilter) { return true; } @@ -997,9 +1008,9 @@ namespace Barotrauma private bool TestSubmarine(GUIButton button, object obj) { - List errorMsgs = new List(); + List errorMsgs = new List(); - if (!Hull.hullList.Any()) + if (!Hull.HullList.Any()) { errorMsgs.Add(TextManager.Get("NoHullsWarning")); } @@ -1011,13 +1022,13 @@ namespace Barotrauma if (errorMsgs.Any()) { - new GUIMessageBox(TextManager.Get("Error"), string.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); + new GUIMessageBox(TextManager.Get("Error"), LocalizedString.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); return true; } CloseItem(); - backedUpSubInfo = new SubmarineInfo(Submarine.MainSub); + backedUpSubInfo = new SubmarineInfo(MainSub); GameMain.GameScreen.Select(); @@ -1042,14 +1053,14 @@ namespace Barotrauma categorizedEntityList.Content.ClearChildren(); allEntityList.Content.ClearChildren(); - int maxTextWidth = (int)(GUI.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50)); + int maxTextWidth = (int)(GUIStyle.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50)); Dictionary> entityLists = new Dictionary>(); Dictionary categoryKeys = new Dictionary(); foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory))) { - string categoryName = TextManager.Get("MapEntityCategory." + category); - maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(categoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + LocalizedString categoryName = TextManager.Get("MapEntityCategory." + category); + maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(categoryName.Replace(" ", "\n")).X + GUI.IntScale(50)); foreach (MapEntityPrefab ep in MapEntityPrefab.List) { if (!ep.Category.HasFlag(category)) { continue; } @@ -1060,10 +1071,10 @@ namespace Barotrauma } entityLists[category + ep.Subcategory].Add(ep); categoryKeys[category + ep.Subcategory] = category; - string subcategoryName = TextManager.Get("subcategory." + ep.Subcategory, returnNull: true) ?? ep.Subcategory; + LocalizedString subcategoryName = TextManager.Get("subcategory." + ep.Subcategory).Fallback(ep.Subcategory); if (subcategoryName != null) { - maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(subcategoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(subcategoryName.Replace(" ", "\n")).X + GUI.IntScale(50)); } } } @@ -1080,12 +1091,12 @@ namespace Barotrauma new GUIFrame(new RectTransform(Vector2.One, categoryFrame.RectTransform), style: "HorizontalLine"); - string categoryName = TextManager.Get("MapEntityCategory." + entityLists[categoryKey].First().Category); - string subCategoryName = entityLists[categoryKey].First().Subcategory; - if (string.IsNullOrEmpty(subCategoryName)) + LocalizedString categoryName = TextManager.Get("MapEntityCategory." + entityLists[categoryKey].First().Category); + LocalizedString subCategoryName = entityLists[categoryKey].First().Subcategory; + if (subCategoryName.IsNullOrEmpty()) { new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), - categoryName, textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont, wrap: true) + categoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont, wrap: true) { Padding = new Vector4(GUI.IntScale(10)) }; @@ -1093,16 +1104,16 @@ namespace Barotrauma } else { - subCategoryName = string.IsNullOrEmpty(subCategoryName) ? + subCategoryName = subCategoryName.IsNullOrEmpty() ? TextManager.Get("mapentitycategory.misc") : - (TextManager.Get("subcategory." + subCategoryName, returnNull: true) ?? subCategoryName); + (TextManager.Get($"subcategory.{subCategoryName}").Fallback(subCategoryName)); var categoryTitle = new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), - categoryName, textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + categoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, wrap: true) { Padding = new Vector4(GUI.IntScale(10)) }; new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(0, (int)(categoryTitle.TextSize.Y + GUI.IntScale(10))) }, - subCategoryName, textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont, wrap: true) + subCategoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont, wrap: true) { Padding = new Vector4(GUI.IntScale(10)) }; @@ -1136,9 +1147,9 @@ namespace Barotrauma categoryFrame.RectTransform.MinSize = new Point(0, contentHeight); entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight); entityListInner.RectTransform.MinSize = new Point(0, contentHeight); - + entityListInner.Content.RectTransform.SortChildren((i1, i2) => - string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); + string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData)?.Name.Value, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name.Value, StringComparison.Ordinal)); } foreach (MapEntityPrefab ep in MapEntityPrefab.List) @@ -1167,14 +1178,13 @@ namespace Barotrauma frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); - string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; + frame.ToolTip = ep.Description.IsNullOrEmpty() ? name : name + '\n' + ep.Description; if (ep.ContentPackage != GameMain.VanillaContent && ep.ContentPackage != null) { frame.Color = Color.Magenta; - string colorStr = XMLExtensions.ColorToString(Color.MediumPurple); - frame.ToolTip += $"\n‖color:{colorStr}‖{ep.ContentPackage?.Name}‖color:end‖"; + frame.ToolTip = RichString.Rich($"{frame.ToolTip}\n‖color:{XMLExtensions.ToStringHex(Color.MediumPurple)}‖{ep.ContentPackage?.Name}‖color:end‖"); } if (ep.HideInMenus) { @@ -1189,7 +1199,7 @@ namespace Barotrauma CanBeFocused = false }; - Sprite icon = ep.sprite; + Sprite icon = ep.Sprite; Color iconColor = Color.White; if (ep is ItemPrefab itemPrefab) { @@ -1204,7 +1214,7 @@ namespace Barotrauma } } GUIImage img = null; - if (ep.sprite != null) + if (ep.Sprite != null) { img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) @@ -1226,16 +1236,22 @@ namespace Barotrauma }) { HideElementsOutsideFrame = true, - ToolTip = frame.RawToolTip + ToolTip = frame.ToolTip.SanitizedString }; } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) + text: name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false }; - if (legacy) textBlock.TextColor *= 0.6f; + if (legacy) { textBlock.TextColor *= 0.6f; } + if (name.IsNullOrEmpty()) + { + DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!"); + textBlock.Text = frame.ToolTip = ep.Identifier.Value; + textBlock.TextColor = GUIStyle.Red; + } textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); if (ep.Category == MapEntityCategory.ItemAssembly) @@ -1338,7 +1354,7 @@ namespace Barotrauma Submarine.Unload(); } - string name = (Submarine.MainSub == null) ? TextManager.Get("unspecifiedsubfilename") : Submarine.MainSub.Info.Name; + string name = (MainSub == null) ? TextManager.Get("unspecifiedsubfilename").Value : MainSub.Info.Name; if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); @@ -1349,21 +1365,21 @@ namespace Barotrauma if (backedUpSubInfo != null) { - Submarine.MainSub = new Submarine(backedUpSubInfo); + MainSub = new Submarine(backedUpSubInfo); if (previewImage != null && backedUpSubInfo.PreviewImage?.Texture != null && !backedUpSubInfo.PreviewImage.Texture.IsDisposed) { previewImage.Sprite = backedUpSubInfo.PreviewImage; } backedUpSubInfo = null; } - else if (Submarine.MainSub == null) + else if (MainSub == null) { var subInfo = new SubmarineInfo(); - Submarine.MainSub = new Submarine(subInfo); + MainSub = new Submarine(subInfo); } - Submarine.MainSub.UpdateTransform(interpolate: false); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + MainSub.UpdateTransform(interpolate: false); + cam.Position = MainSub.Position + MainSub.HiddenSubPosition; GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); @@ -1389,7 +1405,7 @@ namespace Barotrauma CreateDummyCharacter(); - if (GameSettings.EnableSubmarineAutoSave && enableAutoSave) + if (GameSettings.CurrentConfig.EnableSubmarineAutoSave && enableAutoSave) { CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } @@ -1397,7 +1413,7 @@ namespace Barotrauma ImageManager.OnEditorSelected(); ReconstructLayers(); - if (!GameMain.Config.EditorDisclaimerShown) + if (!GameSettings.CurrentConfig.EditorDisclaimerShown) { GameMain.Instance.ShowEditorDisclaimer(); } @@ -1416,7 +1432,7 @@ namespace Barotrauma return; } - string body = TextManager.GetWithVariable("SubEditor.LoadConfirmBody", "[submarine]", info.Name); + LocalizedString body = TextManager.GetWithVariable("SubEditor.LoadConfirmBody", "[submarine]", info.Name); GUI.AskForConfirmation(TextManager.Get("Load"), body, onConfirm: () => LoadSub(info), onDeny: () => info.Dispose()); break; @@ -1434,9 +1450,9 @@ namespace Barotrauma Texture2D texture = Sprite.LoadTexture(filePath); previewImage.Sprite = new Sprite(texture, null, null); - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + MainSub.Info.PreviewImage = previewImage.Sprite; } break; @@ -1454,7 +1470,7 @@ namespace Barotrauma /// private static IEnumerable AutoSaveCoroutine() { - DateTime target = DateTime.Now.AddMinutes(GameSettings.AutoSaveIntervalSeconds); + DateTime target = DateTime.Now.AddMinutes(GameSettings.CurrentConfig.AutoSaveIntervalSeconds); DateTime tempTarget = DateTime.Now; bool wasPaused = false; @@ -1495,7 +1511,7 @@ namespace Barotrauma TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM - SteamAchievementManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); + SteamAchievementManager.IncrementStat("hoursineditor".ToIdentifier(), (float)timeInEditor.TotalHours); #endif GUI.ForceMouseOn(null); @@ -1510,9 +1526,9 @@ namespace Barotrauma SetMode(Mode.Default); - SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume); + SoundPlayer.OverrideMusicType = Identifier.Empty; + GameMain.SoundManager.SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume); if (CoroutineManager.IsCoroutineRunning("SubEditorAutoSave")) { @@ -1541,6 +1557,10 @@ namespace Barotrauma ClearFilter(); ClearLayers(); + while (packageReloadQueue.TryDequeue(out var p)) + { + ContentPackageManager.ReloadContentPackage(p); + } } private void CreateDummyCharacter() @@ -1572,15 +1592,15 @@ namespace Barotrauma /// The saving is ran in another thread to avoid lag spikes private static void AutoSave() { - if (MapEntity.mapEntityList.Any() && GameSettings.EnableSubmarineAutoSave && !isAutoSaving) + if (MapEntity.mapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving) { - if (Submarine.MainSub != null) + if (MainSub != null) { isAutoSaving = true; if (!Directory.Exists(autoSavePath)) { return; } XDocument doc = new XDocument(new XElement("Submarine")); - Submarine.MainSub.SaveToXElement(doc.Root); + MainSub.SaveToXElement(doc.Root); Thread saveThread = new Thread(start => { try @@ -1592,13 +1612,14 @@ namespace Barotrauma CrossThread.RequestExecutionOnMainThread(() => { - if (AutoSaveInfo?.Root == null || Submarine.MainSub?.Info == null) { return; } + if (AutoSaveInfo?.Root == null || MainSub?.Info == null) { return; } int saveCount = AutoSaveInfo.Root.Elements().Count(); while (AutoSaveInfo.Root.Elements().Count() > maxAutoSaves) { XElement min = AutoSaveInfo.Root.Elements().OrderBy(element => element.GetAttributeUInt64("time", 0)).FirstOrDefault(); - string path = min.GetAttributeString("file", ""); + #warning TODO: revise + string path = min.GetAttributeStringUnrestricted("file", ""); if (string.IsNullOrWhiteSpace(path)) { continue; } if (IO.File.Exists(path)) { IO.File.Delete(path); } @@ -1607,7 +1628,7 @@ namespace Barotrauma XElement newElement = new XElement("AutoSave", new XAttribute("file", filePath), - new XAttribute("name", Submarine.MainSub.Info.Name), + new XAttribute("name", MainSub.Info.Name), new XAttribute("time", (ulong)time.TotalSeconds)); AutoSaveInfo.Root.Add(newElement); @@ -1640,7 +1661,7 @@ namespace Barotrauma if (Selected != GameMain.SubEditorScreen) { return; } autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); - string label = TextManager.Get("AutoSaved"); + LocalizedString label = TextManager.Get("AutoSaved"); autoSaveLabel = new GUILayoutGroup(new RectTransform(new Point(GUI.IntScale(150), GUI.IntScale(32)), GameMain.SubEditorScreen.EntityMenu.RectTransform, Anchor.TopRight) { ScreenSpaceOffset = new Point(-GUI.IntScale(16), -GUI.IntScale(48)) @@ -1650,7 +1671,7 @@ namespace Barotrauma }; GUIImage checkmark = new GUIImage(new RectTransform(new Vector2(0.25f, 1f), autoSaveLabel.RectTransform), style: "MissionCompletedIcon", scaleToFit: true); - GUITextBlock labelComponent = new GUITextBlock(new RectTransform(new Vector2(0.75f, 1f), autoSaveLabel.RectTransform), label, font: GUI.SubHeadingFont, color: GUI.Style.Green) + GUITextBlock labelComponent = new GUITextBlock(new RectTransform(new Vector2(0.75f, 1f), autoSaveLabel.RectTransform), label, font: GUIStyle.SubHeadingFont, color: GUIStyle.Green) { Padding = Vector4.Zero, AutoScaleHorizontal = true, @@ -1666,47 +1687,43 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(nameBox.Text)) { - GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); nameBox.Flash(); return false; } string specialSavePath = ""; - if (Submarine.MainSub.Info.Type != SubmarineType.Player) + if (MainSub.Info.Type != SubmarineType.Player) { - ContentType contentType = ContentType.Submarine; - switch (Submarine.MainSub.Info.Type) + Identifier typeIdentifier = MainSub.Info.Type.ToString().ToIdentifier(); + Type contentType = ContentFile.Types.FirstOrDefault(t + => !t.Type.IsAbstract + && t.Type.IsSubclassOf(typeof(BaseSubFile)) + && t.Names.Contains(typeIdentifier)) + ?.Type ?? + typeof(SubmarineFile); + if (MainSub.Info.Type == SubmarineType.OutpostModule && + MainSub.Info.OutpostModuleInfo != null) { - case SubmarineType.OutpostModule: - if (Submarine.MainSub.Info?.OutpostModuleInfo != null) - { - contentType = ContentType.OutpostModule; - Submarine.MainSub.Info.PreviewImage = null; - } - break; - case SubmarineType.Outpost: - contentType = ContentType.Outpost; - break; - case SubmarineType.Wreck: - contentType = ContentType.Wreck; - break; - case SubmarineType.EnemySubmarine: - contentType = ContentType.EnemySubmarine; - break; + contentType = typeof(OutpostModuleFile); + MainSub.Info.PreviewImage = null; } - if (contentType != ContentType.Submarine) + + if (contentType != typeof(SubmarineFile)) { #if DEBUG - var existingFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), contentType); - if (contentType == ContentType.OutpostModule) + var existingFiles = GameMain.VanillaContent.GetFiles(contentType); + if (contentType == typeof(OutpostModuleFile)) { - existingFiles = existingFiles.Where(f => f.Path.Contains("Ruin") == Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin")); + existingFiles = existingFiles.Where(f => f.Path.Value.Contains("Ruin") == MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier())); } #else - var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages.Where(c => c != GameMain.VanillaContent), contentType); + var existingFiles = ContentPackageManager.EnabledPackages.All + .Where(c => c != GameMain.VanillaContent) + .SelectMany(c => c.GetFiles(contentType)); #endif - specialSavePath = existingFiles.FirstOrDefault(f => - Path.GetFullPath(f.Path) != Path.GetFullPath(SubmarineInfo.SavePath) && ContentPackage.IsModFilePathAllowed(f.Path))?.Path; + specialSavePath = existingFiles.FirstOrDefault(f => + ContentPackage.PathAllowedAsLocalModFile(f.Path.Value))?.Path.Value; if (!string.IsNullOrEmpty(specialSavePath)) { @@ -1714,9 +1731,9 @@ namespace Barotrauma } } } - else if (Submarine.MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle)) + else if (MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !MainSub.Info.HasTag(SubmarineTag.Shuttle)) { - var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("undefinedsubmarineclasswarning"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("undefinedsubmarineclasswarning"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { @@ -1734,16 +1751,16 @@ namespace Barotrauma } if (!string.IsNullOrEmpty(specialSavePath) && - (string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) || Path.GetFileNameWithoutExtension(Submarine.MainSub.Info.Name) != nameBox.Text || Path.GetDirectoryName(Submarine.MainSub?.Info.FilePath) != specialSavePath)) + (string.IsNullOrEmpty(MainSub?.Info.FilePath) || Path.GetFileNameWithoutExtension(MainSub.Info.Name) != nameBox.Text || Path.GetDirectoryName(MainSub?.Info.FilePath) != specialSavePath)) { - string submarineTypeTag = "SubmarineType." + Submarine.MainSub.Info.Type; - if (Submarine.MainSub.Info.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(submarineTypeTag)) + string submarineTypeTag = $"SubmarineType.{MainSub.Info.Type}"; + if (MainSub.Info.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(submarineTypeTag)) { submarineTypeTag = "MissionType.Pirate"; } var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("savesubtospecialfolderprompt", - new string[] { "[type]", "[outpostpath]" }, new string[] { TextManager.Get(submarineTypeTag), specialSavePath }), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + ("[type]", TextManager.Get(submarineTypeTag)), ("[outpostpath]", specialSavePath)), + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { SaveSubToFile(nameBox.Text, specialSavePath); @@ -1766,45 +1783,87 @@ namespace Barotrauma return result; } + private readonly Queue packageReloadQueue = new Queue(); + + private void EnqueueForReload(ContentPackage p) + { + if (p is null) { return; } + if (!packageReloadQueue.Contains(p)) { packageReloadQueue.Enqueue(p); } + } + private bool SaveSubToFile(string name, string specialSavePath = null) { + bool canModifyPackage(ContentPackage p) + => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; + + Type subFileType = MainSub?.Info.Type switch + { + SubmarineType.Outpost => typeof(OutpostFile), + SubmarineType.OutpostModule => typeof(OutpostModuleFile), + SubmarineType.Ruin => typeof(OutpostModuleFile), + SubmarineType.Wreck => typeof(WreckFile), + SubmarineType.BeaconStation => typeof(BeaconStationFile), + SubmarineType.EnemySubmarine => typeof(EnemySubmarineFile), + SubmarineType.Player => typeof(SubmarineFile) + }; + + void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) + { + filePath = filePath.CleanUpPath(); + packagePath = packagePath.CleanUpPath(); + string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false); + if (filePath.StartsWith(packageDir)) + { + filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; + } + if (!modProject.Files.Any(f => f.Type == subFileType && + f.Path == filePath)) + { + var newFile = ModProject.File.FromPath(filePath, subFileType); + modProject.AddFile(newFile); + } + + modProject.DiscardHashAndInstallTime(); + modProject.Save(packagePath); + } + if (string.IsNullOrWhiteSpace(name)) { - GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); return false; } foreach (var illegalChar in Path.GetInvalidFileNameChars()) { if (!name.Contains(illegalChar)) continue; - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } + 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 prevSavePath = null; - string directoryName = Submarine.MainSub?.Info?.FilePath == null ? - SubmarineInfo.SavePath : Path.GetDirectoryName(Submarine.MainSub.Info.FilePath); if (!string.IsNullOrEmpty(specialSavePath)) { - directoryName = specialSavePath; + string directoryName = specialSavePath; savePath = Path.Combine(directoryName, savePath); - ContentPackage contentPackage = GameMain.Config.AllEnabledPackages.FirstOrDefault(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path) == directoryName)); + ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path.Value) == directoryName)); - bool allowSavingToVanilla = false; -#if DEBUG - allowSavingToVanilla = true; -#endif - if (!contentPackage.Files.Any(f => Path.GetFullPath(f.Path) == Path.GetFullPath(savePath)) && (allowSavingToVanilla || contentPackage != GameMain.VanillaContent)) + if (!contentPackage.Files.Any(f => f.Path == savePath) && canModifyPackage(contentPackage)) { var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("addtocontentpackageprompt", "[packagename]", contentPackage.Name), - new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked = (bt, userdata) => { - contentPackage.AddFile(savePath, ContentType.OutpostModule); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - contentPackage.Save(contentPackage.Path, reload: false); - Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; + ModProject modProject = new ModProject(contentPackage); + addSubAndSaveModProject(modProject, savePath, contentPackage.Path); + EnqueueForReload(contentPackage); + msgBox.Close(); return true; }; @@ -1815,72 +1874,58 @@ namespace Barotrauma }; } } - else if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && - Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + else if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && + MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { - prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); - string prevDir = Path.GetDirectoryName(Submarine.MainSub.Info.FilePath).CleanUpPath(); + prevSavePath = MainSub.Info.FilePath.CleanUpPath(); + string prevDir = Path.GetDirectoryName(MainSub.Info.FilePath).CleanUpPath(); string[] subDirs = prevDir.Split('/'); - bool forceToSubFolder = Steam.SteamManager.IsInitialized; - bool isInSubFolder = subDirs.Length > 0 && subDirs[0].Equals("Submarines", StringComparison.InvariantCultureIgnoreCase); - if (forceToSubFolder && subDirs.Length > 1 && subDirs[0].Equals("Mods", StringComparison.InvariantCultureIgnoreCase)) + + ModProject modProject = new ModProject() { Name = name }; + string fileListPath = null; + + if (subDirs.Length > 1 && subDirs[0].Equals(ContentPackage.LocalModsDir, StringComparison.InvariantCultureIgnoreCase)) { string modName = subDirs[1]; - ContentPackage contentPackage = ContentPackage.AllPackages.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); + ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); if (contentPackage != null) { - Steamworks.Data.PublishedFileId packageId = contentPackage.SteamWorkshopId; - - Task itemInfoTask = Steamworks.Ugc.Item.GetAsync(packageId); - Task itemUpdateTask = Task.Run(async () => - { - while (!itemInfoTask.IsCompleted) - { - Steamworks.SteamClient.RunCallbacks(); - await Task.Delay(16); - } - return itemInfoTask.Result; - }); - - Steamworks.Ugc.Item? item = itemUpdateTask.Result; - if (item?.Owner.Id == Steam.SteamManager.GetSteamID()) - { - forceToSubFolder = false; - string targetPath = Path.Combine(prevDir, savePath).CleanUpPath(); - if (!contentPackage.Files.Any(f => f.Type == ContentType.Submarine && - f.Path.CleanUpPath().Equals(targetPath, StringComparison.InvariantCultureIgnoreCase))) - { - contentPackage.AddFile(new ContentFile(targetPath, ContentType.Submarine)); - } - contentPackage.Save(contentPackage.Path, reload: false); - } + modProject = new ModProject(contentPackage); + fileListPath = contentPackage.Path; + EnqueueForReload(contentPackage); } } - savePath = Path.Combine(forceToSubFolder && !isInSubFolder ? SubmarineInfo.SavePath : prevDir, savePath).CleanUpPath(); + + savePath = Path.Combine(prevDir, savePath).CleanUpPath(); + if (!isVanillaSub) + { + addSubAndSaveModProject(modProject, savePath, fileListPath ?? Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName)); + } } else { - savePath = Path.Combine(SubmarineInfo.SavePath, savePath); + 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 - var vanilla = GameMain.VanillaContent; +#if !DEBUG if (vanilla != null) { - var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); string pathToCompare = savePath.Replace(@"\", @"/"); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) + if (vanillaSubs.Any(sub => sub.Value.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) { - GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUI.Style.Red, font: GUI.LargeFont); + GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } } #endif - if (Submarine.MainSub != null) + if (MainSub != null) { Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; - if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && Submarine.MainSub.Info.Type != SubmarineType.OutpostModule) + if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule) { bool savePreviewImage = true; using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); @@ -1890,21 +1935,30 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError($"Saving the preview image of the submarine \"{Submarine.MainSub.Info.Name}\" failed.", e); + DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e); savePreviewImage = false; } - Submarine.MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); + MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); } else { - Submarine.MainSub.TrySaveAs(savePath); + MainSub.TrySaveAs(savePath); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; - Submarine.MainSub.CheckForErrors(); + MainSub.CheckForErrors(); - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); + if (savePath.StartsWith(newLocalModDir)) + { + ContentPackageManager.LocalPackages.Refresh(); + var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); + if (newPackage is RegularPackage regular) + { + ContentPackageManager.EnabledPackages.EnableRegular(regular); + } + } SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } @@ -1916,7 +1970,7 @@ namespace Barotrauma if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } linkedSubBox.AddItem(sub.Name, sub); } - subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } return false; @@ -1942,7 +1996,7 @@ namespace Barotrauma 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 }; - //var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveSubDialogHeader"), font: GUI.LargeFont); + //var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveSubDialogHeader"), font: GUIStyle.LargeFont); var columnArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), paddedSaveFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.01f, Stretch = true }; @@ -1952,7 +2006,7 @@ namespace Barotrauma var nameHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), true); var saveSubLabel = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), - TextManager.Get("SaveSubDialogName"), font: GUI.SubHeadingFont); + TextManager.Get("SaveSubDialogName"), font: GUIStyle.SubHeadingFont); submarineNameCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); @@ -1965,7 +2019,7 @@ namespace Barotrauma if (text.Length > submarineNameLimit) { nameBox.Text = text.Substring(0, submarineNameLimit); - nameBox.Flash(GUI.Style.Red); + nameBox.Flash(GUIStyle.Red); return true; } @@ -1973,18 +2027,18 @@ namespace Barotrauma return true; }; - nameBox.Text = subNameLabel?.Text ?? ""; + nameBox.Text = subNameLabel?.Text?.SanitizedValue ?? ""; submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit; var descriptionHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), isHorizontal: true); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), descriptionHeaderGroup.RectTransform), TextManager.Get("SaveSubDialogDescription"), font: GUI.SubHeadingFont); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), descriptionHeaderGroup.RectTransform), TextManager.Get("SaveSubDialogDescription"), font: GUIStyle.SubHeadingFont); submarineDescriptionCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), descriptionHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform)); descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), - font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) + font: GUIStyle.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { Padding = new Vector4(10 * GUI.Scale) }; @@ -1994,7 +2048,7 @@ namespace Barotrauma if (text.Length > submarineDescriptionLimit) { descriptionBox.Text = text.Substring(0, submarineDescriptionLimit); - descriptionBox.Flash(GUI.Style.Red); + descriptionBox.Flash(GUIStyle.Red); return true; } @@ -2046,13 +2100,13 @@ namespace Barotrauma var outpostModuleGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); - HashSet availableFlags = new HashSet(); - foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } - foreach (string flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + 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 (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) + foreach (Identifier flag in sub.OutpostModuleInfo.ModuleFlags) { if (flag == "none") { continue; } availableFlags.Add(flag); @@ -2060,22 +2114,22 @@ namespace Barotrauma } var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), - text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s)) ?? "None".ToEnumerable()), selectMultiple: true); - foreach (string flag in availableFlags) + text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true); + foreach (Identifier flag in availableFlags) { - moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } - if (Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag)) + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); + if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag)) { moduleTypeDropDown.SelectItem(flag); } } moduleTypeDropDown.OnSelected += (_, __) => { - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); + if (MainSub?.Info?.OutpostModuleInfo == null) { return false; } + MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); moduleTypeDropDown.Text = ToolBox.LimitString( - Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", + MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", moduleTypeDropDown.Font, moduleTypeDropDown.Rect.Width); return true; }; @@ -2088,30 +2142,30 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), TextManager.Get("outpostmoduleallowattachto"), textAlignment: Alignment.CenterLeft); var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), - text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s)) ?? "Any".ToEnumerable()), selectMultiple: true); + text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"Any").ToEnumerable()), selectMultiple: true); allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any"); - if (Submarine.MainSub.Info.OutpostModuleInfo == null || - !Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || - Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) + if (MainSub.Info.OutpostModuleInfo == null || + !MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || + MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")) { allowAttachDropDown.SelectItem("any"); } - foreach (string flag in availableFlags) + foreach (Identifier flag in availableFlags) { - if (flag.Equals("any", StringComparison.OrdinalIgnoreCase) || flag.Equals("none", StringComparison.OrdinalIgnoreCase)) { continue; } - allowAttachDropDown.AddItem(TextManager.Capitalize(flag), flag); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } - if (Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Contains(flag)) + if (flag == "any" || flag == "none") { continue; } + allowAttachDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); + if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Contains(flag)) { allowAttachDropDown.SelectItem(flag); } } allowAttachDropDown.OnSelected += (_, __) => { - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast()); + if (MainSub?.Info?.OutpostModuleInfo == null) { return false; } + MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast()); allowAttachDropDown.Text = ToolBox.LimitString( - Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? allowAttachDropDown.Text : "None", + MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? allowAttachDropDown.Text.Value : "None", allowAttachDropDown.Font, allowAttachDropDown.Rect.Width); return true; }; @@ -2122,26 +2176,26 @@ namespace Barotrauma var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier.ToLowerInvariant()); } + HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; + foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), - text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); - foreach (string locationType in availableLocationTypes) + text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); + foreach (Identifier locationType in availableLocationTypes) { - locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } - if (Submarine.MainSub.Info.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType)) + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); + if (MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (MainSub.Info.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType)) { locationTypeDropDown.SelectItem(locationType); } } - if (!Submarine.MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any"); } + if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any"); } locationTypeDropDown.OnSelected += (_, __) => { - Submarine.MainSub?.Info?.OutpostModuleInfo?.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); - locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); + MainSub?.Info?.OutpostModuleInfo?.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text.Value, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); return true; }; locationTypeGroup.RectTransform.MinSize = new Point(0, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); @@ -2156,12 +2210,12 @@ namespace Barotrauma var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); - var outpostModuleInfo = Submarine.MainSub.Info?.OutpostModuleInfo; + var outpostModuleInfo = MainSub.Info?.OutpostModuleInfo; if (outpostModuleInfo != null) { if (outpostModuleInfo.GapPositions == OutpostModuleInfo.GapPosition.None) { - outpostModuleInfo.DetermineGapPositions(Submarine.MainSub); + outpostModuleInfo.DetermineGapPositions(MainSub); } foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) { @@ -2176,14 +2230,14 @@ namespace Barotrauma gapPositionDropDown.OnSelected += (_, __) => { - if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None; + if (MainSub.Info?.OutpostModuleInfo == null) { return false; } + MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None; if (gapPositionDropDown.SelectedDataMultiple.Any()) { - List gapPosTexts = new List(); + List gapPosTexts = new List(); foreach (OutpostModuleInfo.GapPosition gapPos in gapPositionDropDown.SelectedDataMultiple) { - Submarine.MainSub.Info.OutpostModuleInfo.GapPositions |= gapPos; + MainSub.Info.OutpostModuleInfo.GapPositions |= gapPos; gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString())); } gapPositionDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), gapPositionDropDown.Font, gapPositionDropDown.Rect.Width); @@ -2210,12 +2264,12 @@ namespace Barotrauma new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), GUINumberInput.NumberType.Int) { ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip"), - IntValue = Submarine.MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000, + IntValue = MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000, MinValueInt = 0, MaxValueInt = 1000, OnValueChanged = (numberInput) => { - Submarine.MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue; + MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue; } }; @@ -2227,12 +2281,12 @@ namespace Barotrauma TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true); new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Float) { - FloatValue = Submarine.MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, + FloatValue = MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, MinValueFloat = 0, MaxValueFloat = 100, OnValueChanged = (numberInput) => { - Submarine.MainSub.Info.OutpostModuleInfo.Commonness = numberInput.FloatValue; + MainSub.Info.OutpostModuleInfo.Commonness = numberInput.FloatValue; } }; outpostSettingsContainer.RectTransform.MinSize = new Point(0, outpostSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); @@ -2255,20 +2309,20 @@ 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 : Submarine.MainSub?.CalculateBasePrice()) ?? 1000; + 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) { - IntValue = Math.Max(Submarine.MainSub?.Info?.Price ?? basePrice, basePrice), + IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice), MinValueInt = basePrice, MaxValueInt = 999999, OnValueChanged = (numberInput) => { - Submarine.MainSub.Info.Price = numberInput.IntValue; + MainSub.Info.Price = numberInput.IntValue; } }; - if (Submarine.MainSub?.Info != null) + if (MainSub?.Info != null) { - Submarine.MainSub.Info.Price = Math.Max(Submarine.MainSub.Info.Price, basePrice); + 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) @@ -2287,11 +2341,11 @@ namespace Barotrauma classDropDown.OnSelected += (selected, userdata) => { SubmarineClass submarineClass = (SubmarineClass)userdata; - Submarine.MainSub.Info.SubmarineClass = submarineClass; + MainSub.Info.SubmarineClass = submarineClass; return true; }; - classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); - classText.Enabled = classDropDown.ButtonEnabled = !Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle); + classDropDown.SelectItem(MainSub.Info.SubmarineClass); + classText.Enabled = classDropDown.ButtonEnabled = !MainSub.Info.HasTag(SubmarineTag.Shuttle); var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) { @@ -2300,7 +2354,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUI.SmallFont); + 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) { MinValueInt = 1, @@ -2316,15 +2370,15 @@ namespace Barotrauma crewSizeMin.OnValueChanged += (numberInput) => { crewSizeMax.IntValue = Math.Max(crewSizeMax.IntValue, numberInput.IntValue); - Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; + MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; crewSizeMax.OnValueChanged += (numberInput) => { crewSizeMin.IntValue = Math.Min(crewSizeMin.IntValue, numberInput.IntValue); - Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; + MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), subSettingsContainer.RectTransform), isHorizontal: true) @@ -2334,52 +2388,53 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewExpArea.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUI.SmallFont); + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true, font: GUIStyle.SmallFont); 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), crewExperienceLevels[0], textAlignment: Alignment.Center); + var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform), + text: crewExperienceLevels[0], 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 = Array.IndexOf(crewExperienceLevels, (string)experienceText.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]); - Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; toggleExpRight.OnClicked += (btn, userData) => { - int currentIndex = Array.IndexOf(crewExperienceLevels, (string)experienceText.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]); - Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; + MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; - if (Submarine.MainSub != null) + if (MainSub != null) { - int min = Submarine.MainSub.Info.RecommendedCrewSizeMin; - int max = Submarine.MainSub.Info.RecommendedCrewSizeMax; + int min = MainSub.Info.RecommendedCrewSizeMin; + int max = MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(Submarine.MainSub.Info.RecommendedCrewExperience) ? - crewExperienceLevels[0] : Submarine.MainSub.Info.RecommendedCrewExperience; + experienceText.UserData = string.IsNullOrEmpty(MainSub.Info.RecommendedCrewExperience) ? + crewExperienceLevels[0] : MainSub.Info.RecommendedCrewExperience; experienceText.Text = TextManager.Get((string)experienceText.UserData); } subTypeDropdown.OnSelected += (selected, userdata) => { SubmarineType type = (SubmarineType)userdata; - Submarine.MainSub.Info.Type = type; + MainSub.Info.Type = type; if (type == SubmarineType.OutpostModule) { - Submarine.MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(Submarine.MainSub.Info); + MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(MainSub.Info); } previewImageButtonHolder.Children.ForEach(c => c.Enabled = type != SubmarineType.OutpostModule); outpostSettingsContainer.Visible = type == SubmarineType.OutpostModule; @@ -2392,11 +2447,11 @@ namespace Barotrauma subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0)); // right column --------------------------------------------------- - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUI.SubHeadingFont); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUIStyle.SubHeadingFont); var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; - previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); + previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), MainSub?.Info.PreviewImage, scaleToFit: true); previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -2408,9 +2463,9 @@ namespace Barotrauma { CreateImage(defaultPreviewImageSize.X, defaultPreviewImageSize.Y, imgStream); previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream, compress: false), null, null); - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + MainSub.Info.PreviewImage = previewImage.Sprite; } } return true; @@ -2430,9 +2485,9 @@ namespace Barotrauma } previewImage.Sprite = new Sprite(file, sourceRectangle: null); - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; + MainSub.Info.PreviewImage = previewImage.Sprite; } }; FileSelection.ClearFileTypeFilters(); @@ -2450,7 +2505,7 @@ namespace Barotrauma var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), rightColumn.RectTransform), style: null); var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), - TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUI.SmallFont); + 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), @@ -2458,15 +2513,15 @@ namespace Barotrauma foreach (SubmarineTag tag in Enum.GetValues(typeof(SubmarineTag))) { - string tagStr = TextManager.Get(tag.ToString()); + LocalizedString tagStr = TextManager.Get(tag.ToString()); var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), - tagStr, font: GUI.SmallFont) + tagStr, font: GUIStyle.SmallFont) { - Selected = Submarine.MainSub != null && Submarine.MainSub.Info.HasTag(tag), + Selected = MainSub != null && MainSub.Info.HasTag(tag), UserData = tag, OnSelected = (GUITickBox tickBox) => { - if (Submarine.MainSub == null) return false; + if (MainSub == null) return false; SubmarineTag tag = (SubmarineTag)tickBox.UserData; if (tag == SubmarineTag.Shuttle) { @@ -2476,17 +2531,17 @@ namespace Barotrauma } else { - classDropDown.SelectItem(Submarine.MainSub.Info.SubmarineClass); + classDropDown.SelectItem(MainSub.Info.SubmarineClass); } classText.Enabled = classDropDown.ButtonEnabled = !tickBox.Selected; } if (tickBox.Selected) { - Submarine.MainSub.Info.AddTag(tag); + MainSub.Info.AddTag(tag); } else { - Submarine.MainSub.Info.RemoveTag(tag); + MainSub.Info.RemoveTag(tag); } return true; } @@ -2494,37 +2549,38 @@ namespace Barotrauma } var contentPackagesLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform, Anchor.TopRight), - TextManager.Get("RequiredContentPackages"), wrap: true, font: GUI.SmallFont); + TextManager.Get("RequiredContentPackages"), wrap: true, font: GUIStyle.SmallFont); var contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomRight)); - if (Submarine.MainSub != null) { - List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); - foreach (ContentPackage contentPack in ContentPackage.AllPackages) + if (MainSub != null) + { + List contentPacks = MainSub.Info.RequiredContentPackages.ToList(); + foreach (ContentPackage contentPack in ContentPackageManager.AllPackages) { //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) - if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } + if (contentPack.Files.All(f => f is SubmarineFile)) { continue; } if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } } foreach (string contentPackageName in contentPacks) { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont) { - Selected = Submarine.MainSub.Info.RequiredContentPackages.Contains(contentPackageName), + Selected = MainSub.Info.RequiredContentPackages.Contains(contentPackageName), UserData = contentPackageName }; cpTickBox.OnSelected += tickBox => { if (tickBox.Selected) { - Submarine.MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); + MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); } else { - Submarine.MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); + MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); } return true; }; @@ -2557,10 +2613,10 @@ namespace Barotrauma subSettingsContainer.Recalculate(); outpostSettingsContainer.Recalculate(); - descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; + descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; - subTypeDropdown.SelectItem(Submarine.MainSub.Info.Type); + subTypeDropdown.SelectItem(MainSub.Info.Type); if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } @@ -2584,7 +2640,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), - TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUI.LargeFont); + TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUIStyle.LargeFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyDialogName")); nameBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 0.1f), paddedSaveFrame.RectTransform)); @@ -2598,7 +2654,7 @@ namespace Barotrauma var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), paddedSaveFrame.RectTransform)); descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.TopLeft), - font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) + font: GUIStyle.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { Padding = new Vector4(10 * GUI.Scale) }; @@ -2639,7 +2695,7 @@ namespace Barotrauma /// private List LoadItemAssemblyInventorySafe(ItemAssemblyPrefab assemblyPrefab) { - var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, Submarine.MainSub); + var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, MainSub); var itemInstance = new List(); realItems.ForEach(entity => { @@ -2655,7 +2711,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(nameBox.Text)) { - GUI.AddMessage(TextManager.Get("ItemAssemblyNameMissingWarning"), GUI.Style.Red); + GUI.AddMessage(TextManager.Get("ItemAssemblyNameMissingWarning"), GUIStyle.Red); nameBox.Flash(); return false; @@ -2665,7 +2721,7 @@ namespace Barotrauma { if (nameBox.Text.Contains(illegalChar)) { - GUI.AddMessage(TextManager.GetWithVariable("ItemAssemblyNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariable("ItemAssemblyNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); nameBox.Flash(); return false; } @@ -2675,28 +2731,15 @@ namespace Barotrauma #if DEBUG string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; #else - string saveFolder = ItemAssemblyPrefab.SaveFolder; - if (!Directory.Exists(saveFolder)) - { - try - { - Directory.CreateDirectory(saveFolder); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to create a directory for the item assmebly.", e); - return false; - } - } + string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); #endif - string filePath = Path.Combine(saveFolder, nameBox.Text + ".xml"); + string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); if (File.Exists(filePath)) { var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { msgBox.Close(); - ItemAssemblyPrefab.Remove(filePath); Save(); return true; }; @@ -2706,7 +2749,7 @@ namespace Barotrauma { var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); - if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath) == ItemAssemblyPrefab.VanillaSaveFolder) + if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath.Value) == ItemAssemblyPrefab.VanillaSaveFolder) { var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); } @@ -2724,7 +2767,20 @@ namespace Barotrauma #else doc.SaveSafe(filePath); #endif - new ItemAssemblyPrefab(filePath, allowOverwrite: true); + ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); + if (existingContentPackage == null) + { + //content package doesn't exist, create one + ModProject modProject = new ModProject() { Name = nameBox.Text }; + var newFile = ModProject.File.FromPath(filePath); + modProject.AddFile(newFile); + ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); + } + else + { + EnqueueForReload(existingContentPackage); + } + UpdateEntityList(); } @@ -2735,16 +2791,21 @@ namespace Barotrauma private void SnapToGrid() { // First move components - foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast()) + foreach (MapEntity e in MapEntity.SelectedList) { - var wire = item.GetComponent(); - if (wire == null) + // Items snap to centre of nearest grid square + Vector2 offset = e.Position; + offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + if (e is Item item) { - // Items snap to centre of nearest grid square - Vector2 offset = item.Position; - offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y); + var wire = item.GetComponent(); + if (wire != null) { continue; } item.Move(offset); } + else if (e is Structure structure) + { + structure.Move(offset); + } } // Then move wires, separated as moving components also moves the start and end node of wires @@ -2786,9 +2847,9 @@ namespace Barotrauma Stretch = true }; - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUI.Font, createClearButton: true); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUIStyle.Font, createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"), - textAlignment: Alignment.CenterLeft, font: GUI.Font) + textAlignment: Alignment.CenterLeft, font: GUIStyle.Font) { CanBeFocused = false, IgnoreLayoutGroups = true @@ -2832,7 +2893,7 @@ namespace Barotrauma textTag = "MissionType.Pirate"; } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, - TextManager.Get(textTag), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") + TextManager.Get(textTag), font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") { CanBeFocused = false }; @@ -2840,7 +2901,7 @@ namespace Barotrauma } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, - ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) + ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80)) { UserData = sub, ToolTip = sub.FilePath @@ -2849,19 +2910,19 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = textBlock.TextColor * 0.8f, - ToolTip = textBlock.RawToolTip + ToolTip = textBlock.ToolTip.SanitizedString }; } else if (sub.IsPlayer) { var classText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = textBlock.TextColor * 0.8f, - ToolTip = textBlock.RawToolTip + ToolTip = textBlock.ToolTip.SanitizedString }; } } @@ -2901,22 +2962,13 @@ namespace Barotrauma DateTime time = DateTime.MinValue.AddSeconds(saveElement.GetAttributeUInt64("time", 0)); TimeSpan difference = DateTime.UtcNow - time; - string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", - new[] - { - "[hours]", - "[minutes]", - "[seconds]" - }, - new[] - { - ((int)Math.Floor(difference.TotalHours)).ToString(), - difference.Minutes.ToString(), - difference.Seconds.ToString() - }); + LocalizedString tooltip = TextManager.GetWithVariables("subeditor.autosaveage", + ("[hours]", ((int)Math.Floor(difference.TotalHours)).ToString()), + ("[minutes]", difference.Minutes.ToString()), + ("[seconds]", difference.Seconds.ToString())); - string submarineName = saveElement.GetAttributeString("name", TextManager.Get("UnspecifiedSubFileName")); - string timeFormat; + string submarineName = saveElement.GetAttributeString("name", TextManager.Get("UnspecifiedSubFileName").Value); + LocalizedString timeFormat; double totalMinutes = difference.TotalMinutes; @@ -2933,7 +2985,7 @@ namespace Barotrauma timeFormat = TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString()); } - string entryName = TextManager.GetWithVariables("subeditor.autosaveentry", new []{ "[submarine]", "[saveage]" }, new []{ submarineName, timeFormat }); + LocalizedString entryName = TextManager.GetWithVariables("subeditor.autosaveentry", ("[submarine]", submarineName), ("[saveage]", timeFormat)); loadAutoSave.AddItem(entryName, saveElement, tooltip); } @@ -2977,14 +3029,15 @@ namespace Barotrauma { if (!(UserData is XElement element)) { return; } - string filePath = element.GetAttributeString("file", ""); + #warning TODO: revise + string filePath = element.GetAttributeStringUnrestricted("file", ""); if (string.IsNullOrWhiteSpace(filePath)) { return; } var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); // set the submarine file path to the "default" value loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); - loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); + loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName").Value; try { loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); @@ -2993,15 +3046,15 @@ namespace Barotrauma { DebugConsole.ThrowError("Failed to find a name for the submarine.", e); } - Submarine.MainSub = loadedSub; - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.UpdateTransform(); - Submarine.MainSub.Info.Name = loadedSub.Info.Name; + MainSub = loadedSub; + MainSub.SetPrevTransform(MainSub.Position); + MainSub.UpdateTransform(); + MainSub.Info.Name = loadedSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); CreateDummyCharacter(); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + cam.Position = MainSub.Position + MainSub.HiddenSubPosition; loadFrame = null; } @@ -3033,15 +3086,15 @@ namespace Barotrauma { Submarine.Unload(); var selectedSub = new Submarine(info); - Submarine.MainSub = selectedSub; - Submarine.MainSub.UpdateTransform(interpolate: false); + MainSub = selectedSub; + MainSub.UpdateTransform(interpolate: false); ClearUndoBuffer(); CreateDummyCharacter(); - string name = Submarine.MainSub.Info.Name; + string name = MainSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + cam.Position = MainSub.Position + MainSub.HiddenSubPosition; loadFrame = null; @@ -3067,31 +3120,38 @@ namespace Barotrauma ReconstructLayers(); } + private RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) + { + foreach (RegularPackage regularPackage in ContentPackageManager.RegularPackages) + { + if (regularPackage.Files.Length == 1 && regularPackage.Files[0].Path == sub.FilePath) + { + return regularPackage; + } + } + + return null; + } + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } - //if the sub is included in a content package that only defines that one sub, - //delete the content package as well - ContentPackage subPackage = null; - foreach (ContentPackage cp in ContentPackage.RegularPackages) - { - if (cp.Files.Count == 1 && Path.GetFullPath(cp.Files[0].Path) == Path.GetFullPath(sub.FilePath)) - { - subPackage = cp; - break; - } - } - subPackage?.Delete(); - + //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. + var subPackage = GetContentPackageIntrinsicallyTiedToSub(sub); + if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } + var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked += (btn, userData) => { try { + Directory.Delete(Path.GetDirectoryName(subPackage.Path), true); + sub.Dispose(); File.Delete(sub.FilePath); SubmarineInfo.RefreshSavedSubs(); @@ -3116,7 +3176,7 @@ namespace Barotrauma categoryButton.UserData == null; string categoryName = entityCategory.HasValue ? entityCategory.Value.ToString() : "All"; selectedCategoryText.Text = TextManager.Get("MapEntityCategory." + categoryName); - selectedCategoryButton.ApplyStyle(GUI.Style.GetComponentStyle("CategoryButton." + categoryName)); + selectedCategoryButton.ApplyStyle(GUIStyle.GetComponentStyle("CategoryButton." + categoryName)); } selectedCategory = entityCategory; @@ -3166,7 +3226,7 @@ namespace Barotrauma var innerList = child.GetChild(); foreach (GUIComponent grandChild in innerList.Content.Children) { - grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.ToLower().Contains(filter); + grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.Value.Contains(filter, StringComparison.OrdinalIgnoreCase); } }; categorizedEntityList.UpdateScrollBarSize(); @@ -3181,7 +3241,7 @@ namespace Barotrauma { child.Visible = (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && - ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); + ((MapEntityPrefab)child.UserData).Name.Value.Contains(filter, StringComparison.OrdinalIgnoreCase); } allEntityList.UpdateScrollBarSize(); allEntityList.BarScroll = 0.0f; @@ -3263,7 +3323,7 @@ namespace Barotrauma new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate { bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null); - foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) + foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) { if (MapEntity.SelectedList.Contains(match)) { continue; } if (match is Gap gap) @@ -3287,7 +3347,7 @@ namespace Barotrauma new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate { ImageManager.EditorMode = !ImageManager.EditorMode; - if (!ImageManager.EditorMode) { GameMain.Config.SaveNewPlayerConfig(); } + if (!ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); } })); } else @@ -3353,7 +3413,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(name)) { - name = TextManager.Get("editor.layer.newlayer"); + name = TextManager.Get("editor.layer.newlayer").Value; } string incrementedName = name; @@ -3433,7 +3493,7 @@ namespace Barotrauma return; } - Submarine sub = Submarine.MainSub; + Submarine sub = MainSub; List entities; try { @@ -3473,7 +3533,7 @@ namespace Barotrauma } else if (selectedEntity is { SerializableProperties: { } props} ) { - if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + if (props.TryGetValue(property.Name.ToIdentifier(), out SerializableProperty foundProp)) { entities.Add((selectedEntity, (Color) foundProp.GetValue(selectedEntity), foundProp)); } @@ -3490,14 +3550,14 @@ namespace Barotrauma Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); - GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) + GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) { UserData = "colorpicker", Draggable = true }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform)); - GUITextBlock headerText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), contentLayout.RectTransform), property.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.TopCenter) + GUITextBlock headerText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), contentLayout.RectTransform), property.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopCenter) { AutoScaleVertical = true }; @@ -3528,17 +3588,17 @@ namespace Barotrauma float currentHue = colorPicker.SelectedHue / 360f; 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: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; + 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 }; 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: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; + 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 }; 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: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; + 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 }; @@ -3594,7 +3654,7 @@ namespace Barotrauma } List affected = entities.Select(t => t.Entity).Where(se => se is MapEntity { Removed: false } || se is ItemComponent).ToList(); - StoreCommand(new PropertyCommand(affected, property.Name, newColor, oldProperties)); + StoreCommand(new PropertyCommand(affected, property.Name.ToIdentifier(), newColor, oldProperties)); if (MapEntity.EditingHUD != null && (MapEntity.EditingHUD.UserData == entity || (!(entity is ItemComponent ic) || MapEntity.EditingHUD.UserData == ic.Item))) { @@ -3605,7 +3665,7 @@ namespace Barotrauma SerializableEntityEditor.LockEditing = true; foreach (SerializableEntityEditor editor in editors) { - if (editor.UserData == entity && editor.Fields.TryGetValue(property.Name, out GUIComponent[] _)) + if (editor.UserData == entity && editor.Fields.TryGetValue(property.Name.ToIdentifier(), out GUIComponent[] _)) { editor.UpdateValue(property, newColor, flash: false); } @@ -3725,7 +3785,7 @@ namespace Barotrauma foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (string.IsNullOrEmpty(itemPrefab.Name)) { continue; } + if (itemPrefab.Name.IsNullOrEmpty()) { continue; } if (!itemPrefab.Tags.Contains("wire")) { continue; } GUIFrame imgFrame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, listBox.Rect.Width / 2), listBox.Content.RectTransform), style: "ListBoxElement") @@ -3733,7 +3793,7 @@ namespace Barotrauma UserData = itemPrefab }; - var img = new GUIImage(new RectTransform(new Vector2(0.9f), imgFrame.RectTransform, Anchor.Center), itemPrefab.sprite, scaleToFit: true) + var img = new GUIImage(new RectTransform(new Vector2(0.9f), imgFrame.RectTransform, Anchor.Center), itemPrefab.Sprite, scaleToFit: true) { UserData = itemPrefab, Color = itemPrefab.SpriteColor @@ -3855,25 +3915,25 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(text)) { - textBox.Flash(GUI.Style.Red); + textBox.Flash(GUIStyle.Red); return false; } - if (Submarine.MainSub != null) Submarine.MainSub.Info.Name = text; + if (MainSub != null) MainSub.Info.Name = text; textBox.Deselect(); textBox.Text = text; - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); return true; } private void ChangeSubDescription(GUITextBox textBox, string text) { - if (Submarine.MainSub != null) + if (MainSub != null) { - Submarine.MainSub.Info.Description = text; + MainSub.Info.Description = text; } else { @@ -3901,7 +3961,7 @@ namespace Barotrauma showEntitiesPanel.Visible = true; showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(entityCountPanel.Rect.Right, saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); matchingTickBox.Selected = true; - matchingTickBox.Flash(GUI.Style.Green); + matchingTickBox.Flash(GUIStyle.Green); } } @@ -3943,7 +4003,7 @@ namespace Barotrauma } case ItemPrefab itemPrefab when PlayerInput.IsShiftDown(): { - var item = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + var item = new Item(itemPrefab, Vector2.Zero, MainSub); if (!inv.TryPutItem(item, dummyCharacter)) { // We failed, remove the item so it doesn't stay at x:0,y:0 @@ -3983,8 +4043,8 @@ namespace Barotrauma private bool GenerateWaypoints() { - if (Submarine.MainSub == null) { return false; } - return WayPoint.GenerateSubWaypoints(Submarine.MainSub); + if (MainSub == null) { return false; } + return WayPoint.GenerateSubWaypoints(MainSub); } private void AddPreviouslyUsed(MapEntityPrefab mapEntityPrefab) @@ -4002,7 +4062,7 @@ namespace Barotrauma if (existing != null) { previouslyUsedList.Content.RemoveChild(existing); } var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), previouslyUsedList.Content.RectTransform) { MinSize = new Point(0, 15) }, - ToolBox.LimitString(mapEntityPrefab.Name, GUI.SmallFont, previouslyUsedList.Content.Rect.Width), font: GUI.SmallFont) + ToolBox.LimitString(mapEntityPrefab.Name.Value, GUIStyle.SmallFont, previouslyUsedList.Content.Rect.Width), font: GUIStyle.SmallFont) { UserData = mapEntityPrefab }; @@ -4295,9 +4355,9 @@ namespace Barotrauma { Rectangle hullRect = rect; hullRect.Y = -hullRect.Y; - Hull newHull = new Hull(MapEntityPrefab.Find(null, "hull"), + Hull newHull = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), hullRect, - Submarine.MainSub); + MainSub); } foreach (MapEntity e in mapEntityList) @@ -4308,7 +4368,7 @@ namespace Barotrauma Rectangle gapRect = e.WorldRect; gapRect.Y -= 8; gapRect.Height = 16; - Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), gapRect); + Gap newGap = new Gap(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), gapRect); } } @@ -4416,7 +4476,7 @@ namespace Barotrauma commandIndex++; // Start removing old commands - if (Commands.Count > Math.Clamp(GameSettings.SubEditorMaxUndoBuffer, 1, 10240)) + if (Commands.Count > Math.Clamp(GameSettings.CurrentConfig.SubEditorUndoBuffer, 1, 10240)) { Commands.First()?.Cleanup(); Commands.RemoveRange(0, 1); @@ -4435,9 +4495,9 @@ namespace Barotrauma layerList.Deselect(); GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft); - new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; - new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; - new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = true }; + new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; + new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes }; foreach (var (layer, (visibility, linkage)) in Layers) { @@ -4499,7 +4559,7 @@ namespace Barotrauma foreach (var child in buttonHeaders.Children) { var btn = child as GUIButton; - string originalBtnText = btn.Text; + string originalBtnText = btn.Text.Value; btn.Text = ToolBox.LimitString(btn.Text, btn.Font, btn.Rect.Width); if (originalBtnText != btn.Text) { @@ -4523,16 +4583,16 @@ namespace Barotrauma for (int i = 0; i < Commands.Count; i++) { Command command = Commands[i]; - string description = command.GetDescription(); + LocalizedString description = command.GetDescription(); CreateTextBlock(description, description, i + 1, command).RectTransform.SetAsFirstChild(); } CreateTextBlock(TextManager.Get("undo.beginning"), TextManager.Get("undo.beginningtooltip"), 0, null); - GUITextBlock CreateTextBlock(string name, string description, int index, Command command) + GUITextBlock CreateTextBlock(LocalizedString name, LocalizedString description, int index, Command command) { return new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), undoBufferList.Content.RectTransform) { MinSize = new Point(0, 15) }, - ToolBox.LimitString(name, GUI.SmallFont, undoBufferList.Content.Rect.Width), font: GUI.SmallFont, textColor: index == commandIndex ? GUI.Style.Green : (Color?) null) + ToolBox.LimitString(name.Value, GUIStyle.SmallFont, undoBufferList.Content.Rect.Width), font: GUIStyle.SmallFont, textColor: index == commandIndex ? GUIStyle.Green : (Color?) null) { UserData = command, ToolTip = description @@ -4627,8 +4687,8 @@ namespace Barotrauma // Move the camera towards to the focus point if (camTargetFocus != Vector2.Zero) { - if (GameMain.Config.KeyBind(InputType.Up).IsDown() || GameMain.Config.KeyBind(InputType.Down).IsDown() || - GameMain.Config.KeyBind(InputType.Left).IsDown() || GameMain.Config.KeyBind(InputType.Right).IsDown()) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up].IsDown() || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down].IsDown() || + GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Left].IsDown() || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Right].IsDown()) { camTargetFocus = Vector2.Zero; } @@ -4746,7 +4806,7 @@ namespace Barotrauma } } - if (GameMain.Config.KeyBind(InputType.ToggleInventory).IsHit() && mode == Mode.Default) + if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].IsHit() && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } @@ -4913,7 +4973,7 @@ namespace Barotrauma } cam.TargetPos = Vector2.Zero; - dummyCharacter.Submarine = Submarine.MainSub; + dummyCharacter.Submarine = MainSub; } // Deposit item from our "infinite stack" into inventory slots @@ -4939,7 +4999,7 @@ namespace Barotrauma // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit if (Inventory.IsMouseOnSlot(slot)) { - var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + var newItem = new Item(itemPrefab, Vector2.Zero, MainSub); if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) { @@ -4963,13 +5023,13 @@ namespace Barotrauma } else { - slot.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f); } } else { newItem.Remove(); - slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f); } if (!newItem.Removed) @@ -5227,12 +5287,12 @@ namespace Barotrauma if (GameMain.DebugDraw) { - GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); - GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); + GUI.DrawLine(spriteBatch, new Vector2(MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); + GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } Submarine.DrawBack(spriteBatch, true, e => e is Structure s && - !IsSubcategoryHidden(e.prefab?.Subcategory) && + !IsSubcategoryHidden(e.Prefab?.Subcategory) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); @@ -5253,15 +5313,15 @@ namespace Barotrauma Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && - !IsSubcategoryHidden(e.prefab?.Subcategory)); + !IsSubcategoryHidden(e.Prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); + Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory)); if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); @@ -5291,18 +5351,18 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); - if (Submarine.MainSub != null && cam.Zoom < 5f) + if (MainSub != null && cam.Zoom < 5f) { - Vector2 position = Submarine.MainSub.SubBody != null ? Submarine.MainSub.WorldPosition : Submarine.MainSub.HiddenSubPosition; + Vector2 position = MainSub.SubBody != null ? MainSub.WorldPosition : MainSub.HiddenSubPosition; GUI.DrawIndicator( spriteBatch, position, cam, cam.WorldView.Width, - GUI.SubmarineIcon, Color.LightBlue * 0.5f); + GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.LightBlue * 0.5f); } - var notificationIcon = GUI.Style.GetComponentStyle("GUINotificationButton"); - var tooltipStyle = GUI.Style.GetComponentStyle("GUIToolTip"); + var notificationIcon = GUIStyle.GetComponentStyle("GUINotificationButton"); + var tooltipStyle = GUIStyle.GetComponentStyle("GUIToolTip"); foreach (Gap gap in Gap.GapList) { if (gap.linkedTo.Count == 2 && gap.linkedTo[0] == gap.linkedTo[1]) @@ -5310,7 +5370,7 @@ namespace Barotrauma Vector2 screenPos = Cam.WorldToScreen(gap.WorldPosition); Rectangle rect = new Rectangle(screenPos.ToPoint() - new Point(20), new Point(40)); tooltipStyle.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, Color.White); - notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUI.Style.Orange); + notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUIStyle.Orange); if (Vector2.Distance(PlayerInput.MousePosition, screenPos) < 30 * Cam.Zoom) { GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("gapinsidehullwarning"), new Rectangle(screenPos.ToPoint(), new Point(10))); @@ -5347,12 +5407,12 @@ namespace Barotrauma } } - GUI.DrawLine(spriteBatch, cam.WorldToScreen(startPos), cam.WorldToScreen(mouseWorldPos), GUI.Style.Green, width: 4); + GUI.DrawLine(spriteBatch, cam.WorldToScreen(startPos), cam.WorldToScreen(mouseWorldPos), GUIStyle.Green, width: 4); decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2); Vector2 offset = new Vector2(GUI.IntScale(24)); - GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUI.Style.TextColor, font: GUI.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); } spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 4d71cfc0a..c6d3deeb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -22,15 +22,15 @@ namespace Barotrauma private Submarine? submarine; private Character? dummyCharacter; - public static Effect BlueprintEffect; - private GUIFrame container; + public static Effect BlueprintEffect = null!; + private GUIFrame container = null!; - private TabMenu tabMenu; + private TabMenu? tabMenu; public TestScreen() { Cam = new Camera(); - BlueprintEffect = GameMain.GameScreen.BlueprintEffect; + BlueprintEffect = GameMain.GameScreen.BlueprintEffect!; new GUIButton(new RectTransform(new Point(256, 256), Frame.RectTransform), "Reload shader") { @@ -38,7 +38,7 @@ namespace Barotrauma { BlueprintEffect.Dispose(); GameMain.Instance.Content.Unload(); - BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); + BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl")!; GameMain.GameScreen.BlueprintEffect = BlueprintEffect; return true; } @@ -47,7 +47,7 @@ namespace Barotrauma } public override void Select() - { + { base.Select(); container = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); var tab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); @@ -79,7 +79,7 @@ namespace Barotrauma public override void Update(double deltaTime) { base.Update(deltaTime); - tabMenu.Update(); + tabMenu!.Update(); if (dummyCharacter is { } dummy) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index ab8ef0e8a..0b2da87b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -44,11 +44,11 @@ namespace Barotrauma /// /// Holds the references to the input fields. /// - public Dictionary Fields { get; private set; } = new Dictionary(); + public Dictionary Fields { get; private set; } = new Dictionary(); public void UpdateValue(SerializableProperty property, object newValue, bool flash = true) { - if (!Fields.TryGetValue(property.Name, out GUIComponent[] fields)) + if (!Fields.TryGetValue(property.Name.ToIdentifier(), out GUIComponent[] fields)) { DebugConsole.ThrowError($"No field for {property.Name} found!"); return; @@ -64,7 +64,7 @@ namespace Barotrauma numInput.FloatValue = f; if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -81,7 +81,7 @@ namespace Barotrauma numInput.IntValue = integer; if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -94,7 +94,7 @@ namespace Barotrauma tickBox.Selected = b; if (flash) { - tickBox.Flash(GUI.Style.Green); + tickBox.Flash(GUIStyle.Green); } } } @@ -105,7 +105,7 @@ namespace Barotrauma textBox.Text = s; if (flash) { - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); } } } @@ -116,7 +116,7 @@ namespace Barotrauma dropDown.Select((int)newValue); if (flash) { - dropDown.Flash(GUI.Style.Green); + dropDown.Flash(GUIStyle.Green); } } } @@ -132,7 +132,7 @@ namespace Barotrauma numInput.FloatValue = i == 0 ? v2.X : v2.Y; if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -161,7 +161,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -193,7 +193,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -225,7 +225,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -265,7 +265,7 @@ namespace Barotrauma } if (flash) { - numInput.Flash(GUI.Style.Green); + numInput.Flash(GUIStyle.Green); } } } @@ -281,27 +281,27 @@ namespace Barotrauma textBox.Text = a[i]; if (flash) { - textBox.Flash(GUI.Style.Green); + textBox.Flash(GUIStyle.Green); } } } } } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) : this(parent, entity, inGame ? SerializableProperty.GetProperties(entity).Union(SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? false)) : SerializableProperty.GetProperties(entity).Where(p => p.GetAttribute()?.IsEditable(entity) ?? true), showName, style, elementHeight, titleFont) { } - public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) + public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, IEnumerable properties, bool showName, string style = "", int elementHeight = 24, GUIFont titleFont = null) : base(style, new RectTransform(Vector2.One, parent)) { this.elementHeight = (int)(elementHeight * GUI.Scale); - var tickBoxStyle = GUI.Style.GetComponentStyle("GUITickBox"); - var textBoxStyle = GUI.Style.GetComponentStyle("GUITextBox"); - var numberInputStyle = GUI.Style.GetComponentStyle("GUINumberInput"); + var tickBoxStyle = GUIStyle.GetComponentStyle("GUITickBox"); + var textBoxStyle = GUIStyle.GetComponentStyle("GUITextBox"); + var numberInputStyle = GUIStyle.GetComponentStyle("GUINumberInput"); if (tickBoxStyle.Height.HasValue) { this.elementHeight = Math.Max(tickBoxStyle.Height.Value, this.elementHeight); } if (textBoxStyle.Height.HasValue) { this.elementHeight = Math.Max(textBoxStyle.Height.Value, this.elementHeight); } if (numberInputStyle.Height.HasValue) { this.elementHeight = Math.Max(numberInputStyle.Height.Value, this.elementHeight); } @@ -309,7 +309,7 @@ namespace Barotrauma layoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform)) { AbsoluteSpacing = (int)(5 * GUI.Scale) }; if (showName) { - new GUITextBlock(new RectTransform(new Point(layoutGroup.Rect.Width, this.elementHeight), layoutGroup.RectTransform, isFixedSize: true), entity.Name, font: titleFont ?? GUI.Font) + new GUITextBlock(new RectTransform(new Point(layoutGroup.Rect.Width, this.elementHeight), layoutGroup.RectTransform, isFixedSize: true), entity.Name, font: titleFont ?? GUIStyle.Font) { TextColor = Color.White, Color = Color.Black @@ -344,25 +344,24 @@ namespace Barotrauma value = ""; } - string propertyTag = (entity.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant(); - string fallbackTag = property.PropertyInfo.Name.ToLowerInvariant(); - string displayName = - TextManager.Get($"{propertyTag}", true, useEnglishAsFallBack: false) ?? - TextManager.Get($"sp.{propertyTag}.name", true, useEnglishAsFallBack: false); - if (string.IsNullOrEmpty(displayName)) + Identifier propertyTag = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); + Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier(); + LocalizedString displayName = + TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier()); + if (displayName.IsNullOrEmpty()) { Editable editable = property.GetAttribute(); if (editable != null && !string.IsNullOrEmpty(editable.FallBackTextTag)) { - displayName = TextManager.Get(editable.FallBackTextTag, true); + displayName = TextManager.Get(editable.FallBackTextTag); } else { - displayName = TextManager.Get(fallbackTag, true) ?? TextManager.Get($"sp.{fallbackTag}.name", true); + displayName = TextManager.Get(fallbackTag, $"sp.{fallbackTag}.name".ToIdentifier()); } } - if (displayName == null) + if (displayName.IsNullOrEmpty()) { displayName = property.Name.FormatCamelCaseWithSpaces(); #if DEBUG @@ -379,7 +378,7 @@ namespace Barotrauma #endif } - string toolTip = TextManager.Get($"sp.{propertyTag}.description", true, !string.IsNullOrEmpty(fallbackTag) ? $"sp.{fallbackTag}.description" : null); + LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description", $"sp.{fallbackTag}.description"); if (toolTip == null) { @@ -445,20 +444,20 @@ namespace Barotrauma return propertyField; } - public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, string displayName, string toolTip) + public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, LocalizedString displayName, LocalizedString toolTip) { var editableAttribute = property.GetAttribute(); if (editableAttribute.ReadOnly) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; var valueField = new GUITextBlock(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; return valueField; } @@ -466,7 +465,7 @@ namespace Barotrauma { GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) { - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Selected = value, ToolTip = toolTip, OnSelected = (tickBox) => @@ -489,15 +488,15 @@ namespace Barotrauma { propertyTickBox.Selected = (bool)property.GetValue(entity); }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyTickBox }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { propertyTickBox }); } return propertyTickBox; } } - public GUIComponent CreateIntField(ISerializableEntity entity, SerializableProperty property, int value, string displayName, string toolTip) + public GUIComponent CreateIntField(ISerializableEntity entity, SerializableProperty property, int value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -508,7 +507,7 @@ namespace Barotrauma var numberInput = new GUITextBlock(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; field = numberInput; } @@ -517,7 +516,7 @@ namespace Barotrauma var numberInput = new GUINumberInput(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), GUINumberInput.NumberType.Int) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = editableAttribute.MinValueInt; numberInput.MaxValueInt = editableAttribute.MaxValueInt; @@ -535,17 +534,17 @@ namespace Barotrauma }; field = numberInput; } - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { field }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { field }); } return frame; } - public GUIComponent CreateFloatField(ISerializableEntity entity, SerializableProperty property, float value, string displayName, string toolTip) + public GUIComponent CreateFloatField(ISerializableEntity entity, SerializableProperty property, float value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent) { CanBeFocused = false }; - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -554,7 +553,7 @@ namespace Barotrauma Anchor.TopRight), GUINumberInput.NumberType.Float) { ToolTip = toolTip, - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; var editableAttribute = property.GetAttribute(); numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -574,14 +573,14 @@ namespace Barotrauma { if (!numberInput.TextBox.Selected) { numberInput.FloatValue = (float)property.GetValue(entity); } }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { numberInput }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { numberInput }); } return frame; } - public GUIComponent CreateEnumField(ISerializableEntity entity, SerializableProperty property, object value, string displayName, string toolTip) + public GUIComponent CreateEnumField(ISerializableEntity entity, SerializableProperty property, object value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -607,14 +606,14 @@ namespace Barotrauma { if (!enumDropDown.Dropped) { enumDropDown.SelectItem(property.GetValue(entity)); } }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { enumDropDown }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { enumDropDown }); } return frame; } - public GUIComponent CreateEnumFlagField(ISerializableEntity entity, SerializableProperty property, object value, string displayName, string toolTip) + public GUIComponent CreateEnumFlagField(ISerializableEntity entity, SerializableProperty property, object value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -645,30 +644,30 @@ namespace Barotrauma return true; }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { enumDropDown }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { enumDropDown }); } return frame; } - public GUIComponent CreateStringField(ISerializableEntity entity, SerializableProperty property, string value, string displayName, string toolTip) + public GUIComponent CreateStringField(ISerializableEntity entity, SerializableProperty property, string value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUILayoutGroup(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont, textAlignment: Alignment.Left) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont, textAlignment: Alignment.Left) { ToolTip = toolTip }; - string translationTextTag = property.GetAttribute()?.translationTextTag; + Identifier translationTextTag = property.GetAttribute()?.TranslationTextTag ?? Identifier.Empty; float browseButtonWidth = 0.1f; var editableAttribute = property.GetAttribute(); float textBoxWidth = inputFieldWidth; - if (translationTextTag != null) { textBoxWidth -= browseButtonWidth; } + if (!translationTextTag.IsEmpty) { textBoxWidth -= browseButtonWidth; } GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(textBoxWidth, 1), frame.RectTransform)) { Enabled = editableAttribute != null && !editableAttribute.ReadOnly, ToolTip = toolTip, - Font = GUI.SmallFont, + Font = GUIStyle.SmallFont, Text = value, OverflowClip = true }; @@ -702,7 +701,7 @@ namespace Barotrauma { TrySendNetworkUpdate(entity, property); textBox.Text = (string) property.GetValue(entity); - textBox.Flash(GUI.Style.Green, flashDuration: 1f); + textBox.Flash(GUIStyle.Green, flashDuration: 1f); } //restore the entities that were selected before applying MapEntity.SelectedList.Clear(); @@ -713,23 +712,23 @@ namespace Barotrauma return true; } - if (translationTextTag != null) + if (!translationTextTag.IsEmpty) { new GUIButton(new RectTransform(new Vector2(browseButtonWidth, 1), frame.RectTransform, Anchor.TopRight), "...", style: "GUIButtonSmall") { - OnClicked = (bt, userData) => { CreateTextPicker(translationTextTag, entity, property, propertyBox); return true; } + OnClicked = (bt, userData) => { CreateTextPicker(translationTextTag.Value, entity, property, propertyBox); return true; } }; propertyBox.OnTextChanged += (tb, text) => { - string translatedText = TextManager.Get(text, returnNull: true); - if (translatedText == null) + LocalizedString translatedText = TextManager.Get(text); + if (translatedText.IsNullOrEmpty()) { propertyBox.TextColor = Color.Gray; propertyBox.ToolTip = TextManager.GetWithVariable("StringPropertyCannotTranslate", "[tag]", text ?? string.Empty); } else { - propertyBox.TextColor = GUI.Style.Green; + propertyBox.TextColor = GUIStyle.Green; propertyBox.ToolTip = TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", translatedText); } return true; @@ -737,14 +736,14 @@ namespace Barotrauma propertyBox.Text = value; } frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyBox }); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), new GUIComponent[] { propertyBox }); } return frame; } - public GUIComponent CreatePointField(ISerializableEntity entity, SerializableProperty property, Point value, string displayName, string toolTip) + public GUIComponent CreatePointField(ISerializableEntity entity, SerializableProperty property, Point value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -759,17 +758,17 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; if (i == 0) @@ -806,14 +805,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateVector2Field(ISerializableEntity entity, SerializableProperty property, Vector2 value, string displayName, string toolTip) + public GUIComponent CreateVector2Field(ISerializableEntity entity, SerializableProperty property, Vector2 value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -828,16 +827,16 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -876,14 +875,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateVector3Field(ISerializableEntity entity, SerializableProperty property, Vector3 value, string displayName, string toolTip) + public GUIComponent CreateVector3Field(ISerializableEntity entity, SerializableProperty property, Vector3 value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -898,17 +897,17 @@ namespace Barotrauma { var element = new GUIFrame(new RectTransform(new Vector2(0.33f, 1), inputArea.RectTransform), style: null); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -952,14 +951,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateVector4Field(ISerializableEntity entity, SerializableProperty property, Vector4 value, string displayName, string toolTip) + public GUIComponent CreateVector4Field(ISerializableEntity entity, SerializableProperty property, Vector4 value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -974,17 +973,17 @@ 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); - string componentLabel = GUI.vectorComponentLabels[i]; + LocalizedString componentLabel = GUI.VectorComponentLabels[i]; if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) { componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); } - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueFloat = editableAttribute.MinValueFloat; @@ -1033,14 +1032,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateColorField(ISerializableEntity entity, SerializableProperty property, Color value, string displayName, string toolTip) + public GUIComponent CreateColorField(ISerializableEntity entity, SerializableProperty property, Color value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform) { MinSize = new Point(80, 26) }, displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - largeInputFieldWidth, 1), frame.RectTransform) { MinSize = new Point(80, 26) }, displayName, font: GUIStyle.SmallFont) { ToolTip = displayName + '\n' + toolTip }; @@ -1073,11 +1072,11 @@ namespace Barotrauma { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), element.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; numberInput.MinValueInt = 0; numberInput.MaxValueInt = 255; @@ -1091,7 +1090,7 @@ namespace Barotrauma else numberInput.IntValue = value.A; - numberInput.Font = GUI.SmallFont; + numberInput.Font = GUIStyle.SmallFont; int comp = i; numberInput.OnValueChanged += (numInput) => @@ -1127,14 +1126,14 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateRectangleField(ISerializableEntity entity, SerializableProperty property, Rectangle value, string displayName, string toolTip) + public GUIComponent CreateRectangleField(ISerializableEntity entity, SerializableProperty property, Rectangle value, LocalizedString displayName, LocalizedString toolTip) { var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = displayName + '\n' + toolTip }; @@ -1148,11 +1147,11 @@ namespace Barotrauma for (int i = 3; i >= 0; i--) { 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: GUI.SmallFont, textAlignment: Alignment.Center); + 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) { - Font = GUI.SmallFont + Font = GUIStyle.SmallFont }; // Not sure if the min value could in any case be negative. numberInput.MinValueInt = 0; @@ -1199,15 +1198,15 @@ namespace Barotrauma ((GUINumberInput)fields[3]).IntValue = value.Height; } }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } - public GUIComponent CreateStringArrayField(ISerializableEntity entity, SerializableProperty property, string[] value, string displayName, string toolTip) + public GUIComponent CreateStringArrayField(ISerializableEntity entity, SerializableProperty property, string[] value, LocalizedString displayName, LocalizedString toolTip) { int elementCount = (value.Length + 1); var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementCount * elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), frame.RectTransform), displayName, font: GUI.SmallFont) + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), frame.RectTransform), displayName, font: GUIStyle.SmallFont) { ToolTip = toolTip }; @@ -1225,8 +1224,8 @@ namespace Barotrauma var elementLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, element.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); // Set the label to be (i + 1) so it's easier to understand for non-programmers string componentLabel = (i + 1).ToString(); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); - GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUI.SmallFont }; + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUIStyle.SmallFont, textAlignment: Alignment.Center); + GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUIStyle.SmallFont }; int comp = i; textBox.OnEnterPressed += (textBox, text) => OnApply(textBox); textBox.OnDeselected += (textBox, keys) => OnApply(textBox); @@ -1243,13 +1242,13 @@ namespace Barotrauma if (SetPropertyValue(property, entity, newValue)) { TrySendNetworkUpdate(entity, property); - textBox.Flash(color: GUI.Style.Green, flashDuration: 1f); + textBox.Flash(color: GUIStyle.Green, flashDuration: 1f); } } else { textBox.Text = newValue[comp]; - textBox.Flash(color: GUI.Style.Red, flashDuration: 1f); + textBox.Flash(color: GUIStyle.Red, flashDuration: 1f); } return true; } @@ -1268,13 +1267,13 @@ namespace Barotrauma }; frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Sum(c => c.MinSize.Y)); - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name.ToIdentifier(), fields); } return frame; } public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { - var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -1293,16 +1292,15 @@ namespace Barotrauma } }; - textTag = textTag.ToLowerInvariant(); - var tagTextPairs = TextManager.GetAllTagTextPairs(); + var tagTextPairs = TextManager.GetAllTagTextPairs().ToList(); tagTextPairs.Sort((t1, t2) => { return t1.Value.CompareTo(t2.Value); }); - foreach (KeyValuePair tagTextPair in tagTextPairs) + foreach (KeyValuePair tagTextPair in tagTextPairs) { if (!tagTextPair.Key.StartsWith(textTag)) { continue; } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(tagTextPair.Value, GUI.Font, textList.Content.Rect.Width)) + ToolBox.LimitString(tagTextPair.Value, GUIStyle.Font, textList.Content.Rect.Width)) { - UserData = tagTextPair.Key + UserData = tagTextPair.Key.ToString() }; } } @@ -1352,7 +1350,7 @@ namespace Barotrauma } }); - PropertyCommand cmd = new PropertyCommand(entities, property.Name, value, oldValues); + PropertyCommand cmd = new PropertyCommand(entities, property.Name.ToIdentifier(), value, oldValues); if (CommandBuffer != null) { if (CommandBuffer.Item1 == property && CommandBuffer.Item2.PropertyCount == cmd.PropertyCount) @@ -1416,8 +1414,7 @@ namespace Barotrauma else if (entity is ISerializableEntity { SerializableProperties: { } } sEntity) { var props = sEntity.SerializableProperties; - - if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + if (props.TryGetValue(property.Name.ToIdentifier(), out SerializableProperty foundProp) && foundProp.Attributes.OfType().Any()) { SafeAdd(sEntity, foundProp); foundProp.PropertyInfo.SetValue(entity, value); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs new file mode 100644 index 000000000..f874026f3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/CompletedTutorials.cs @@ -0,0 +1,45 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + public class CompletedTutorials + { + private readonly HashSet identifiers = new HashSet(); + + private CompletedTutorials() { } + + private CompletedTutorials(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + identifiers.Add(subElement.GetAttributeIdentifier("name", Identifier.Empty)); + } + } + + public static void Init(XElement? element) + { + if (element is null) { return; } + + Instance = new CompletedTutorials(element); + } + + public void SaveTo(XElement element) + { + identifiers.ForEach(id => new XElement("Tutorial", new XAttribute("name", id.Value))); + } + + public bool Contains(Identifier identifier) => identifiers.Contains(identifier); + + public void Add(Identifier identifier) => identifiers.Add(identifier); + + public void Remove(Identifier identifier) => identifiers.Remove(identifier); + + public static CompletedTutorials Instance { get; private set; } = new CompletedTutorials(); + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs new file mode 100644 index 000000000..f7471c950 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/DebugConsoleMapping.cs @@ -0,0 +1,58 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma.ClientSource.Settings +{ + public class DebugConsoleMapping + { + private readonly Dictionary bindings = new Dictionary(); + public IReadOnlyDictionary Bindings => bindings; + + private DebugConsoleMapping() { } + + private DebugConsoleMapping(XElement element) + { + var bindings = new Dictionary(); + foreach (var subElement in element.Elements()) + { + KeyOrMouse keyOrMouse = subElement.GetAttributeKeyOrMouse("key", MouseButton.None); + if (keyOrMouse == MouseButton.None) { continue; } + string command = subElement.GetAttributeString("command", ""); + if (command.IsNullOrWhiteSpace()) { continue; } + bindings[keyOrMouse] = command; + } + + this.bindings = bindings; + } + + public static void Init(XElement? element) + { + if (element is null) { return; } + + Instance = new DebugConsoleMapping(element); + } + + public void SaveTo(XElement element) + { + Bindings + .ForEach(kvp => element.Add( + new XElement("Keybind", + new XAttribute("key", kvp.Key), + new XAttribute("command", kvp.Value)))); + } + + public void Set(KeyOrMouse key, string command) + => bindings[key] = command; + + public void Remove(KeyOrMouse key) + => bindings.Remove(key); + + public static DebugConsoleMapping Instance { get; private set; } = new DebugConsoleMapping(); + } + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs new file mode 100644 index 000000000..9410a39b6 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/IgnoredHints.cs @@ -0,0 +1,42 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class IgnoredHints + { + private readonly HashSet identifiers = new HashSet(); + + private IgnoredHints() { } + + private IgnoredHints(XElement element) + { + identifiers = element.GetAttributeIdentifierArray("identifiers", Array.Empty()) + .ToHashSet(); + } + + public static void Init(XElement? element) + { + if (element is null) { return; } + + Instance = new IgnoredHints(element); + } + + public void SaveTo(XElement element) + { + element.SetAttributeValue("identifiers", string.Join(",", identifiers)); + } + + public bool Contains(Identifier identifier) => identifiers.Contains(identifier); + + public void Add(Identifier identifier) => identifiers.Add(identifier); + + public void Remove(Identifier identifier) => identifiers.Remove(identifier); + + public static IgnoredHints Instance { get; private set; } = new IgnoredHints(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs new file mode 100644 index 000000000..a1cb8dcf1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/MultiplayerPreferences.cs @@ -0,0 +1,112 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + class MultiplayerPreferences + { + public readonly struct JobPreference + { + public JobPreference(Identifier jobIdentifier, int variant) + { + JobIdentifier = jobIdentifier; + Variant = variant; + } + + public JobPreference(XElement element) : this( + element.GetAttributeIdentifier("identifier", Identifier.Empty), + element.GetAttributeInt("variant", -1)) { } + + public readonly Identifier JobIdentifier; + public readonly int Variant; + + public static bool operator ==(JobPreference a, JobPreference b) + => a.JobIdentifier == b.JobIdentifier && a.Variant == b.Variant; + + public static bool operator !=(JobPreference a, JobPreference b) => !(a == b); + + public override bool Equals(object? obj) + => obj is JobPreference jp && jp == this; + + public bool Equals(JobPreference other) => other == this; + + public override int GetHashCode() => HashCode.Combine(JobIdentifier, Variant); + } + + public readonly List JobPreferences = new List(); + public CharacterTeamType TeamPreference; + public string PlayerName = string.Empty; + + public readonly HashSet TagSet = new HashSet(); + public int HairIndex = -1; + public int BeardIndex = -1; + public int MoustacheIndex = -1; + public int FaceAttachmentIndex = -1; + public Color HairColor = Color.Black; + public Color FacialHairColor = Color.Black; + public Color SkinColor = Color.Black; + + public static MultiplayerPreferences Instance { get; private set; } = new MultiplayerPreferences(); + + private MultiplayerPreferences() { } + + private MultiplayerPreferences(IEnumerable elements) + { + foreach (var element in elements) + { + PlayerName = element.GetAttributeString("name", PlayerName); + + TagSet.UnionWith(element.GetAttributeIdentifierArray("tags", Array.Empty())); + HairIndex = element.GetAttributeInt(nameof(HairIndex), HairIndex); + BeardIndex = element.GetAttributeInt(nameof(BeardIndex), BeardIndex); + MoustacheIndex = element.GetAttributeInt(nameof(MoustacheIndex), MoustacheIndex); + FaceAttachmentIndex = element.GetAttributeInt(nameof(FaceAttachmentIndex), FaceAttachmentIndex); + + HairColor = element.GetAttributeColor(nameof(HairColor), HairColor); + FacialHairColor = element.GetAttributeColor(nameof(FacialHairColor), FacialHairColor); + SkinColor = element.GetAttributeColor(nameof(SkinColor), SkinColor); + + foreach (var subElement in element.GetChildElements("job")) + { + JobPreferences.Add(new JobPreference(subElement)); + } + } + } + + public static void Init(params XElement?[] elements) + { + Instance = new MultiplayerPreferences(elements.Where(e => e != null)!); + } + + public void SaveTo(XElement element) + { + element.SetAttributeValue("name", PlayerName); + + element.SetAttributeValue("tags", string.Join(",", TagSet)); + element.SetAttributeValue(nameof(HairIndex), HairIndex); + element.SetAttributeValue(nameof(BeardIndex), BeardIndex); + element.SetAttributeValue(nameof(MoustacheIndex), MoustacheIndex); + element.SetAttributeValue(nameof(FaceAttachmentIndex), FaceAttachmentIndex); + + element.SetAttributeValue(nameof(HairColor), HairColor.ToStringHex()); + element.SetAttributeValue(nameof(FacialHairColor), FacialHairColor.ToStringHex()); + element.SetAttributeValue(nameof(SkinColor), SkinColor.ToStringHex()); + + foreach (var jobPreference in JobPreferences) + { + element.Add(new XElement("job", + new XAttribute("identifier", jobPreference.JobIdentifier.Value), + new XAttribute("variant", jobPreference.Variant.ToString(CultureInfo.InvariantCulture)))); + } + } + + public bool AreJobPreferencesEqual(IReadOnlyList other) + => JobPreferences.SequenceEqual(other); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs new file mode 100644 index 000000000..48bde97dc --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/ServerListFilters.cs @@ -0,0 +1,66 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + #warning TODO: implement properly + public class ServerListFilters + { + private readonly Dictionary attributes = new Dictionary(); + + private ServerListFilters() { } + + private ServerListFilters(XElement elem) + { + if (elem == null) { return; } + foreach (var attr in elem.Attributes()) + { + attributes.Add(attr.NameAsIdentifier(), attr.Value); + } + } + + public static void Init(XElement? elem) + { + if (elem is null) { return; } + + Instance = new ServerListFilters(elem); + } + + public void SaveTo(XElement elem) + { + foreach (var kvp in attributes) + { + elem.Add(new XAttribute(kvp.Key.Value, kvp.Value)); + } + } + + public bool GetAttributeBool(Identifier key, bool def) + { + if (attributes.TryGetValue(key, out string? val)) + { + if (bool.TryParse(val, out bool result)) { return result; } + } + + return def; + } + + public T GetAttributeEnum(Identifier key, T def) where T : struct, Enum + { + if (attributes.TryGetValue(key, out string? val)) + { + if (Enum.TryParse(val, out T result)) { return result; } + } + + return def; + } + + public void SetAttribute(Identifier key, string val) + { + attributes[key] = val; + } + + public static ServerListFilters Instance { get; private set; } = new ServerListFilters(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs new file mode 100644 index 000000000..c809ad75f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -0,0 +1,752 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using OpenAL; + +namespace Barotrauma +{ + public class SettingsMenu + { + public static SettingsMenu? Instance { get; private set; } + + public enum Tab + { + Graphics, + AudioAndVC, + Controls, + Gameplay, + Mods + } + + private GameSettings.Config unsavedConfig; + + private readonly GUIFrame mainFrame; + + private readonly GUILayoutGroup tabber; + private readonly GUIFrame contentFrame; + private readonly GUILayoutGroup bottom; + + public readonly WorkshopMenu WorkshopMenu; + + public static SettingsMenu Create(RectTransform mainParent) + { + Instance?.Close(); + Instance = new SettingsMenu(mainParent); + return Instance; + } + + private SettingsMenu(RectTransform mainParent) + { + unsavedConfig = GameSettings.CurrentConfig; + + mainFrame = new GUIFrame(new RectTransform(Vector2.One, mainParent)); + + var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, mainFrame.RectTransform, Anchor.Center, Pivot.Center), + isHorizontal: false, childAnchor: Anchor.TopRight); + + new GUITextBlock(new RectTransform((1.0f, 0.07f), mainLayout.RectTransform), TextManager.Get("Settings"), + font: GUIStyle.LargeFont); + + var tabberAndContentLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.86f), mainLayout.RectTransform), + isHorizontal: true); + + void tabberPadding() + => new GUIFrame(new RectTransform((0.01f, 1.0f), tabberAndContentLayout.RectTransform), style: null); + + tabberPadding(); + tabber = new GUILayoutGroup(new RectTransform((0.06f, 1.0f), tabberAndContentLayout.RectTransform), isHorizontal: false) { AbsoluteSpacing = GUI.IntScale(5f) }; + tabberPadding(); + tabContents = new Dictionary(); + + contentFrame = new GUIFrame(new RectTransform((0.92f, 1.0f), tabberAndContentLayout.RectTransform), + style: "InnerFrame"); + + bottom = new GUILayoutGroup(new RectTransform((contentFrame.RectTransform.RelativeSize.X, 0.04f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; + + CreateGraphicsTab(); + CreateAudioAndVCTab(); + CreateControlsTab(); + CreateGameplayTab(); + CreateModsTab(out WorkshopMenu); + + CreateBottomButtons(); + + SelectTab(Tab.Graphics); + + tabber.Recalculate(); + } + + private void SwitchContent(GUIFrame newContent) + { + contentFrame.Children.ForEach(c => c.Visible = false); + newContent.Visible = true; + } + + private readonly Dictionary tabContents; + + public void SelectTab(Tab tab) + { + SwitchContent(tabContents[tab].Content); + tabber.Children.ForEach(c => + { + if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; } + }); + } + + private void AddButtonToTabber(Tab tab, GUIFrame content) + { + var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.TopLeft, Pivot.TopLeft, scaleBasis: ScaleBasis.Smallest), "", style: $"SettingsMenuTab.{tab}") + { + ToolTip = TextManager.Get($"SettingsTab.{tab}"), + OnClicked = (b, _) => + { + SelectTab(tab); + return false; + } + }; + button.RectTransform.MaxSize = RectTransform.MaxPoint; + button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + + tabContents.Add(tab, (button, content)); + } + + private GUIFrame CreateNewContentFrame(Tab tab) + { + var content = new GUIFrame(new RectTransform(Vector2.One * 0.95f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); + AddButtonToTabber(tab, content); + return content; + } + + private static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false) + { + GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true); + GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null); + if (split) + { + new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.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((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); + return (left, right); + } + + private static GUILayoutGroup CreateCenterLayout(GUIFrame parent) + { + return new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.TopCenter, Pivot.TopCenter)) { ChildAnchor = Anchor.TopCenter }; + } + + private static RectTransform NewItemRectT(GUILayoutGroup parent) + => new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft); + + private static void Spacer(GUILayoutGroup parent) + { + new GUIFrame(new RectTransform((1.0f, 0.03f), parent.RectTransform, Anchor.CenterLeft), style: null); + } + + private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font) + { + return new GUITextBlock(NewItemRectT(parent), str, font: font); + } + + private static void DropdownEnum(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, T currentValue, + Action setter) where T : Enum + => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); + + private static void Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) + { + var dropdown = new GUIDropDown(NewItemRectT(parent)); + values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); + dropdown.Select(values.IndexOf(currentValue)); + dropdown.OnSelected = (dd, obj) => + { + setter((T)obj); + return true; + }; + } + + 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") + { + Range = range, + BarScrollValue = currentValue, + Step = 1.0f / (float)(steps - 1), + BarSize = 1.0f / steps + }; + if (tooltip != null) + { + slider.ToolTip = tooltip; + } + var label = new GUITextBlock(new RectTransform((0.18f, 1.0f), layout.RectTransform), + labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center); + slider.OnMoved = (sb, val) => + { + label.Text = labelFunc(sb.BarScrollValue); + setter(sb.BarScrollValue); + return true; + }; + } + + private void Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) + { + var tickbox = new GUITickBox(NewItemRectT(parent), label) + { + Selected = currentValue, + ToolTip = tooltip, + OnSelected = (tb) => + { + setter(tb.Selected); + return true; + } + }; + } + + 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); + + private void CreateGraphicsTab() + { + GUIFrame content = CreateNewContentFrame(Tab.Graphics); + + var (left, right) = CreateSidebars(content); + + List<(int Width, int Height)> supportedResolutions = + GameMain.GraphicsDeviceManager.GraphicsDevice.Adapter.SupportedDisplayModes + .Where(m => m.Format == SurfaceFormat.Color) + .Select(m => (m.Width, m.Height)) + .ToList(); + var currentResolution = (unsavedConfig.Graphics.Width, unsavedConfig.Graphics.Height); + if (!supportedResolutions.Contains(currentResolution)) + { + supportedResolutions.Add(currentResolution); + } + + Label(left, TextManager.Get("Resolution"), GUIStyle.SubHeadingFont); + Dropdown(left, (m) => $"{m.Width}x{m.Height}", null, supportedResolutions, currentResolution, + (res) => + { + unsavedConfig.Graphics.Width = res.Width; + unsavedConfig.Graphics.Height = res.Height; + }); + Spacer(left); + + Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont); + DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, (v) => unsavedConfig.Graphics.DisplayMode = v); + Spacer(left); + + 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")); + 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); + } + + private static string TrimAudioDeviceName(string name) + { + if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } + string[] prefixes = { "OpenAL Soft on " }; + foreach (string prefix in prefixes) + { + if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return name.Remove(0, prefix.Length); + } + } + return name; + } + + private static int HandleAlErrors(string message) + { + int alcError = Alc.GetError(IntPtr.Zero); + if (alcError != Alc.NoError) + { + DebugConsole.ThrowError($"{message}: ALC error {Alc.GetErrorString(alcError)}"); + return alcError; + } + + int alError = Al.GetError(); + if (alError != Al.NoError) + { + DebugConsole.ThrowError($"{message}: AL error {Al.GetErrorString(alError)}"); + return alError; + } + + return Al.NoError; + } + + private static void GetAudioDevices(int listSpecifier, int defaultSpecifier, out IReadOnlyList list, ref string current) + { + list = Array.Empty(); + + var retVal = Alc.GetStringList(IntPtr.Zero, listSpecifier).ToList(); + if (HandleAlErrors("Alc.GetStringList failed") != Al.NoError) { return; } + + list = retVal; + if (string.IsNullOrEmpty(current)) + { + current = Alc.GetString(IntPtr.Zero, defaultSpecifier); + if (HandleAlErrors("Alc.GetString failed") != Al.NoError) { return; } + } + + string currentVal = current; + if (list.Any() && !list.Any(n => n.Equals(currentVal, StringComparison.OrdinalIgnoreCase))) + { + current = list[0]; + } + } + + private void CreateAudioAndVCTab() + { + if (GameMain.Client == null + && VoipCapture.Instance == null) + { + string currDevice = unsavedConfig.Audio.VoiceCaptureDevice; + GetAudioDevices(Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, out var deviceList, ref currDevice); + + if (deviceList.Any()) + { + VoipCapture.Create(unsavedConfig.Audio.VoiceCaptureDevice); + } + if (VoipCapture.Instance == null) + { + unsavedConfig.Audio.VoiceSetting = VoiceMode.Disabled; + } + } + + GUIFrame content = CreateNewContentFrame(Tab.AudioAndVC); + + var (audio, voiceChat) = CreateSidebars(content, split: true); + + static void audioDeviceElement( + GUILayoutGroup parent, + Action setter, + int listSpecifier, + int defaultSpecifier, + ref string currentDevice) + { +#if OSX + //At the time of writing there are no OpenAL implementations + //on macOS that return the list of available devices, or + //allow selecting any other than the default one. I'm not + //about to write my own OpenAL implementation to fix this + //so here's a workaround instead, just a label that shows the + //name of the current device. + var deviceNameContainerElement = new GUIFrame(NewItemRectT(parent), style: "GUITextBoxNoIcon"); + var deviceNameElement = new GUITextBlock(new RectTransform(Vector2.One, deviceNameContainerElement.RectTransform), currentDevice, textAlignment: Alignment.CenterLeft); + new GUICustomComponent(new RectTransform(Vector2.Zero, deviceNameElement.RectTransform), onUpdate: + (deltaTime, component) => + { + deviceNameElement.Text = Alc.GetString(IntPtr.Zero, listSpecifier); + }); +#else + GetAudioDevices(listSpecifier, defaultSpecifier, out var devices, ref currentDevice); + Dropdown(parent, v => TrimAudioDeviceName(v), null, devices, currentDevice, setter); +#endif + } + + Label(audio, TextManager.Get("AudioOutputDevice"), GUIStyle.SubHeadingFont); + + string currentOutputDevice = unsavedConfig.Audio.AudioOutputDevice; + audioDeviceElement(audio, v => unsavedConfig.Audio.AudioOutputDevice = v, Alc.OutputDevicesSpecifier, Alc.DefaultDeviceSpecifier, ref currentOutputDevice); + Spacer(audio); + + Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, (v) => unsavedConfig.Audio.SoundVolume = v); + + Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, (v) => unsavedConfig.Audio.MusicVolume = v); + + Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, (v) => unsavedConfig.Audio.MuteOnFocusLost = v); + Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, (v) => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); + Spacer(audio); + + Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); + Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, (v) => unsavedConfig.Audio.VoiceChatVolume = v); + + Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, (v) => unsavedConfig.Audio.UseDirectionalVoiceChat = v); + Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, (v) => unsavedConfig.Audio.VoipAttenuationEnabled = v); + + Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont); + + string currentInputDevice = unsavedConfig.Audio.VoiceCaptureDevice; + audioDeviceElement(voiceChat, v => unsavedConfig.Audio.VoiceCaptureDevice = v, Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, ref currentInputDevice); + Spacer(voiceChat); + + Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); + DropdownEnum(voiceChat, (v) => TextManager.Get($"VoiceMode.{v}"), (v) => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, (v) => unsavedConfig.Audio.VoiceSetting = v); + Spacer(voiceChat); + + var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont); + var dbMeter = new GUIProgressBar(NewItemRectT(voiceChat), 0.0f, Color.Lime); + dbMeter.ProgressGetter = () => + { + if (VoipCapture.Instance == null) { return 0.0f; } + + dbMeter.Color = unsavedConfig.Audio.VoiceSetting switch + { + VoiceMode.Activity => VoipCapture.Instance.LastdB > unsavedConfig.Audio.NoiseGateThreshold ? GUIStyle.Green : GUIStyle.Orange, + VoiceMode.PushToTalk => GUIStyle.Green, + VoiceMode.Disabled => Color.LightGray + }; + + float scrollVal = double.IsNegativeInfinity(VoipCapture.Instance.LastdB) ? 0.0f : ((float)VoipCapture.Instance.LastdB + 100.0f) / 100.0f; + return scrollVal * scrollVal; + }; + var noiseGateSlider = new GUIScrollBar(new RectTransform(Vector2.One, dbMeter.RectTransform, Anchor.Center), color: Color.White, + style: "GUISlider", barSize: 0.03f); + noiseGateSlider.Frame.Visible = false; + noiseGateSlider.Step = 0.01f; + noiseGateSlider.Range = new Vector2(-100.0f, 0.0f); + noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, unsavedConfig.Audio.NoiseGateThreshold); + noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll; + noiseGateSlider.OnMoved = (scrollBar, barScroll) => + { + unsavedConfig.Audio.NoiseGateThreshold = MathHelper.Lerp(-100.0f, 0.0f, (float)Math.Sqrt(scrollBar.BarScroll)); + return true; + }; + new GUICustomComponent(new RectTransform(Vector2.Zero, voiceChat.RectTransform), onUpdate: + (deltaTime, component) => + { + noiseGateThresholdLabel.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity; + noiseGateSlider.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity; + }); + Spacer(voiceChat); + + Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont); + Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, (v) => unsavedConfig.Audio.MicrophoneVolume = v); + Spacer(voiceChat); + + Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont); + Slider(voiceChat, (0, 500), 26, (v) => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, (v) => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); + } + + private void CreateControlsTab() + { + GUIFrame content = CreateNewContentFrame(Tab.Controls); + + GUILayoutGroup layout = CreateCenterLayout(content); + + Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont); + Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, (v) => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); + Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, (v) => unsavedConfig.EnableMouseLook = v); + Spacer(layout); + + GUIListBox keyMapList = + new GUIListBox(new RectTransform((2.0f, 0.7f), + layout.RectTransform)) + { + CanBeFocused = false, + OnSelected = (_, __) => false + }; + Spacer(layout); + + GUILayoutGroup createInputRowLayout() + => new GUILayoutGroup(new RectTransform((1.0f, 0.1f), keyMapList.Content.RectTransform), isHorizontal: true); + + HashSet inputButtons = new HashSet(); + Action? currentSetter = null; + void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter) + { + var inputFrame = new GUIFrame(new RectTransform((0.5f, 1.0f), currRow.RectTransform), + style: null); + var label = new GUITextBlock(new RectTransform((0.6f, 1.0f), inputFrame.RectTransform), labelText, + font: GUIStyle.SmallFont) {ForceUpperCase = ForceUpperCase.Yes}; + var inputBox = new GUIButton( + new RectTransform((0.4f, 1.0f), inputFrame.RectTransform, Anchor.TopRight, Pivot.TopRight), + valueNameGetter(), style: "GUITextBoxNoIcon") + { + OnClicked = (btn, obj) => + { + inputButtons.ForEach(b => + { + if (b != btn) { b.Selected = false; } + }); + bool willBeSelected = !btn.Selected; + if (willBeSelected) + { + currentSetter = (v) => + { + valueSetter(v); + btn.Text = valueNameGetter(); + }; + } + else + { + currentSetter = null; + } + + btn.Selected = willBeSelected; + return true; + } + }; + inputButtons.Add(inputBox); + } + + var inputListener = new GUICustomComponent(new RectTransform(Vector2.Zero, layout.RectTransform), onUpdate: (deltaTime, component) => + { + if (currentSetter is null) { return; } + + void clearSetter() + { + currentSetter = null; + inputButtons.ForEach(b => b.Selected = false); + } + + void callSetter(KeyOrMouse v) + { + currentSetter?.Invoke(v); + clearSetter(); + } + + var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys(); + if ((pressedKeys?.Any() ?? false)) + { + if (pressedKeys.Contains(Keys.Escape)) + { + clearSetter(); + } + else + { + callSetter(pressedKeys.First()); + } + } + else if (PlayerInput.PrimaryMouseButtonClicked() && !(GUI.MouseOn is GUIButton)) + { + callSetter(MouseButton.PrimaryMouse); + } + else if (PlayerInput.SecondaryMouseButtonClicked()) + { + callSetter(MouseButton.SecondaryMouse); + } + else if (PlayerInput.MidButtonClicked()) + { + callSetter(MouseButton.MiddleMouse); + } + else if (PlayerInput.Mouse4ButtonClicked()) + { + callSetter(MouseButton.MouseButton4); + } + else if (PlayerInput.Mouse5ButtonClicked()) + { + callSetter(MouseButton.MouseButton5); + } + else if (PlayerInput.MouseWheelUpClicked()) + { + callSetter(MouseButton.MouseWheelUp); + } + else if (PlayerInput.MouseWheelDownClicked()) + { + callSetter(MouseButton.MouseWheelDown); + } + }); + + InputType[] inputTypes = (InputType[])Enum.GetValues(typeof(InputType)); + InputType[][] inputTypeColumns = + { + inputTypes.Take(inputTypes.Length - (inputTypes.Length / 2)).ToArray(), + inputTypes.TakeLast(inputTypes.Length / 2).ToArray() + }; + for (int i = 0; i < inputTypes.Length; i+=2) + { + var currRow = createInputRowLayout(); + for (int j = 0; j < 2; j++) + { + var column = inputTypeColumns[j]; + if (i / 2 >= column.Length) { break; } + var input = column[i / 2]; + addInputToRow( + currRow, + TextManager.Get($"InputType.{input}"), + () => unsavedConfig.KeyMap.Bindings[input].Name, + (v) => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v)); + } + } + + for (int i = 0; i < unsavedConfig.InventoryKeyMap.Bindings.Length; i += 2) + { + var currRow = createInputRowLayout(); + for (int j = 0; j < 2; j++) + { + int currIndex = i + j; + if (currIndex >= unsavedConfig.InventoryKeyMap.Bindings.Length) { break; } + + var input = unsavedConfig.InventoryKeyMap.Bindings[currIndex]; + addInputToRow( + currRow, + 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) + { + RelativeSpacing = 0.1f + }; + + var defaultBindingsButton = + new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), + TextManager.Get("SetDefaultBindings"), 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") + }; + } + + private void CreateGameplayTab() + { + GUIFrame content = CreateNewContentFrame(Tab.Gameplay); + + GUILayoutGroup layout = CreateCenterLayout(content); + + var languages = TextManager.AvailableLanguages + .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) + .ToArray(); + Label(layout, TextManager.Get("Language"), GUIStyle.SubHeadingFont); + Dropdown(layout, (v) => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, (v) => unsavedConfig.Language = v); + Spacer(layout); + + Tickbox(layout, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, (v) => unsavedConfig.PauseOnFocusLost = v); + Spacer(layout); + + Tickbox(layout, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, (v) => unsavedConfig.DisableInGameHints = v); + var resetInGameHintsButton = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), layout.RectTransform), + TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") + { + ToolTip = TextManager.Get("ResetInGameHintsTooltip") + }; + Spacer(layout); + + Label(layout, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, (v) => unsavedConfig.Graphics.HUDScale = v); + Label(layout, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, (v) => unsavedConfig.Graphics.InventoryScale = v); + Label(layout, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); + Slider(layout, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, (v) => unsavedConfig.Graphics.TextScale = v); + +#if !OSX + Spacer(layout); + var statisticsTickBox = new GUITickBox(NewItemRectT(layout), TextManager.Get("statisticsconsenttickbox")) + { + OnSelected = tickBox => + { + GameAnalyticsManager.SetConsent( + tickBox.Selected + ? GameAnalyticsManager.Consent.Ask + : GameAnalyticsManager.Consent.No); + return false; + } + }; +#if DEBUG + statisticsTickBox.Enabled = false; +#endif + void updateGATickBoxToolTip() + => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); + updateGATickBoxToolTip(); + + var cachedConsent = GameAnalyticsManager.Consent.Unknown; + var statisticsTickBoxUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), + onUpdate: (deltaTime, component) => + { + bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes; + + bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented + || statisticsTickBox.Selected != shouldTickBoxBeSelected; + + if (!shouldUpdateTickBoxState) { return; } + + updateGATickBoxToolTip(); + cachedConsent = GameAnalyticsManager.UserConsented; + GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected; + statisticsTickBox.OnSelected = null; + statisticsTickBox.Selected = shouldTickBoxBeSelected; + statisticsTickBox.OnSelected = prevHandler; + statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; + }); +#endif + } + + private void CreateModsTab(out WorkshopMenu workshopMenu) + { + GUIFrame content = CreateNewContentFrame(Tab.Mods); + content.RectTransform.RelativeSize = Vector2.One; + + workshopMenu = new WorkshopMenu(content); + } + + private void CreateBottomButtons() + { + GUIButton cancelButton = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: "Cancel") + { + OnClicked = (btn, obj) => + { + Close(); + return false; + } + }; + GUIButton applyButton = + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: "Apply") + { + OnClicked = (btn, obj) => + { + GameSettings.SetCurrentConfig(unsavedConfig); + WorkshopMenu.Apply(); + GameSettings.SaveCurrentConfig(); + mainFrame.Flash(color: GUIStyle.Green); + return false; + } + }; + } + + public void Close() + { + if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) + { + VoipCapture.Instance?.Dispose(); + } + mainFrame.Parent.RemoveChild(mainFrame); + if (Instance == this) { Instance = null; } + + GUI.SettingsMenuOpen = false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs index 785c510b4..96df43626 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OpenAL/Alc.cs @@ -109,6 +109,14 @@ namespace OpenAL public const int CaptureDefaultDeviceSpecifier = 0x311; public const int EnumCaptureSamples = 0x312; public const int EnumConnected = 0x313; + + + public const int OutputDevicesSpecifier = +#if OSX + DeviceSpecifier; +#else + AllDevicesSpecifier; +#endif #endregion @@ -214,7 +222,7 @@ namespace OpenAL return Encoding.UTF8.GetString(bytes); } - public static IList GetStringList(IntPtr device, int param) + public static IReadOnlyList GetStringList(IntPtr device, int param) { List retVal = new List(); IntPtr strPtr = _GetString(device, param); @@ -224,7 +232,8 @@ namespace OpenAL byte currChar = Marshal.ReadByte(strPtr, strEnd); if (currChar == '\0') { return retVal; } byte prevChar = 255; - while (true) { + while (true) + { strEnd++; prevChar = currChar; currChar = Marshal.ReadByte(strPtr, strEnd); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 281b031f2..41675864d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -234,7 +234,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - DebugConsole.ThrowError("Failed to set source's gain: " + debugName + ", " + Al.GetErrorString(alError), appendStackTrace: true); + DebugConsole.ThrowError($"Failed to set source's gain to {gain} (effective gain {effectiveGain}): {debugName}, {Al.GetErrorString(alError)}", appendStackTrace: true); return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index b967865ac..ac40b98bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -207,7 +207,7 @@ namespace Barotrauma.Sounds playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; playingChannels[(int)SourcePoolIndex.Voice] = new SoundChannel[16]; - string deviceName = GameMain.Config.AudioOutputDevice; + string deviceName = GameSettings.CurrentConfig.Audio.AudioOutputDevice; if (string.IsNullOrEmpty(deviceName)) { @@ -221,7 +221,10 @@ namespace Barotrauma.Sounds deviceName = audioDeviceNames[0]; } #endif - GameMain.Config.AudioOutputDevice = deviceName; + if (GameSettings.CurrentConfig.Audio.AudioOutputDevice != deviceName) + { + SetAudioOutputDevice(deviceName); + } InitializeAlcDevice(deviceName); @@ -232,6 +235,13 @@ namespace Barotrauma.Sounds CompressionDynamicRangeGain = 1.0f; } + private void SetAudioOutputDevice(string deviceName) + { + var config = GameSettings.CurrentConfig; + config.Audio.AudioOutputDevice = deviceName; + GameSettings.SetCurrentConfig(config); + } + public bool InitializeAlcDevice(string deviceName) { ReleaseResources(true); @@ -351,11 +361,11 @@ namespace Barotrauma.Sounds return newSound; } - public Sound LoadSound(XElement element, bool stream = false, string overrideFilePath = null) + public Sound LoadSound(ContentXElement element, bool stream = false, string overrideFilePath = null) { if (Disabled) { return null; } - string filePath = overrideFilePath ?? element.GetAttributeString("file", ""); + string filePath = overrideFilePath ?? element.GetAttributeContentPath("file")?.Value ?? ""; if (!File.Exists(filePath)) { throw new System.IO.FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); @@ -631,13 +641,13 @@ namespace Barotrauma.Sounds if (isConnected == 0) { DebugConsole.ThrowError("Playback device has been disconnected. You can select another available device in the settings."); - GameMain.Config.AudioOutputDevice = ""; + SetAudioOutputDevice(""); Disconnected = true; return; } } - if (GameMain.Client != null && GameMain.Config.VoipAttenuationEnabled) + if (GameMain.Client != null && GameSettings.CurrentConfig.Audio.VoipAttenuationEnabled) { if (Timing.TotalTime > lastAttenuationTime+0.2) { @@ -653,7 +663,7 @@ namespace Barotrauma.Sounds SetCategoryGainMultiplier("waterambience", VoipAttenuatedGain, 1); SetCategoryGainMultiplier("music", VoipAttenuatedGain, 1); - if (GameMain.Config.DynamicRangeCompressionEnabled) + if (GameSettings.CurrentConfig.Audio.DynamicRangeCompressionEnabled) { float targetGain = (Math.Min(1.0f, 1.0f / PlaybackAmplitude) - 1.0f) * 0.5f + 1.0f; if (targetGain < CompressionDynamicRangeGain) @@ -695,6 +705,15 @@ namespace Barotrauma.Sounds } } + public void ApplySettings() + { + SetCategoryGainMultiplier("default", GameSettings.CurrentConfig.Audio.SoundVolume, 0); + SetCategoryGainMultiplier("ui", GameSettings.CurrentConfig.Audio.SoundVolume, 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); + } + public void InitStreamThread() { if (Disabled) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 57920dea6..177cb11a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -9,78 +9,28 @@ using System.Xml.Linq; namespace Barotrauma { - public struct DamageSound - { - //the range of inflicted damage where the sound can be played - //(10.0f, 30.0f) would be played when the inflicted damage is between 10 and 30 - public readonly Vector2 damageRange; - - public readonly string damageType; - - public readonly Sound sound; - - public readonly string requiredTag; - - public bool ignoreMuffling; - - public DamageSound(Sound sound, Vector2 damageRange, string damageType, bool ignoreMuffling, string requiredTag = "") - { - this.sound = sound; - this.damageRange = damageRange; - this.damageType = damageType; - this.ignoreMuffling = ignoreMuffling; - this.requiredTag = requiredTag; - } - } - - public class BackgroundMusic - { - public readonly string File; - public readonly string Type; - public readonly bool DuckVolume; - public readonly float Volume; - - public readonly Vector2 IntensityRange; - - public readonly bool ContinueFromPreviousTime; - public int PreviousTime; - - public readonly XElement Element; - - public BackgroundMusic(XElement element) - { - this.File = Path.GetFullPath(element.GetAttributeString("file", "")).CleanUpPath(); - this.Type = element.GetAttributeString("type", "").ToLowerInvariant(); - this.IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); - this.DuckVolume = element.GetAttributeBool("duckvolume", false); - this.Volume = element.GetAttributeFloat("volume", 1.0f); - this.ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); - this.Element = element; - } - } - static class SoundPlayer { - private static ILookup miscSounds; - //music private const float MusicLerpSpeed = 1.0f; private const float UpdateMusicInterval = 5.0f; const int MaxMusicChannels = 6; - private readonly static Sound[] currentMusic = new Sound[MaxMusicChannels]; + private readonly static BackgroundMusic[] currentMusic = new BackgroundMusic[MaxMusicChannels]; private readonly static SoundChannel[] musicChannel = new SoundChannel[MaxMusicChannels]; private readonly static BackgroundMusic[] targetMusic = new BackgroundMusic[MaxMusicChannels]; - private static List musicClips; + private static IEnumerable musicClips => BackgroundMusic.BackgroundMusicPrefabs; private static BackgroundMusic previousDefaultMusic; private static float updateMusicTimer; //ambience - private static Sound waterAmbienceIn, waterAmbienceOut, waterAmbienceMoving; - private static readonly SoundChannel[] waterAmbienceChannels = new SoundChannel[3]; + private static Sound waterAmbienceIn => SoundPrefab.WaterAmbienceIn.ActivePrefab.Sound; + private static Sound waterAmbienceOut => SoundPrefab.WaterAmbienceOut.ActivePrefab.Sound; + private static Sound waterAmbienceMoving => SoundPrefab.WaterAmbienceMoving.ActivePrefab.Sound; + private static readonly HashSet waterAmbienceChannels = new HashSet(); private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max @@ -92,8 +42,8 @@ namespace Barotrauma //misc private static float[] targetFlowLeft, targetFlowRight; - public static List FlowSounds = new List(); - public static List SplashSounds = new List(); + public static IReadOnlyList FlowSounds => SoundPrefab.FlowSounds; + public static IReadOnlyList SplashSounds => SoundPrefab.SplashSounds; private static SoundChannel[] flowSoundChannels; private static float[] flowVolumeLeft; private static float[] flowVolumeRight; @@ -112,17 +62,13 @@ namespace Barotrauma private static string[] fireSoundTags = new string[fireSizes] { "fire", "firemedium", "firelarge" }; // TODO: could use a dictionary to split up the list into smaller lists of same type? - private static List damageSounds; - - private static Dictionary> guiSounds; + private static IEnumerable damageSounds => DamageSound.DamageSoundPrefabs; private static bool firstTimeInMainMenu = true; - private static Sound startUpSound; + private static Sound startUpSound => SoundPrefab.StartupSound.ActivePrefab.Sound; - public static bool Initialized; - - public static string OverrideMusicType + public static Identifier OverrideMusicType { get; set; @@ -130,291 +76,35 @@ namespace Barotrauma public static float? OverrideMusicDuration; - public static int SoundCount; - - private static List loadedSoundElements; - - private static bool SoundElementsEquivalent(XElement a, XElement b) - { - string filePathA = a.GetAttributeString("file", "").CleanUpPath(); - float baseGainA = a.GetAttributeFloat("volume", 1.0f); - float rangeA = a.GetAttributeFloat("range", 1000.0f); - string filePathB = b.GetAttributeString("file", "").CleanUpPath(); - float baseGainB = b.GetAttributeFloat("volume", 1.0f); - float rangeB = b.GetAttributeFloat("range", 1000.0f); - return a.Name.ToString().Equals(b.Name.ToString(), StringComparison.OrdinalIgnoreCase) && - filePathA == filePathB && MathUtils.NearlyEqual(baseGainA, baseGainB) && - MathUtils.NearlyEqual(rangeA, rangeB); - } - - public static IEnumerable Init() - { - OverrideMusicType = null; - - var soundFiles = GameMain.Instance.GetFilesOfType(ContentType.Sounds).ToList(); - - List soundElements = new List(); - foreach (ContentFile soundFile in soundFiles) - { - XDocument doc = XMLExtensions.TryLoadXml(soundFile.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - DebugConsole.NewMessage($"Overriding all sounds with {soundFile.Path}", Color.Yellow); - soundElements.Clear(); - } - soundElements.AddRange(mainElement.Elements()); - } - - SoundCount = 1 + soundElements.Count(); - - var startUpSoundElement = soundElements.Find(e => e.Name.ToString().Equals("startupsound", StringComparison.OrdinalIgnoreCase)); - if (startUpSoundElement != null) - { - startUpSound = GameMain.SoundManager.LoadSound(startUpSoundElement, false); - startUpSound?.Play(); - } - - yield return CoroutineStatus.Running; - - List> miscSoundList = new List>(); - damageSounds ??= new List(); - musicClips ??= new List(); - guiSounds ??= new Dictionary>(); - - bool firstWaterAmbienceLoaded = false; - - foreach (XElement soundElement in soundElements) - { - yield return CoroutineStatus.Running; - - if (loadedSoundElements != null && loadedSoundElements.Any(e => SoundElementsEquivalent(e, soundElement))) - { - continue; - } - - try - { - switch (soundElement.Name.ToString().ToLowerInvariant()) - { - case "music": - var newMusicClip = new BackgroundMusic(soundElement); - if (File.Exists(newMusicClip.File)) - { - musicClips.AddIfNotNull(newMusicClip); - if (loadedSoundElements != null) - { - if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) - { - targetMusic[0] = newMusicClip; - } - } - } - else - { - DebugConsole.NewMessage($"Music file \"{newMusicClip.File}\" not found."); - } - break; - case "splash": - SplashSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); - break; - case "flow": - FlowSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); - break; - case "waterambience": - //backwards compatibility (1st waterambience used to be played both inside and outside, 2nd when moving) - if (!firstWaterAmbienceLoaded) - { - waterAmbienceIn?.Dispose(); - waterAmbienceOut?.Dispose(); - if (File.Exists(soundElement.GetAttributeString("file", ""))) - { - waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); - waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); - } - else - { - waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceIn.ogg"); - waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceOut.ogg"); - } - firstWaterAmbienceLoaded = true; - } - else - { - waterAmbienceMoving?.Dispose(); - if (File.Exists(soundElement.GetAttributeString("file", ""))) - { - waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); - } - else - { - waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceMoving.ogg"); - } - } - break; - case "waterambiencein": - waterAmbienceIn?.Dispose(); - waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); - break; - case "waterambienceout": - waterAmbienceOut?.Dispose(); - waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); - break; - case "waterambiencemoving": - waterAmbienceMoving?.Dispose(); - waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); - break; - case "damagesound": - Sound damageSound = GameMain.SoundManager.LoadSound(soundElement, false); - if (damageSound == null) { continue; } - - string damageSoundType = soundElement.GetAttributeString("damagesoundtype", "None"); - damageSounds.Add(new DamageSound( - damageSound, - soundElement.GetAttributeVector2("damagerange", Vector2.Zero), - damageSoundType, - soundElement.GetAttributeBool("ignoremuffling", false), - soundElement.GetAttributeString("requiredtag", ""))); - - break; - case "guisound": - Sound guiSound = GameMain.SoundManager.LoadSound(soundElement, stream: false); - if (guiSound == null) { continue; } - if (Enum.TryParse(soundElement.GetAttributeString("guisoundtype", null), true, out GUISoundType soundType)) - { - if (guiSounds.ContainsKey(soundType)) - { - guiSounds[soundType].Add(guiSound); - } - else - { - guiSounds.Add(soundType, new List() { guiSound }); - } - } - break; - default: - Sound sound = GameMain.SoundManager.LoadSound(soundElement, false); - if (sound != null) - { - miscSoundList.Add(new KeyValuePair(soundElement.Name.ToString().ToLowerInvariant(), sound)); - } - break; - } - } - catch (System.IO.FileNotFoundException e) - { - DebugConsole.ThrowError("Error while initializing SoundPlayer.", e); - } - } - - musicClips.RemoveAll(mc => !soundElements.Any(e => SoundElementsEquivalent(mc.Element, e))); - - for (int i = 0; i < currentMusic.Length; i++) - { - if (currentMusic[i] != null && !musicClips.Any(mc => mc.File == currentMusic[i].Filename)) - { - DisposeMusicChannel(i); - } - } - - SplashSounds.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - SplashSounds.RemoveAll(s => s.Disposed); - - FlowSounds.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - FlowSounds.RemoveAll(s => s.Disposed); - - damageSounds.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.sound.XElement, e))) { s.sound.Dispose(); } - }); - damageSounds.RemoveAll(s => s.sound.Disposed); - - guiSounds.ForEach(kvp => - { - kvp.Value?.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - }); - guiSounds.ForEach(kvp => kvp.Value?.RemoveAll(s => s.Disposed)); - - miscSounds?.ForEach(g => g.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - else { miscSoundList.Add(new KeyValuePair(g.Key, s)); } - })); - - flowSoundChannels?.ForEach(ch => ch?.Dispose()); - flowSoundChannels = new SoundChannel[FlowSounds.Count]; - flowVolumeLeft = new float[FlowSounds.Count]; - flowVolumeRight = new float[FlowSounds.Count]; - targetFlowLeft = new float[FlowSounds.Count]; - targetFlowRight = new float[FlowSounds.Count]; - - fireSoundChannels?.ForEach(ch => ch?.Dispose()); - fireSoundChannels = new SoundChannel[fireSizes]; - fireVolumeLeft = new float[fireSizes]; - fireVolumeRight = new float[fireSizes]; - - miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); - - Initialized = true; - - loadedSoundElements = soundElements; - - yield return CoroutineStatus.Success; - - } - public static void Update(float deltaTime) { - if (!Initialized) { return; } - UpdateMusic(deltaTime); - - if (startUpSound != null && !GameMain.SoundManager.IsPlaying(startUpSound)) + if (flowSoundChannels == null || flowSoundChannels.Length != FlowSounds.Count) { - startUpSound.Dispose(); - startUpSound = null; + flowSoundChannels = new SoundChannel[FlowSounds.Count]; + flowVolumeLeft = new float[FlowSounds.Count]; + flowVolumeRight = new float[FlowSounds.Count]; + targetFlowLeft = new float[FlowSounds.Count]; + targetFlowRight = new float[FlowSounds.Count]; } - + if (fireSoundChannels == null || fireSoundChannels.Length != fireSizes) + { + fireSoundChannels = new SoundChannel[fireSizes]; + fireVolumeLeft = new float[fireSizes]; + fireVolumeRight = new float[fireSizes]; + } + //stop water sounds if no sub is loaded if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) { - for (int i = 0; i < waterAmbienceChannels.Length; i++) + foreach (var chn in waterAmbienceChannels.Concat(flowSoundChannels).Concat(fireSoundChannels)) { - if (waterAmbienceChannels[i] == null) { continue; } - waterAmbienceChannels[i].FadeOutAndDispose(); - waterAmbienceChannels[i] = null; - } - for (int i = 0; i < FlowSounds.Count; i++) - { - if (flowSoundChannels[i] == null) { continue; } - flowSoundChannels[i].FadeOutAndDispose(); - flowSoundChannels[i] = null; - } - for (int i = 0; i < fireSoundChannels.Length; i++) - { - if (fireSoundChannels[i] == null) { continue; } - fireSoundChannels[i].FadeOutAndDispose(); - fireSoundChannels[i] = null; + chn?.FadeOutAndDispose(); } fireVolumeLeft[0] = 0.0f; fireVolumeLeft[1] = 0.0f; fireVolumeRight[0] = 0.0f; fireVolumeRight[1] = 0.0f; - if (hullSoundChannel != null) - { - hullSoundChannel.FadeOutAndDispose(); - hullSoundChannel = null; - hullSoundSource = null; - } + hullSoundChannel?.FadeOutAndDispose(); + hullSoundSource = null; return; } @@ -482,44 +172,29 @@ namespace Barotrauma } } - for (int i = 0; i < 3; i++) + void updateWaterAmbience(Sound sound, float volume) { - float volume = 0.0f; - Sound sound = null; - switch (i) + SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); + if (chn is null || !chn.IsPlaying) { - case 0: - volume = ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor; - sound = waterAmbienceIn; - break; - case 1: - volume = ambienceVolume * movementSoundVolume * insideSubFactor; - sound = waterAmbienceMoving; - break; - case 2: - volume = 1.0f - insideSubFactor; - sound = waterAmbienceOut; - break; + if (!(chn is null)) { waterAmbienceChannels.Remove(chn); } + chn = sound.Play(volume, "waterambience"); + chn.Looping = true; + waterAmbienceChannels.Add(chn); } - - if (sound == null) { continue; } - - // Consider the volume set in sounds.xml - volume *= sound.BaseGain; - if ((waterAmbienceChannels[i] == null || !waterAmbienceChannels[i].IsPlaying) && volume > 0.01f) + else { - waterAmbienceChannels[i] = sound.Play(volume, "waterambience"); - waterAmbienceChannels[i].Looping = true; - } - else if (waterAmbienceChannels[i] != null) - { - waterAmbienceChannels[i].Gain += deltaTime * Math.Sign(volume - waterAmbienceChannels[i].Gain); - if (waterAmbienceChannels[i].Gain < 0.01f) + chn.Gain += deltaTime * Math.Sign(volume - chn.Gain); + if (chn.Gain < 0.01f) { - waterAmbienceChannels[i].FadeOutAndDispose(); + chn.FadeOutAndDispose(); } } } + + updateWaterAmbience(waterAmbienceIn, ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor); + updateWaterAmbience(waterAmbienceMoving, ambienceVolume * movementSoundVolume * insideSubFactor); + updateWaterAmbience(waterAmbienceOut, 1.0f - insideSubFactor); } private static void UpdateWaterFlowSounds(float deltaTime) @@ -606,7 +281,7 @@ namespace Barotrauma Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (flowVolumeRight[i] - flowVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (flowSoundChannels[i] == null || !flowSoundChannels[i].IsPlaying) { - flowSoundChannels[i] = FlowSounds[i].Play(1.0f, FlowSoundRange, soundPos); + 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); @@ -624,7 +299,7 @@ namespace Barotrauma } Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { foreach (FireSource fs in hull.FireSources) { @@ -760,10 +435,10 @@ namespace Barotrauma public static Sound GetSound(string soundTag) { - var matchingSounds = miscSounds[soundTag].ToList(); - if (matchingSounds.Count == 0) return null; + var matchingSounds = SoundPrefab.Prefabs.Where(p => p.ElementName == soundTag); + if (!matchingSounds.Any()) return null; - return matchingSounds[Rand.Int(matchingSounds.Count)]; + return matchingSounds.GetRandomUnsynced().Sound; } /// @@ -806,14 +481,14 @@ namespace Barotrauma private static void UpdateMusic(float deltaTime) { - if (musicClips == null || GameMain.SoundManager.Disabled) { return; } + if (musicClips == null || (GameMain.SoundManager?.Disabled ?? true)) { return; } if (OverrideMusicType != null && OverrideMusicDuration.HasValue) { OverrideMusicDuration -= deltaTime; if (OverrideMusicDuration <= 0.0f) { - OverrideMusicType = null; + OverrideMusicType = Identifier.Empty; OverrideMusicDuration = null; } } @@ -824,7 +499,7 @@ namespace Barotrauma if (updateMusicTimer <= 0.0f) { //find appropriate music for the current situation - string currentMusicType = GetCurrentMusicType(); + Identifier currentMusicType = GetCurrentMusicType(); float currentIntensity = GameMain.GameSession?.EventManager != null ? GameMain.GameSession.EventManager.MusicIntensity * 100.0f : 0.0f; @@ -835,13 +510,13 @@ namespace Barotrauma targetMusic[mainTrackIndex] = null; } //switch the music if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m.File == currentMusic[mainTrackIndex].Filename)) + else if (targetMusic[mainTrackIndex] == null || currentMusic[mainTrackIndex] == null || !currentMusic[mainTrackIndex].IsPlaying() || !suitableMusic.Any(m => m == currentMusic[mainTrackIndex])) { if (currentMusicType == "default") { if (previousDefaultMusic == null) { - targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandom(); + targetMusic[mainTrackIndex] = previousDefaultMusic = suitableMusic.GetRandomUnsynced(); } else { @@ -850,7 +525,7 @@ namespace Barotrauma } else { - targetMusic[mainTrackIndex] = suitableMusic.GetRandom(); + targetMusic[mainTrackIndex] = suitableMusic.GetRandomUnsynced(); } } @@ -858,16 +533,16 @@ namespace Barotrauma { // Find background noise loop for the current biome IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips(Level.Loaded.LevelData?.Biome?.Identifier, currentIntensity) : + GetSuitableMusicClips(Level.Loaded.LevelData.Biome.Identifier, currentIntensity) : Enumerable.Empty(); if (suitableNoiseLoops.Count() == 0) { targetMusic[noiseLoopIndex] = null; } // Switch the noise loop if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m.File == currentMusic[noiseLoopIndex].Filename)) + else if (targetMusic[noiseLoopIndex] == null || currentMusic[noiseLoopIndex] == null || !suitableNoiseLoops.Any(m => m == currentMusic[noiseLoopIndex])) { - targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandom(); + targetMusic[noiseLoopIndex] = suitableNoiseLoops.GetRandomUnsynced(); } } else @@ -875,35 +550,36 @@ namespace Barotrauma targetMusic[noiseLoopIndex] = null; } - IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience", currentIntensity); + IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience".ToIdentifier(), currentIntensity); int typeAmbienceTrackIndex = 2; if (suitableTypeAmbiences.None()) { targetMusic[typeAmbienceTrackIndex] = null; } // Switch the type ambience if nothing playing atm or the currently playing clip is not suitable anymore - else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m.File == currentMusic[typeAmbienceTrackIndex].Filename)) + else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m == currentMusic[typeAmbienceTrackIndex])) { - targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandom(); + targetMusic[typeAmbienceTrackIndex] = suitableTypeAmbiences.GetRandomUnsynced(); } //get the appropriate intensity layers for current situation IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? - GetSuitableMusicClips("intensity", currentIntensity) : + GetSuitableMusicClips("intensity".ToIdentifier(), currentIntensity) : Enumerable.Empty(); int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { //disable targetmusics that aren't suitable anymore - if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m.File == targetMusic[i].File)) + if (targetMusic[i] != null && !suitableIntensityMusic.Any(m => m == targetMusic[i])) { targetMusic[i] = null; } } + foreach (BackgroundMusic intensityMusic in suitableIntensityMusic) { //already playing, do nothing - if (targetMusic.Any(m => m != null && m.File == intensityMusic.File)) { continue; } + if (targetMusic.Any(m => m != null && m == intensityMusic)) { continue; } for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { @@ -932,12 +608,12 @@ namespace Barotrauma } } //something should be playing, but the targetMusic is invalid - else if (!musicClips.Any(mc => mc.File == targetMusic[i].File)) + else if (!musicClips.Any(mc => mc == targetMusic[i])) { - targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandom(); + targetMusic[i] = GetSuitableMusicClips(targetMusic[i].Type, 0.0f).GetRandomUnsynced(); } //something should be playing, but the channel is playing nothing or an incorrect clip - else if (currentMusic[i] == null || targetMusic[i].File != currentMusic[i].Filename) + else if (currentMusic[i] == null || targetMusic[i] != currentMusic[i]) { //something playing -> mute it first if (musicChannel[i] != null && musicChannel[i].IsPlaying) @@ -949,18 +625,9 @@ namespace Barotrauma if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) { DisposeMusicChannel(i); - try - { - currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true); - } - catch (System.IO.InvalidDataException e) - { - DebugConsole.ThrowError($"Failed to load the music clip \"{targetMusic[i].File}\".", e); - musicClips.Remove(targetMusic[i]); - targetMusic[i] = null; - break; - } - musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "default" : "music"); + + currentMusic[i] = targetMusic[i]; + musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? "default" : "music"); if (targetMusic[i].ContinueFromPreviousTime) { musicChannel[i].StreamSeekPos = targetMusic[i].PreviousTime; @@ -974,7 +641,7 @@ namespace Barotrauma if (musicChannel[i] == null || !musicChannel[i].IsPlaying) { musicChannel[i]?.Dispose(); - musicChannel[i] = currentMusic[i].Play(0.0f, i == noiseLoopIndex ? "default" : "music"); + musicChannel[i] = currentMusic[i].Sound.Play(0.0f, i == noiseLoopIndex ? "default" : "music"); musicChannel[i].Looping = true; } float targetGain = targetMusic[i].Volume; @@ -989,17 +656,17 @@ namespace Barotrauma private static void DisposeMusicChannel(int index) { - var clip = musicClips.Find(m => m.File == musicChannel[index]?.Sound?.Filename); + var clip = musicClips.FirstOrDefault(m => m.Sound == musicChannel[index]?.Sound); if (clip != null) { if (clip.ContinueFromPreviousTime) { clip.PreviousTime = musicChannel[index].StreamSeekPos; } } musicChannel[index]?.Dispose(); musicChannel[index] = null; - currentMusic[index]?.Dispose(); currentMusic[index] = null; + currentMusic[index] = null; } - private static IEnumerable GetSuitableMusicClips(string musicType, float currentIntensity) + private static IEnumerable GetSuitableMusicClips(Identifier musicType, float currentIntensity) { return musicClips.Where(music => music != null && @@ -1008,28 +675,22 @@ namespace Barotrauma currentIntensity <= music.IntensityRange.Y); } - private static string GetCurrentMusicType() + private static Identifier GetCurrentMusicType() { if (OverrideMusicType != null) { return OverrideMusicType; } - if (Screen.Selected == null) { return "menu"; } + if (Screen.Selected == null) { return "menu".ToIdentifier(); } - if (Screen.Selected == GameMain.CharacterEditorScreen || - Screen.Selected == GameMain.LevelEditorScreen || - Screen.Selected == GameMain.ParticleEditorScreen || - Screen.Selected == GameMain.SpriteEditorScreen || - Screen.Selected == GameMain.SubEditorScreen || - Screen.Selected == GameMain.EventEditorScreen || - (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode) || - Screen.Selected == GameMain.NetLobbyScreen) + if ((Screen.Selected?.IsEditor ?? false) + || (Screen.Selected == GameMain.NetLobbyScreen)) { - return "editor"; + return "editor".ToIdentifier(); } if (Screen.Selected != GameMain.GameScreen) { previousDefaultMusic = null; - return firstTimeInMainMenu ? "menu" : "default"; + return (firstTimeInMainMenu ? "menu" : "default").ToIdentifier(); } firstTimeInMainMenu = false; @@ -1040,21 +701,21 @@ namespace Barotrauma if (Level.Loaded != null && Level.Loaded.Ruins != null && Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) { - return "ruins"; + return "ruins".ToIdentifier(); } if (Character.Controlled.Submarine?.Info?.IsWreck ?? false) { - return "wreck"; + return "wreck".ToIdentifier(); } if (Level.IsLoadedOutpost) { // Only return music type for location types which have music tracks defined - var locationType = Level.Loaded.StartLocation?.Type?.Identifier?.ToLowerInvariant(); - if (!string.IsNullOrEmpty(locationType) && musicClips.Any(c => c.Type == locationType)) + var locationType = Level.Loaded.StartLocation?.Type?.Identifier; + if (locationType.HasValue && locationType != Identifier.Empty && musicClips.Any(c => c.Type == locationType)) { - return locationType; + return locationType.Value; } } } @@ -1062,26 +723,26 @@ namespace Barotrauma Submarine targetSubmarine = Character.Controlled?.Submarine; if (targetSubmarine != null && targetSubmarine.AtDamageDepth) { - return "deep"; + return "deep".ToIdentifier(); } if (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Submarine.MainSub != null && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Submarine.MainSub.RealWorldCrushDepth) { - return "deep"; + return "deep".ToIdentifier(); } if (targetSubmarine != null) { float floodedArea = 0.0f; float totalArea = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != targetSubmarine) { continue; } floodedArea += hull.WaterVolume; totalArea += hull.Volume; } - if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded"; } + if (totalArea > 0.0f && floodedArea / totalArea > 0.25f) { return "flooded".ToIdentifier(); } } float enemyDistThreshold = 5000.0f; @@ -1100,14 +761,14 @@ namespace Barotrauma { if (Vector2.DistanceSquared(character.WorldPosition, targetSubmarine.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { - return "monster"; + return "monster".ToIdentifier(); } } else if (Character.Controlled != null) { if (Vector2.DistanceSquared(character.WorldPosition, Character.Controlled.WorldPosition) < enemyDistThreshold * enemyDistThreshold) { - return "monster"; + return "monster".ToIdentifier(); } } } @@ -1116,16 +777,16 @@ namespace Barotrauma { if (Submarine.Loaded != null && Level.Loaded != null && Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - return "levelend"; + return "levelend".ToIdentifier(); } if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 && Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { - return "start"; + return "start".ToIdentifier(); } } - return "default"; + return "default".ToIdentifier(); } public static bool ShouldMuffleSound(Character listener, Vector2 soundWorldPos, float range, Hull hullGuess) @@ -1159,7 +820,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].Play(1.0f, range, worldPosition, muffle: ShouldMuffleSound(Character.Controlled, worldPosition, range, null)); + var channel = 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) @@ -1169,35 +830,29 @@ namespace Barotrauma } private static readonly List tempList = new List(); - public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) + public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { damage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); tempList.Clear(); foreach (var s in damageSounds) { - if ((s.damageRange == Vector2.Zero || - (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && - string.Equals(s.damageType, damageType, StringComparison.OrdinalIgnoreCase) && - (string.IsNullOrEmpty(s.requiredTag) || (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag)))) + if ((s.DamageRange == Vector2.Zero || + (damage >= s.DamageRange.X && damage <= s.DamageRange.Y)) && + s.DamageType == damageType && + (s.RequiredTag.IsEmpty || (tags == null ? s.RequiredTag.IsEmpty : tags.Contains(s.RequiredTag)))) { tempList.Add(s); } } - var damageSound = tempList.GetRandom(); - if (damageSound.sound != null) - { - damageSound.sound.Play(1.0f, range, position, muffle: !damageSound.ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); - } + var damageSound = tempList.GetRandomUnsynced(); + damageSound?.Sound?.Play(1.0f, range, position, muffle: !damageSound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, range, null)); } public static void PlayUISound(GUISoundType soundType) { - if (guiSounds == null || guiSounds.Count < 1) { return; } - if (guiSounds.TryGetValue(soundType, out List sounds)) - { - if (sounds == null || sounds.Count < 1) { return; } - sounds.GetRandom()?.Play(null, "ui"); - } + GUISound.GUISoundPrefabs + .Where(s => s.Type == soundType) + .GetRandomUnsynced()?.Sound?.Play(null, "ui"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs new file mode 100644 index 000000000..8d510a115 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -0,0 +1,267 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class TagNames : Attribute + { + public readonly ImmutableHashSet Names; + + public TagNames(params string[] names) + { + Names = names.Select(n => n.ToIdentifier()).ToImmutableHashSet(); + } + } + + class SoundPrefab : Prefab + { + private class PrefabCollectionHandler + { + public readonly object Collection; + public readonly MethodInfo AddMethod; + public readonly MethodInfo RemoveMethod; + public readonly MethodInfo SortAllMethod; + public readonly MethodInfo AddOverrideFileMethod; + public readonly MethodInfo RemoveOverrideFileMethod; + + public void Add(SoundPrefab p, bool isOverride) + { + AddMethod.Invoke(Collection, new object[] { p, isOverride }); + } + + public void Remove(SoundPrefab p) + { + RemoveMethod.Invoke(Collection, new object[] { p }); + } + + public void AddOverrideFile(ContentFile file) + { + AddOverrideFileMethod.Invoke(Collection, new object[] { file }); + } + + public void RemoveOverrideFile(ContentFile file) + { + RemoveOverrideFileMethod.Invoke(Collection, new object[] { file }); + } + + public void SortAll() + { + SortAllMethod.Invoke(Collection, null); + } + + public PrefabCollectionHandler(Type type) + { + var collectionField = type.GetField($"{type.Name}Prefabs", BindingFlags.Public | BindingFlags.Static); + if (collectionField is null) { throw new InvalidOperationException($"Couldn't determine PrefabCollection for {type.Name}"); } + Collection = collectionField.GetValue(null) ?? throw new InvalidOperationException($"PrefabCollection for {type.Name} was null"); + AddMethod = Collection.GetType().GetMethod("Add", BindingFlags.Public | BindingFlags.Instance); + RemoveMethod = Collection.GetType().GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance); + AddOverrideFileMethod = Collection.GetType().GetMethod("AddOverrideFile", BindingFlags.Public | BindingFlags.Instance); + RemoveOverrideFileMethod = Collection.GetType().GetMethod("RemoveOverrideFile", BindingFlags.Public | BindingFlags.Instance); + SortAllMethod = Collection.GetType().GetMethod("SortAll", BindingFlags.Public | BindingFlags.Instance); + } + } + + public readonly static PrefabSelector WaterAmbienceIn = new PrefabSelector(); + public readonly static PrefabSelector WaterAmbienceOut = new PrefabSelector(); + public readonly static PrefabSelector WaterAmbienceMoving = new PrefabSelector(); + public readonly static PrefabSelector StartupSound = new PrefabSelector(); + + private readonly static List flowSounds = new List(); + public static IReadOnlyList FlowSounds => flowSounds; + private readonly static List splashSounds = new List(); + public static IReadOnlyList SplashSounds => splashSounds; + + public readonly static ImmutableDictionary TagToDerivedPrefab; + private readonly static ImmutableDictionary derivedPrefabCollections; + private readonly static ImmutableDictionary> prefabSelectors; + private readonly static ImmutableDictionary> prefabsWithTag; + public readonly static PrefabCollection Prefabs; + + static SoundPrefab() + { + var types = ReflectionUtils.GetDerivedNonAbstract(); + //types.ForEach(t => t.GetProperties(BindingFlags.Public | BindingFlags.Static)); + TagToDerivedPrefab = types.SelectMany(t => + t.GetCustomAttribute().Names.Select(n => (n, t))).ToImmutableDictionary(); + derivedPrefabCollections = types.Select(t => (t, new PrefabCollectionHandler(t))).ToImmutableDictionary(); + + var prefabSelectorFields = typeof(SoundPrefab).GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.FieldType == typeof(PrefabSelector)); + prefabSelectors = prefabSelectorFields.Select(f => (f.Name.ToIdentifier(), (PrefabSelector)f.GetValue(null))).ToImmutableDictionary(); + + var prefabsOfTagName = typeof(SoundPrefab).GetFields(BindingFlags.Static | BindingFlags.NonPublic) + .Where(f => f.FieldType == typeof(List)); + prefabsWithTag = prefabsOfTagName.Select(f => (f.Name.Substring(0, f.Name.Length-6).ToIdentifier(), (List)f.GetValue(null))).ToImmutableDictionary(); + + Prefabs = new PrefabCollection( + onAdd: (SoundPrefab p, bool isOverride) => + { + if (derivedPrefabCollections.ContainsKey(p.GetType())) + { + derivedPrefabCollections[p.GetType()].Add(p, isOverride); + } + if (prefabSelectors.ContainsKey(p.ElementName)) { prefabSelectors[p.ElementName].Add(p, isOverride); } + UpdateSoundsWithTag(); + }, + onRemove: (SoundPrefab p) => + { + if (derivedPrefabCollections.ContainsKey(p.GetType())) + { + derivedPrefabCollections[p.GetType()].Remove(p); + } + if (prefabSelectors.ContainsKey(p.ElementName)) { prefabSelectors[p.ElementName].RemoveIfContains(p); } + UpdateSoundsWithTag(); + }, + onSort: () => + { + derivedPrefabCollections.Values.ForEach(h => h.SortAll()); + prefabSelectors.Values.ForEach(h => h.Sort()); + }, + onAddOverrideFile: (file) => {derivedPrefabCollections.Values.ForEach(h => h.AddOverrideFile(file)); }, + onRemoveOverrideFile: (file) => { derivedPrefabCollections.Values.ForEach(h => h.RemoveOverrideFile(file)); } + ); + } + + private static void UpdateSoundsWithTag() + { + foreach (var tag in prefabsWithTag.Keys) + { + var list = prefabsWithTag[tag]; + list.Clear(); + list.AddRange(Prefabs.Where(p => p.ElementName == tag)); + list.Sort((p1, p2) => + { + if (p1.ContentFile.ContentPackage.Index < p2.ContentFile.ContentPackage.Index) { return -1; } + if (p1.ContentFile.ContentPackage.Index > p2.ContentFile.ContentPackage.Index) { return 1; } + if (p2.Element.ComesAfter(p1.Element)) { return -1; } + if (p1.Element.ComesAfter(p2.Element)) { return 1; } + return 0; + }); + } + } + + protected override Identifier DetermineIdentifier(XElement element) + { + Identifier id = base.DetermineIdentifier(element); + if (id.IsEmpty) + { + if (id.IsEmpty) { id = Path.GetFileNameWithoutExtension(element.GetAttributeStringUnrestricted("path", "")).ToIdentifier(); } + if (id.IsEmpty) { id = Path.GetFileNameWithoutExtension(element.GetAttributeStringUnrestricted("file", "")).ToIdentifier(); } + + if (!id.IsEmpty) + { + id = $"{element.Name}_{id}".ToIdentifier(); + + string damageSoundType = element.GetAttributeString("damagesoundtype", ""); + if (!damageSoundType.IsNullOrEmpty()) + { + id = $"{id}_{damageSoundType}".ToIdentifier(); + } + + string musicType = element.GetAttributeString("type", ""); + if (!musicType.IsNullOrEmpty()) + { + id = $"{id}_{musicType}".ToIdentifier(); + } + } + } + + return id; + } + + public readonly ContentPath SoundPath; + public readonly ContentXElement Element; + public readonly Identifier ElementName; + public Sound Sound { get; private set; } + + public SoundPrefab(ContentXElement element, SoundsFile file, bool stream = false) : base(file, element) + { + SoundPath = element.GetAttributeContentPath("file") ?? ContentPath.Empty; + Element = element; + ElementName = element.NameAsIdentifier(); + Sound = GameMain.SoundManager.LoadSound(element, stream: stream); + } + + public bool IsPlaying() + { + return Sound.IsPlaying(); + } + + public override void Dispose() + { + Sound?.Dispose(); Sound = null; + } + } + + [TagNames("damagesound")] + class DamageSound : SoundPrefab + { + public readonly static PrefabCollection DamageSoundPrefabs = new PrefabCollection(); + + //the range of inflicted damage where the sound can be played + //(10.0f, 30.0f) would be played when the inflicted damage is between 10 and 30 + public readonly Vector2 DamageRange; + + public readonly Identifier DamageType; + + public readonly Identifier RequiredTag; + + public bool IgnoreMuffling; + + public DamageSound(ContentXElement element, SoundsFile file) : base(element, file) + { + DamageRange = element.GetAttributeVector2("damagerange", Vector2.Zero); + DamageType = element.GetAttributeIdentifier("damagesoundtype", "None"); + IgnoreMuffling = element.GetAttributeBool("ignoremuffling", false); + RequiredTag = element.GetAttributeIdentifier("requiredtag", ""); + } + } + + [TagNames("music")] + class BackgroundMusic : SoundPrefab + { + public readonly static PrefabCollection BackgroundMusicPrefabs = new PrefabCollection(); + + public readonly Identifier Type; + public readonly bool DuckVolume; + public readonly float Volume; + + public readonly Vector2 IntensityRange; + + public readonly bool ContinueFromPreviousTime; + public int PreviousTime; + + public BackgroundMusic(ContentXElement element, SoundsFile file) : base(element, file, stream: true) + { + Type = element.GetAttributeIdentifier("type", ""); + IntensityRange = element.GetAttributeVector2("intensityrange", new Vector2(0.0f, 100.0f)); + DuckVolume = element.GetAttributeBool("duckvolume", false); + this.Volume = element.GetAttributeFloat("volume", 1.0f); + ContinueFromPreviousTime = element.GetAttributeBool("continuefromprevioustime", false); + } + } + + [TagNames("guisound")] + class GUISound : SoundPrefab + { + //public readonly static Dictionary> GUISoundsByType = new Dictionary>(); + public readonly static PrefabCollection GUISoundPrefabs = new PrefabCollection(); + + public readonly GUISoundType Type; + + public GUISound(ContentXElement element, SoundsFile file) : base(element, file) + { + Type = element.GetAttributeEnum("guisoundtype", GUISoundType.UIMessage); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index e7f1c6aad..92394a518 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Sounds { if (soundChannel == null) { return; } gain = value; - soundChannel.Gain = value * GameMain.Config.VoiceChatVolume; + soundChannel.Gain = value * GameSettings.CurrentConfig.Audio.VoiceChatVolume; } } @@ -105,9 +105,9 @@ namespace Barotrauma.Sounds { float fVal = ShortToFloat(buffer[i]); - if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account? + if (gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume > 1.0f) //TODO: take distance into account? { - fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1f, 1f); + fVal = Math.Clamp(fVal * gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume, -1f, 1f); } if (UseMuffleFilter) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index ae7ed3b35..765fa93ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Xml.Linq; namespace Barotrauma @@ -18,7 +19,7 @@ namespace Barotrauma } public string Name => $"Decorative Sprite"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } public Sprite Sprite { get; private set; } @@ -29,22 +30,22 @@ namespace Barotrauma Noise } - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 Offset { get; private set; } - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 RandomOffset { get; private set; } - [Serialize(AnimationType.None, false), Editable] + [Serialize(AnimationType.None, IsPropertySaveable.No), Editable] public AnimationType OffsetAnim { get; private set; } - [Serialize(0.0f, true), Editable] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float OffsetAnimSpeed { get; private set; } private float rotationSpeedRadians; private float absRotationSpeedRadians; - [Serialize(0.0f, true), Editable] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float RotationSpeed { get @@ -59,7 +60,7 @@ namespace Barotrauma } private float rotationRadians; - [Serialize(0.0f, true), Editable] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable] public float Rotation { get @@ -73,7 +74,7 @@ namespace Barotrauma } private Vector2 randomRotationRadians; - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 RandomRotation { get @@ -87,30 +88,30 @@ namespace Barotrauma } private float scale; - [Serialize(1.0f, true), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable] public float Scale { get { return scale; } private set { scale = MathHelper.Clamp(value, 0.0f, 10.0f); } } - [Serialize("0,0", true), Editable] + [Serialize("0,0", IsPropertySaveable.Yes), Editable] public Vector2 RandomScale { get; private set; } - [Serialize(AnimationType.None, false), Editable] + [Serialize(AnimationType.None, IsPropertySaveable.No), Editable] public AnimationType RotationAnim { get; private set; } /// /// If > 0, only one sprite of the same group is used (chosen randomly) /// - [Serialize(0, false, description: "If > 0, only one sprite of the same group is used (chosen randomly)"), Editable(ReadOnly = true)] + [Serialize(0, IsPropertySaveable.No, description: "If > 0, only one sprite of the same group is used (chosen randomly)"), Editable(ReadOnly = true)] public int RandomGroupID { get; private set; } - [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } /// @@ -122,12 +123,12 @@ namespace Barotrauma /// internal List AnimationConditionals { get; private set; } = new List(); - public DecorativeSprite(XElement element, string path = "", string file = "", bool lazyLoad = false) + public DecorativeSprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) { Sprite = new Sprite(element, path, file, lazyLoad: lazyLoad); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); // load property conditionals - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { //choose which list the new conditional should be placed to List conditionalList = null; @@ -217,18 +218,18 @@ namespace Barotrauma return MathHelper.Lerp(RandomScale.X, RandomScale.Y, randomScaleModifier); } - public static void UpdateSpriteStates(Dictionary> spriteGroups, Dictionary animStates, + public static void UpdateSpriteStates(ImmutableDictionary> spriteGroups, Dictionary animStates, int entityID, float deltaTime, Func checkConditional) { foreach (int spriteGroup in spriteGroups.Keys) { - for (int i = 0; i < spriteGroups[spriteGroup].Count; i++) + for (int i = 0; i < spriteGroups[spriteGroup].Length; i++) { var decorativeSprite = spriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } if (spriteGroup > 0) { - int activeSpriteIndex = entityID % spriteGroups[spriteGroup].Count; + int activeSpriteIndex = entityID % spriteGroups[spriteGroup].Length; if (i != activeSpriteIndex) { animStates[decorativeSprite].IsActive = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs index 91175d95a..2f9b72f5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/CustomDeformation.cs @@ -9,11 +9,11 @@ namespace Barotrauma.SpriteDeformations class CustomDeformationParams : SpriteDeformationParams { [Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f), - Serialize(0.0f, true, description: "How fast the deformation \"oscillates\" back and forth. " + + Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the deformation \"oscillates\" back and forth. " + "For example, if the sprite is stretched up, setting this value above zero would make it do a wave-like movement up and down.")] public override float Frequency { get; set; } = 1; - [Serialize(1.0f, true, description: "The \"strength\" of the deformation."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The \"strength\" of the deformation."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float Amplitude { get; set; } public CustomDeformationParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs index 6b7d599dd..5f4ef5f80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/Inflate.cs @@ -6,9 +6,9 @@ namespace Barotrauma.SpriteDeformations { class InflateParams : SpriteDeformationParams { - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1)] public override float Frequency { get; set; } = 1; - [Serialize(1.0f, true), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.1f)] public float Scale { get; set; } public InflateParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs index 3830daaa1..775aac2ec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/NoiseDeformation.cs @@ -5,13 +5,13 @@ namespace Barotrauma.SpriteDeformations { class NoiseDeformationParams : SpriteDeformationParams { - [Serialize(0.0f, true, description: "The frequency of the noise."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The frequency of the noise."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ValueStep = 1f)] public override float Frequency { get; set; } - [Serialize(1.0f, true, description: "How much the noise distorts the sprite."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the noise distorts the sprite."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] public float Amplitude { get; set; } - [Serialize(0.0f, true, description: "How fast the noise changes."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the noise changes."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ValueStep = 0.01f)] public float ChangeSpeed { get; set; } public NoiseDeformationParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs index 6901234d1..26ad4c949 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs @@ -10,26 +10,26 @@ namespace Barotrauma.SpriteDeformations /// 0 = no falloff, the entire sprite is stretched /// 1 = stretching the center of the sprite has no effect at the edges /// - [Serialize(0.0f, true, description: "0 = no falloff, the entire sprite is stretched, 1 = stretching the center of the sprite has no effect at the edges."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "0 = no falloff, the entire sprite is stretched, 1 = stretching the center of the sprite has no effect at the edges."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Falloff { get; set; } /// /// Maximum stretch per vertex (1 = the size of the sprite) /// - [Serialize(1.0f, true, description: "Maximum stretch per vertex (1 = the size of the sprite)"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Maximum stretch per vertex (1 = the size of the sprite)"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float MaxDeformation { get; set; } /// /// How fast the sprite reacts to being stretched /// - [Serialize(10.0f, true, description: "How fast the sprite reacts to being stretched"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the sprite reacts to being stretched"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ReactionSpeed { get; set; } /// /// How fast the sprite returns back to normal after stretching ends /// - [Serialize(0.05f, true, description: "How fast the sprite returns back to normal after stretching ends"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.05f, IsPropertySaveable.Yes, description: "How fast the sprite returns back to normal after stretching ends"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float RecoverSpeed { get; set; } public PositionalDeformationParams(XElement element) : base(element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index 118734f9e..d15eb2729 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -14,21 +14,21 @@ namespace Barotrauma.SpriteDeformations /// A positive value means that this deformation is or could be used for multiple sprites. /// This behaviour is not automatic, and has to be implemented for any particular case separately (currently only used in Limbs). /// - [Serialize(-1, true), Editable(minValue: -1, maxValue: 100)] + [Serialize(-1, IsPropertySaveable.Yes), Editable(minValue: -1, maxValue: 100)] public int Sync { get; private set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string TypeName { get; set; } - [Serialize(SpriteDeformation.DeformationBlendMode.Add, true), Editable] + [Serialize(SpriteDeformation.DeformationBlendMode.Add, IsPropertySaveable.Yes), Editable] public SpriteDeformation.DeformationBlendMode BlendMode { get; @@ -37,30 +37,30 @@ namespace Barotrauma.SpriteDeformations public string Name => $"Deformation ({TypeName})"; - [Serialize(1.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float Strength { get; private set; } - [Serialize(90f, true), Editable(MinValueFloat = 0, MaxValueFloat = 90)] + [Serialize(90f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 90)] public float MaxRotation { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseMovementSine { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool StopWhenHostIsDead { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyInWater { get; set; } /// /// Only used if UseMovementSine is enabled. Multiplier for Pi. /// - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float SineOffset { get; set; } public virtual float Frequency { get; set; } = 1; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; @@ -72,7 +72,7 @@ namespace Barotrauma.SpriteDeformations public static readonly Point ShaderMaxResolution = new Point(15, 15); private Point _resolution; - [Serialize("2,2", true)] + [Serialize("2,2", IsPropertySaveable.Yes)] public Point Resolution { get { return _resolution; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index 45bcb951b..7b89b66bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -345,7 +345,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Point(container.Rect.Width, (int)(60 * GUI.Scale)), container.RectTransform) { IsFixedSize = true }, - "Sprite Deformations", textAlignment: Alignment.BottomCenter, font: GUI.LargeFont); + "Sprite Deformations", textAlignment: Alignment.BottomCenter, font: GUIStyle.LargeFont); var resolutionField = GUI.CreatePointField(new Point(subDivX + 1, subDivY + 1), (int)(30 * GUI.Scale), "Resolution", container.RectTransform, "How many vertices the deformable sprite has on the x and y axes. Larger values make the deformations look smoother, but are more performance intensive."); @@ -387,7 +387,7 @@ namespace Barotrauma foreach (SpriteDeformation deformation in deformations) { var deformEditor = new SerializableEntityEditor(container.RectTransform, deformation.Params, - inGame: false, showName: true, titleFont: GUI.SubHeadingFont); + inGame: false, showName: true, titleFont: GUIStyle.SubHeadingFont); deformEditor.RectTransform.MinSize = new Point(deformEditor.Rect.Width, deformEditor.Rect.Height); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 818255063..8336320d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -10,11 +10,19 @@ namespace Barotrauma { public partial class Sprite { + private class TextureRefCounter + { + public Texture2D Texture; + public int RefCount; + } + + private readonly static Dictionary textureRefCounts = new Dictionary(); + private bool cannotBeLoaded; protected volatile bool loadingAsync = false; - - protected Texture2D texture; + + protected Texture2D texture { get; private set; } public Texture2D Texture { get @@ -24,6 +32,8 @@ namespace Barotrauma } } + private string disposeStackTrace; + public bool Loaded { get { return texture != null && !cannotBeLoaded; } @@ -32,7 +42,6 @@ namespace Barotrauma public Sprite(Sprite other) : this(other.texture, other.sourceRect, other.offset, other.rotation) { FilePath = other.FilePath; - FullPath = other.FullPath; Compress = other.Compress; size = other.size; effects = other.effects; @@ -47,18 +56,13 @@ namespace Barotrauma origin = Vector2.Zero; effects = SpriteEffects.None; rotation = newRotation; - FilePath = path; + FilePath = ContentPath.FromRaw(path); AddToList(this); } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(this.FilePath, out Sprite reusedSprite, Compress); - if (reusedSprite != null) - { - FilePath = string.Intern(reusedSprite.FilePath); - FullPath = string.Intern(reusedSprite.FullPath); - } + texture = LoadTexture(FilePath.Value, Compress); if (texture == null) { @@ -118,7 +122,7 @@ namespace Barotrauma public void ReloadTexture(IEnumerable spritesToUpdate) { texture.Dispose(); - texture = TextureLoader.FromFile(FilePath, Compress); + texture = TextureLoader.FromFile(FilePath.Value, Compress); foreach (Sprite sprite in spritesToUpdate) { sprite.texture = texture; @@ -130,14 +134,8 @@ namespace Barotrauma sourceRect = new Rectangle(0, 0, texture.Width, texture.Height); } - public static Texture2D LoadTexture(string file) + public static Texture2D LoadTexture(string file, bool compress = true) { - return LoadTexture(file, out _); - } - - public static Texture2D LoadTexture(string file, out Sprite reusedSprite, bool compress = true) - { - reusedSprite = null; if (string.IsNullOrWhiteSpace(file)) { Texture2D t = null; @@ -147,9 +145,15 @@ namespace Barotrauma }); return t; } - string fullPath = Path.GetFullPath(file); - reusedSprite = FindMatchingSprite(fullPath, requireTexture: true); - if (reusedSprite != null) { return reusedSprite.texture; } + Identifier fullPath = Path.GetFullPath(file).CleanUpPathCrossPlatform(correctFilenameCase: false).ToIdentifier(); + lock (list) + { + if (textureRefCounts.ContainsKey(fullPath)) + { + textureRefCounts[fullPath].RefCount++; + return textureRefCounts[fullPath].Texture; + } + } if (File.Exists(file)) { @@ -159,7 +163,13 @@ namespace Barotrauma DebugConsole.ThrowError("Texture file \"" + file + "\" has incorrect case!"); #endif } - return TextureLoader.FromFile(file, compress); + + Texture2D newTexture = TextureLoader.FromFile(file, compress); + lock (list) + { + textureRefCounts.Add(fullPath, new TextureRefCounter { RefCount = 1, Texture = newTexture }); + } + return newTexture; } else { @@ -170,22 +180,6 @@ namespace Barotrauma return null; } - private static Sprite FindMatchingSprite(string fullPath, bool requireTexture) - { - lock (list) - { - foreach (var wRef in list) - { - if (wRef.TryGetTarget(out Sprite sprite)) - { - bool hasTexture = sprite.texture != null && !sprite.texture.IsDisposed; - if (sprite.FullPath == fullPath && (hasTexture || !requireTexture)) { return sprite; } - } - } - } - return null; - } - public void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate = 0.0f, float scale = 1.0f, SpriteEffects spriteEffect = SpriteEffects.None) { this.Draw(spriteBatch, pos, Color.White, rotate, scale, spriteEffect); @@ -378,15 +372,33 @@ namespace Barotrauma partial void DisposeTexture() { - //check if another sprite is using the same texture - if (!string.IsNullOrEmpty(FilePath)) //file can be empty if the sprite is created directly from a Texture2D instance - { - if (FindMatchingSprite(FullPath, requireTexture: false) != null) { return; } - } - - //if not, free the texture + disposeStackTrace = Environment.StackTrace; if (texture != null) { + //check if another sprite is using the same texture + lock (list) + { + if (!FilePath.IsNullOrEmpty()) //file can be empty if the sprite is created directly from a Texture2D instance + { + Identifier pathKey = FullPath.ToIdentifier(); + if (!pathKey.IsEmpty && textureRefCounts.ContainsKey(pathKey)) + { + textureRefCounts[pathKey].RefCount--; + if (textureRefCounts[pathKey].RefCount <= 0) + { + textureRefCounts.Remove(pathKey); + } + else + { + texture = null; + FilePath = ContentPath.Empty; + return; + } + } + } + } + + //if not, free the texture CrossThread.RequestExecutionOnMainThread(() => { texture.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 2b06523f6..ca6ea6874 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -24,11 +24,11 @@ namespace Barotrauma private double loopStartTime; private bool loopSound; - partial void InitProjSpecific(XElement element, string parentDebugName) + partial void InitProjSpecific(ContentXElement element, string parentDebugName) { particleEmitters = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -36,7 +36,7 @@ namespace Barotrauma particleEmitters.Add(new ParticleEmitter(subElement)); break; case "sound": - var sound = Submarine.LoadRoundSound(subElement); + var sound = RoundSound.Load(subElement); if (sound?.Sound != null) { loopSound = subElement.GetAttributeBool("loop", false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs new file mode 100644 index 000000000..82fa24ac1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/AuthTicket.cs @@ -0,0 +1,27 @@ +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; + + DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); + Steamworks.BeginAuthResult startResult = Steamworks.SteamUser.BeginAuthSession(authTicketData, clientSteamID); + if (startResult != Steamworks.BeginAuthResult.OK) + { + DebugConsole.Log("Authentication failed: failed to start auth session (" + startResult.ToString() + ")"); + } + + return startResult; + } + + public static void StopAuthSession(ulong clientSteamID) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) return; + + DebugConsole.NewMessage("SteamManager ending auth session with Steam client " + clientSteamID); + Steamworks.SteamUser.EndAuthSession(clientSteamID); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs new file mode 100644 index 000000000..93b6b3235 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs @@ -0,0 +1,187 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private readonly struct BBWord + { + [Flags] + public enum TagType + { + None = 0x0, + Bold = 0x1, + Italic = 0x2, + Header = 0x4, + List = 0x8, + NewLine = 0x10 + } + + public readonly string Text; + public readonly Vector2 Size; + public readonly TagType TagTypes; + + public readonly GUIFont Font; + + public BBWord(string text, TagType tagTypes) + { + Text = text; + TagTypes = tagTypes; + Font = tagTypes.HasFlag(TagType.Header) + ? GUIStyle.LargeFont + : tagTypes.HasFlag(TagType.Bold) + ? GUIStyle.SubHeadingFont + : GUIStyle.Font; + Size = Font.MeasureString(Text); + } + } + + private static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container) + { + Point cachedContainerSize = Point.Zero; + List bbWords = new List(); + Stack tagStack = new Stack(); + + void recalculate() + { + if (cachedContainerSize == container.Content.RectTransform.NonScaledSize) { return; } + + bbWords.Clear(); + cachedContainerSize = container.Content.RectTransform.NonScaledSize; + + var matches = new Stack(bbTagRegex.Matches(bbCode).Reverse()); + Match? nextTag = null; + matches.TryPop(out nextTag); + int wordStart = 0; + BBWord.TagType currTagType; + for (int i = 0; i < bbCode.Length; i++) + { + char currChar = bbCode[i]; + currTagType = tagStack.TryPeek(out var t) ? t : BBWord.TagType.None; + + bool charIsCJK = TextManager.IsCJK($"{currChar}"); + bool wordEnd = char.IsWhiteSpace(currChar) || charIsCJK; + int reachedTagLength = 0; + if (nextTag is { Index: int tagIndex, Length: int tagLength } + && i == tagIndex) + { + reachedTagLength = tagLength; + string tagStr = nextTag.Value.Replace("[", "").Replace("]", "").Trim(); + bool isClosing = tagStr.StartsWith("/"); + tagStr = tagStr.Replace("/", "").Trim().ToLowerInvariant(); + BBWord.TagType tagType = tagStr switch + { + "b" => BBWord.TagType.Bold, + "i" => BBWord.TagType.Italic, + "h1" => BBWord.TagType.Header, + _ => BBWord.TagType.None + }; + + if (tagType != BBWord.TagType.None) + { + if (isClosing) + { + if (currTagType == tagType) + { + tagStack.Pop(); + } + } + else + { + tagStack.Push(tagType); + } + } + } + + if (wordEnd || reachedTagLength > 0) + { + string word = bbCode[wordStart..i]; + if (charIsCJK) { word = bbCode[wordStart..(i + 1)]; } + else if (char.IsWhiteSpace(currChar) && currChar != '\n') { word += " "; } + + if (!word.IsNullOrEmpty()) + { + bbWords.Add(new BBWord(word, currTagType)); + } + else if (currChar == '\n') + { + bbWords.Add(new BBWord("", BBWord.TagType.NewLine)); + } + + if (reachedTagLength > 0) + { + i += reachedTagLength - 1; + nextTag = matches.TryPop(out var tag) ? tag : null; + } + + wordStart = i + 1; + } + } + + currTagType = tagStack.TryPeek(out var ft) ? ft : BBWord.TagType.None; + string finalWord = bbCode[wordStart..]; + if (!finalWord.IsNullOrEmpty()) + { + bbWords.Add(new BBWord(finalWord, currTagType)); + } + } + + void draw(SpriteBatch spriteBatch, GUICustomComponent component) + { + recalculate(); + Vector2 currPos = Vector2.Zero; + Vector2 rectPos = component.Rect.Location.ToVector2(); + for (int i = 0; i < bbWords.Count; i++) + { + var bbWord = bbWords[i]; + if (currPos.X > 0.0f + && currPos.X + bbWord.Size.X >= component.Rect.Width) + { + //wrap because we went over width limit + currPos = (0.0f, currPos.Y + bbWord.Size.Y); + } + + bbWord.Font.DrawString( + spriteBatch, + bbWord.Text, + (currPos + rectPos).ToPoint().ToVector2(), + GUIStyle.TextColorNormal, + forceUpperCase: ForceUpperCase.No, + italics: bbWord.TagTypes.HasFlag(BBWord.TagType.Italic)); + bool breakLine + = bbWord.TagTypes.HasFlag(BBWord.TagType.NewLine) + || (i < bbWords.Count - 1 && + bbWords[i + 1].TagTypes.HasFlag(BBWord.TagType.Header) != + bbWord.TagTypes.HasFlag(BBWord.TagType.Header)); + if (breakLine) + { + //break line because of a header change or newline was found + currPos = (0.0f, currPos.Y + bbWord.Size.Y); + } + else + { + currPos.X += bbWord.Size.X; + } + } + + component.RectTransform.NonScaledSize + = (component.RectTransform.NonScaledSize.X, + (int)(currPos.Y + bbWords.LastOrDefault().Size.Y)); + component.RectTransform.RelativeSize + = component.RectTransform.NonScaledSize.ToVector2() / component.Parent.Rect.Size.ToVector2(); + } + + return new GUICustomComponent(new RectTransform(Vector2.One, container.Content.RectTransform), + onDraw: draw); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs new file mode 100644 index 000000000..9c3cfb54b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs @@ -0,0 +1,694 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private string ExtractTitle(ItemOrPackage itemOrPackage) + => itemOrPackage.TryGet(out ContentPackage package) + ? package.Name + : ((Steamworks.Ugc.Item)itemOrPackage).Title; + + private void CreateWorkshopItemDetailContainer( + GUIFrame parent, + out GUIListBox outerContainer, + Action onSelected, + Action onDeselected, + out Action select, + out Action deselect) + { + ItemOrPackage? selectedItemOrPackage = null; + + GUIListBox outContainer = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform), + isHorizontal: true, + style: null) + { + ScrollBarEnabled = false, + ScrollBarVisible = false, + HoverCursor = CursorState.Default + }; + outerContainer = outContainer; + + var selectedLayout = + new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + var selectedHeaderLayout = + new GUILayoutGroup(new RectTransform((1.0f, 0.05f), selectedLayout.RectTransform), + isHorizontal: true, + childAnchor: Anchor.CenterLeft); + + void deselectMethod() + { + if (selectedItemOrPackage is null) { return; } + selectedItemOrPackage = null; + onDeselected(); + } + + deselect = deselectMethod; + + var backButton = + new GUIButton(new RectTransform((0.04f, 1.0f), selectedHeaderLayout.RectTransform), + style: "GUIButtonToggleLeft") + { + OnClicked = (button, o) => + { + deselectMethod(); + return false; + } + }; + var padding = new GUIFrame(new RectTransform((1.0f, 0.005f), selectedLayout.RectTransform), style: null); + var selectedFrame = new GUIFrame(new RectTransform((1.0f, 0.945f), selectedLayout.RectTransform), + style: null); + + var selectionScroller = new GUICustomComponent( + new RectTransform(Vector2.Zero, outerContainer.Parent.RectTransform), + onUpdate: (deltaTime, component) => + { + float targetScroll = selectedItemOrPackage is null + ? 0.0f + : 1.0f; + outContainer.ScrollBar.BarScroll + = MathUtils.NearlyEqual(targetScroll, outContainer.ScrollBar.BarScroll) + ? targetScroll + : MathHelper.Lerp(outContainer.ScrollBar.BarScroll, targetScroll, 0.3f); + }); + + select = itemOrPackage => + { + //showInSteamButton.Visible = itemOrPackage.TryGet(out Steamworks.Ugc.Item _); + //selectedItem = itemOrPackage; + //selectedTitle.Text = ExtractTitle(itemOrPackage); + selectedFrame.ClearChildren(); + + //Jank to fix mouserect not clamping properly + //when shifting all elements to the left + var dropdowns = outContainer.Content.GetAllChildren().ToArray(); + var allChildren = outContainer.Content.GetAllChildren() + .Concat(selectedFrame.GetAllChildren()); + allChildren.ForEach(c => + { + //c.CascadingMouseRectClamp = !dropdowns.Any(dd => dd.IsParentOf(c) || dd.ListBox.IsParentOf(c)); + //c.CanBeFocused = c.CanBeFocused || !c.CascadingMouseRectClamp; + c.ClampMouseRectToParent = !(c.Parent?.Parent is GUIDropDown); + } + ); + + selectedItemOrPackage = itemOrPackage; + onSelected(itemOrPackage, selectedFrame); + }; + } + + private void CreateWorkshopItemList( + GUIFrame parent, + out GUIListBox outerContainer, + out GUIListBox workshopItemList, + Action onSelected) + => CreateWorkshopItemOrPackageList( + parent, + out outerContainer, + out workshopItemList, + onSelected: (ItemOrPackage itemOrPackage, GUIFrame frame) + => onSelected((Steamworks.Ugc.Item)itemOrPackage, frame)); + + private GUIButton CreateShowInSteamButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT) + => new GUIButton( + rectT, + TextManager.Get("WorkshopShowItemInSteam"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + SteamManager.OverlayCustomURL(workshopItem.Url); + return false; + } + }; + + private GUIButton? CreateShowInSteamButton(ItemOrPackage itemOrPackage) + => itemOrPackage.TryGet(out Steamworks.Ugc.Item workshopItem) + ? CreateShowInSteamButton(workshopItem) + : null; + + private void CreateWorkshopItemOrPackageList( + GUIFrame parent, + out GUIListBox outerContainer, + out GUIListBox workshopItemList, + Action onSelected) + { + GUIListBox? itemList = null; + + CreateWorkshopItemDetailContainer( + parent, + out outerContainer, + onSelected: onSelected, + onDeselected: () => itemList?.Deselect(), + out var select, out var deselect); + + itemList = new GUIListBox(new RectTransform(Vector2.One, outerContainer.Content.RectTransform)); + itemList.RectTransform.SetAsFirstChild(); + workshopItemList = itemList; + + var deselectCarrier + = CreateActionCarrier(outerContainer.Content, nameof(deselect).ToIdentifier(), deselect); + + itemList.OnSelected = (component, userData) => + { + //Don't select if hitting the subscribe button + if (GUI.MouseOn.Parent != itemList.Content) { return false; } + + if (!(userData is ItemOrPackage itemOrPackage)) { return false; } + + select(itemOrPackage); + + return true; + }; + } + + private void AddUnpublishedMods(ISet workshopItems) + { + //Users that don't have a proper license cannot publish Workshop items + //(see https://partner.steamgames.com/doc/features/workshop#15) + void clearWithMessage(LocalizedString message) + { + selfModsList.ClearChildren(); + var messageFrame = new GUIFrame(new RectTransform(Vector2.One, selfModsList.Content.RectTransform), + style: null) + { + CanBeFocused = false + }; + new GUITextBlock(new RectTransform((0.5f, 1.0f), messageFrame.RectTransform, Anchor.Center), + text: message, + textAlignment: Alignment.Center, + wrap: true, + font: GUIStyle.Font); + } + + if (SteamManager.IsFreeWeekend()) + { + clearWithMessage(TextManager.Get("FreeWeekendCantPublish")); + return; + } + if (SteamManager.IsFamilyShared()) + { + clearWithMessage(TextManager.Get("FamilySharedCantPublish")); + return; + } + + DateTime getEditTime(ContentPackage p) + => File.GetLastWriteTime(Path.GetDirectoryName(p.Path)!); + + //Find local packages associated with the Workshop items if available + (Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems + .Select(item => (item, + (ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p + => p.SteamWorkshopId != 0 && p.SteamWorkshopId == item.Id))) + //Sort the pairs by last local edit time if available + .OrderBy(t => t.Item2 == null) + .ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime) + .ToArray(); + + int indexOfUserDataInPublishedItemsArray(object userData) + => publishedItems.IndexOf(t + => t.WorkshopItem.Id == ((Steamworks.Ugc.Item)(userData as ItemOrPackage)).Id); + + //Take the existing GUI items that are in the list and sort to match the order of publishedItems + var publishedGuiComponents = selfModsList.Content.Children.OrderBy(c => indexOfUserDataInPublishedItemsArray(c.UserData)).ToArray(); + + //Get mods that haven't been published and add them to the list + var unpublishedMods = ContentPackageManager.LocalPackages + .Where(p => p.SteamWorkshopId == 0 || !publishedItems.Any(item => item.WorkshopItem.Id == p.SteamWorkshopId)) + .OrderByDescending(getEditTime).ToArray(); + + if (unpublishedMods.Any()) + { + var unpublishedHeader + = new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform), + TextManager.Get("UnpublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + } + + foreach (var unpublishedMod in unpublishedMods) + { + var unpublishedFrame = new GUIFrame( + new RectTransform((1.0f, 1.0f / 5.5f), selfModsList.Content.RectTransform), + style: "ListBoxElement") + { + UserData = (ItemOrPackage)unpublishedMod + }; + var unpublishedLayout + = new GUILayoutGroup(new RectTransform(Vector2.One, unpublishedFrame.RectTransform), + isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + var unpublishedPadding + = new GUIFrame( + new RectTransform(Vector2.One, unpublishedLayout.RectTransform, + scaleBasis: ScaleBasis.BothHeight), style: null) + { + CanBeFocused = false + }; + var unpublishedTextBlock + = new GUITextBlock(new RectTransform(Vector2.One, unpublishedLayout.RectTransform), + $"{unpublishedMod.Name}\n\n" + + TextManager.GetWithVariable("LastLocalEditTime", + "[datetime]", + getEditTime(unpublishedMod).ToString()), + font: GUIStyle.Font) + { + CanBeFocused = false + }; + } + + if (publishedGuiComponents.Any()) + { + var publishedHeader + = new GUITextBlock(new RectTransform((1.0f, 1.0f / 11.0f), selfModsList.Content.RectTransform), + TextManager.Get("PublishedModsHeader"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; + } + + foreach (var c in publishedGuiComponents) + { + c.SetAsLastChild(); + var textBlock = (c.FindChild(b => b is GUITextBlock, recursive: true) as GUITextBlock)!; + textBlock.Text += $"\n"; + + int index = indexOfUserDataInPublishedItemsArray(c.UserData); + (Steamworks.Ugc.Item workshopItem, ContentPackage? localMod) = publishedItems[index]; + if (localMod != null) + { + textBlock.Text += $"\n" + TextManager.GetWithVariable("LastLocalEditTime", "[datetime]", getEditTime(localMod).ToString()); + } + textBlock.Text += $"\n" + TextManager.GetWithVariable("LatestPublishTime", "[datetime]", workshopItem.LatestUpdateTime.ToLocalTime().ToString()); + } + } + + private static (GUIButton Button, GUIFrame Sprite) CreatePaddedButton(RectTransform rectT, string style, float spriteScale) + { + var button = new GUIButton( + rectT, + style: null); + + var sprite = new GUIFrame( + new RectTransform(Vector2.One * spriteScale, button.RectTransform, Anchor.Center), + style: style) + { + CanBeFocused = false + }; + + return (button, sprite); + } + + private static void CreateSubscribeButton(Steamworks.Ugc.Item workshopItem, RectTransform rectT, float spriteScale) + { + const string plusButton = "GUIPlusButton"; + const string minusButton = "GUIMinusButton"; + + LocalizedString subscribeTooltip = TextManager.Get("DownloadButton"); + LocalizedString unsubscribeTooptip = TextManager.Get("WorkshopItemUnsubscribe"); + + var (subscribeButton, subscribeButtonSprite) = CreatePaddedButton(rectT, plusButton, spriteScale); + subscribeButton.ToolTip = subscribeTooltip; + + subscribeButton.OnClicked = (button, o) => + { + if (!workshopItem.IsSubscribed) + { + workshopItem.Subscribe(); + TaskPool.Add($"DownloadSubscribedItem{workshopItem.Id}", + SteamManager.Workshop.ForceRedownload(workshopItem), + t => { }); + } + else + { + workshopItem.Unsubscribe(); + SteamManager.Workshop.Uninstall(workshopItem); + } + + return false; + }; + + var buttonStyleUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, subscribeButton.RectTransform), + onUpdate: (deltaTime, component) => + { + if (subscribeButtonSprite.Style is { Identifier: { } styleId }) + { + if (workshopItem.IsSubscribed && styleId != minusButton) + { + subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(minusButton)); + subscribeButton.ToolTip = unsubscribeTooptip; + } + if (!workshopItem.IsSubscribed && styleId != plusButton) + { + subscribeButtonSprite.ApplyStyle(GUIStyle.GetComponentStyle(plusButton)); + subscribeButton.ToolTip = subscribeTooltip; + } + } + }); + + float displayedDownloadAmount = workshopItem.DownloadAmount; + var downloadProgressBar = new GUICustomComponent( + new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center), + onDraw: (spriteBatch, component) => + { + bool visible = workshopItem.IsSubscribed + && (workshopItem.IsDownloading + || workshopItem.IsDownloadPending + || !MathUtils.NearlyEqual(workshopItem.DownloadAmount, displayedDownloadAmount)); + if (!visible) { return; } + + void drawSection(float amount, Color color, float thickness) + => GUI.DrawDonutSection( + spriteBatch, + component.Rect.Center.ToVector2() + (0, 1), + new Range(component.Rect.Width * 0.55f - thickness * 0.5f, component.Rect.Width * 0.55f + thickness * 0.5f), + amount * MathF.PI * 2.0f, + color); + + void drawSectionFuzzy(float amount, Color color, float thickness) + { + drawSection(amount, color, thickness); + drawSection(amount, color * 0.6f, thickness + 0.5f); + drawSection(amount, color * 0.3f, thickness + 1.0f); + } + + drawSectionFuzzy(1.0f, Color.Lerp(Color.Black, GUIStyle.Blue, 0.2f), component.Rect.Width * 0.25f); + drawSectionFuzzy(1.0f, Color.Black, component.Rect.Width * 0.15f); + drawSectionFuzzy(displayedDownloadAmount, GUIStyle.Green, component.Rect.Width * 0.08f); + }, + onUpdate: (deltaTime, component) => + { + displayedDownloadAmount = Math.Min( + workshopItem.DownloadAmount, + MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f)); + }) + { + CanBeFocused = false + }; + } + + private void PopulateItemList(GUIListBox itemListBox, Task> items, bool includeSubscribeButton, Action>? onFill = null) + { + itemListBox.ClearChildren(); + itemListBox.Deselect(); + itemListBox.ScrollBar.BarScroll = 0.0f; + TaskPool.Add("PopulateTabWithItemList", items, + (t) => + { + taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; + itemListBox.ClearChildren(); + var workshopItems = ((Task>)t).Result; + foreach (var workshopItem in workshopItems) + { + var itemFrame = new GUIFrame( + new RectTransform((1.0f, 1.0f / 5.5f), itemListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = (ItemOrPackage)workshopItem + }; + var itemLayout = new GUILayoutGroup( + new RectTransform(Vector2.One, itemFrame.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + var thumbnailContainer + = CreateThumbnailContainer(itemLayout, Vector2.One, ScaleBasis.BothHeight); + CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer); + thumbnailContainer.CanBeFocused = false; + thumbnailContainer.GetAllChildren().ForEach(c => c.CanBeFocused = false); + + var title = new GUITextBlock( + new RectTransform(Vector2.One, itemLayout.RectTransform), + workshopItem.Title, font: GUIStyle.Font) + { + CanBeFocused = false + }; + + if (includeSubscribeButton) + { + CreateSubscribeButton(workshopItem, new RectTransform(Vector2.One, itemLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), spriteScale: 0.4f); + } + } + onFill?.Invoke(workshopItems); + }); + } + + private GUIFrame CreateThumbnailContainer( + GUIComponent parent, + Vector2 relativeSize, + ScaleBasis scaleBasis) + => new GUIFrame(new RectTransform(relativeSize, parent.RectTransform, scaleBasis: scaleBasis), + style: "GUIFrameListBox"); + + private SteamManager.Workshop.ItemThumbnail CreateItemThumbnail( + in Steamworks.Ugc.Item workshopItem, + CancellationToken cancellationToken, + GUIFrame thumbnailContainer) + { + var thumbnail = new SteamManager.Workshop.ItemThumbnail(workshopItem, cancellationToken); + itemThumbnails.Add(thumbnail); + CreateAsyncThumbnailComponent(thumbnailContainer, () => thumbnail.Texture, () => thumbnail.Loading); + return thumbnail; + } + + private GUICustomComponent CreateAsyncThumbnailComponent(GUIFrame thumbnailContainer, Func textureGetter, Func throbberEnabled) + { + int randomThrobberOffset = Rand.Range(0, 10, Rand.RandSync.Unsynced); + return new GUICustomComponent( + new RectTransform(Vector2.One, thumbnailContainer.RectTransform, Anchor.Center), + onDraw: (spriteBatch, component) => + { + Rectangle rect = component.Rect; + Texture2D? texture = textureGetter(); + if (texture != null) + { + rect.Location += (4, 4); + rect.Size -= (8, 8); + Point destinationSizeMaxWidth = (rect.Width, rect.Width * texture.Height / texture.Width); + Point destinationSizeMaxHeight = (rect.Height * texture.Width / texture.Height, rect.Height); + Point destinationSize = destinationSizeMaxHeight.X > rect.Width + ? destinationSizeMaxWidth + : destinationSizeMaxHeight; + Rectangle destinationRectangle = new Rectangle( + rect.Center.X - destinationSize.X / 2, + rect.Center.Y - destinationSize.Y / 2, + destinationSize.X, + destinationSize.Y); + spriteBatch.Draw(texture, destinationRectangle, Color.White); + } + else if (throbberEnabled()) + { + var sheet = GUIStyle.GenericThrobber; + Vector2 pos = rect.Center.ToVector2() - Vector2.One * rect.Height * 0.4f; + sheet.Draw(spriteBatch, ((int)Math.Floor(Timing.TotalTime * 24.0f) + randomThrobberOffset) % sheet.FrameCount, pos, Color.White, + origin: Vector2.Zero, rotate: 0.0f, + scale: Vector2.One * component.Rect.Height / sheet.FrameSize.ToVector2() * 0.8f); + } + }); + } + + private GUIListBox CreateTagsList(IEnumerable tags, RectTransform rectT, bool canBeFocused) + { + var tagsList + = new GUIListBox(rectT, style: null, isHorizontal: false) + { + UseGridLayout = true, + ScrollBarEnabled = false, + ScrollBarVisible = false, + HideChildrenOutsideFrame = false, + Spacing = GUI.IntScale(4) + }; + tagsList.Content.ClampMouseRectToParent = false; + foreach (Identifier tag in tags) + { + var tagBtn = new GUIButton( + new RectTransform(new Vector2(0.25f, 1.0f / 8.0f), tagsList.Content.RectTransform, + anchor: Anchor.TopLeft), + TextManager.Get($"workshop.contenttag.{tag.Value.RemoveWhitespace()}") + .Fallback(tag.Value.CapitaliseFirstInvariant()), style: "GUIButtonRound") + { + CanBeFocused = canBeFocused, + Selected = !canBeFocused, + UserData = tag + }; + tagBtn.RectTransform.NonScaledSize + = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(5)); + tagBtn.RectTransform.IsFixedSize = true; + tagBtn.ClampMouseRectToParent = false; + } + + return tagsList; + } + + private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) + { + taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; + + var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform)); + + var headerLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.1f), verticalLayout.RectTransform), + isHorizontal: true) { Stretch = true }; + + var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform)); + + var selectedTitle = + new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title, + font: GUIStyle.LargeFont); + + var author = workshopItem.Owner; + var authorButton = new GUIButton(new RectTransform((1.0f, 0.5f), + titleAndAuthorLayout.RectTransform), + style: null, + textAlignment: Alignment.CenterLeft) + { + ForceUpperCase = ForceUpperCase.No, + Font = GUIStyle.SubHeadingFont, + TextColor = GUIStyle.TextColorNormal, + HoverTextColor = Color.White, + SelectedTextColor = GUIStyle.TextColorNormal, + OnClicked = (button, o) => + { + SteamManager.OverlayCustomURL( + $"https://steamcommunity.com/profiles/{author.Id}/myworkshopfiles/?appid={SteamManager.AppID}"); + return false; + } + }; + var authorPadding = authorButton.GetChild().Padding; + + RectTransform rightSideButtonRectT() + => new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight); + + var (reinstallButton, reinstallSprite) = CreatePaddedButton( + rightSideButtonRectT(), + "GUIReloadButton", + spriteScale: 0.8f); + reinstallButton.ToolTip = TextManager.Get("WorkshopItemReinstall"); + reinstallButton.OnClicked += (button, o) => + { + SteamManager.Workshop.Uninstall(workshopItem); + TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.ForceRedownload(workshopItem), t => { }); + return false; + }; + var reinstallButtonUpdater = new GUICustomComponent( + new RectTransform(Vector2.Zero, reinstallButton.RectTransform), + onUpdate: (f, component) => + { + reinstallButton.Visible = workshopItem.IsSubscribed; + reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending && + !SteamManager.Workshop.IsInstalling(workshopItem); + reinstallSprite.Color = reinstallButton.Enabled + ? reinstallSprite.Style.Color + : Color.DimGray; + }); + CreateSubscribeButton(workshopItem, + rightSideButtonRectT(), + spriteScale: 0.8f); + + var padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null); + + var horizontalLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.45f), verticalLayout.RectTransform), + isHorizontal: true) + { + Stretch = true + }; + + TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) => + { + authorButton.Text = author.Name; + authorButton.RectTransform.NonScaledSize = + ((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z), + authorButton.RectTransform.NonScaledSize.Y); + }); + + var thumbnailSuperContainer = new GUIFrame( + new RectTransform(Vector2.One, horizontalLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null); + GUIFrame thumbnailContainer = CreateThumbnailContainer(thumbnailSuperContainer, Vector2.One, + scaleBasis: ScaleBasis.BothHeight); + CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer); + thumbnailContainer.RectTransform.Anchor = Anchor.Center; + thumbnailContainer.RectTransform.Pivot = Pivot.Center; + + var statsBox = new GUIFrame(new RectTransform((0.6f, 1.0f), horizontalLayout.RectTransform), + style: "GUIFrameListBox"); + + #region Stats box + var statsHorizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, statsBox.RectTransform), isHorizontal: true); + var statsVertical0 + = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform)); + + statFrame("", ""); //padding + + var scoreFrame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), style: null); + var scoreLabel = new GUITextBlock(new RectTransform((0.4f, 1.0f), scoreFrame.RectTransform), + TextManager.Get("WorkshopItemScore"), font: GUIStyle.SubHeadingFont); + var scoreStarContainer + = new GUILayoutGroup( + new RectTransform((0.6f, 1.0f), scoreFrame.RectTransform, Anchor.CenterRight), + isHorizontal: true, + childAnchor: Anchor.CenterLeft) { Stretch = true }; + var starColor = Color.Lerp( + Color.Lerp(Color.Red, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)), + Color.Lime, Math.Max(0.0f, (workshopItem.Score - 0.5f) * 2.0f)); + for (int i = 0; i < 5; i++) + { + bool isStarLit = i <= Round(workshopItem.Score * 5.0f); + var star = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: isStarLit ? "GUIStarIconBright" : "GUIStarIconDark"); + if (isStarLit) + { + star.Color = starColor; + star.HoverColor = starColor; + star.SelectedColor = starColor; + } + } + var scoreVoteCountPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null); + var scoreVoteCount = new GUITextBlock( + new RectTransform(Vector2.One, scoreStarContainer.RectTransform), + TextManager.GetWithVariable("WorkshopItemVotes", "[VoteCount]", + (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.CenterLeft) + { + Padding = Vector4.Zero + }; + + void statFrame(LocalizedString labelText, LocalizedString dataText) + { + var frame = new GUIFrame(new RectTransform((1.0f, 0.12f), statsVertical0!.RectTransform), style: null); + var label = new GUITextBlock(new RectTransform((0.4f, 1.0f), frame.RectTransform), + labelText, font: GUIStyle.SubHeadingFont); + var data = new GUITextBlock(new RectTransform((0.6f, 1.0f), frame.RectTransform, Anchor.CenterRight), + dataText, font: GUIStyle.Font) + { + Padding = Vector4.Zero + }; + } + + statFrame(TextManager.Get("WorkshopItemFileSize"), MathUtils.GetBytesReadable(workshopItem.SizeOfFileInBytes)); + statFrame(TextManager.Get("WorkshopItemCreationDate"), workshopItem.Created.ToShortDateString()); + statFrame(TextManager.Get("WorkshopItemModificationDate"), workshopItem.Updated.ToShortDateString()); + + var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), + TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); + CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((1.0f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + #endregion + + var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); + CreateBBCodeElement(workshopItem.Description, descriptionListBox); + + var showInSteamContainer + = new GUIFrame(new RectTransform((1.0f, 0.05f), verticalLayout.RectTransform), style: null); + CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), showInSteamContainer.RectTransform, Anchor.CenterRight)); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs new file mode 100644 index 000000000..b67cab661 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -0,0 +1,422 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + private enum LobbyState + { + NotConnected, + Creating, + Owner, + Joining, + Joined + } + private static UInt64 lobbyID = 0; + private static LobbyState lobbyState = LobbyState.NotConnected; + private static Steamworks.Data.Lobby? currentLobby; + public static UInt64 CurrentLobbyID + { + get { return currentLobby?.Id ?? 0; } + } + + public static void CreateLobby(ServerSettings serverSettings) + { + if (lobbyState != LobbyState.NotConnected) { return; } + lobbyState = LobbyState.Creating; + TaskPool.Add("CreateLobbyAsync", Steamworks.SteamMatchmaking.CreateLobbyAsync(serverSettings.MaxPlayers + 10), + (lobby) => + { + if (lobbyState != LobbyState.Creating) + { + LeaveLobby(); + return; + } + + currentLobby = ((Task)lobby).Result; + + if (currentLobby == null) + { + DebugConsole.ThrowError("Failed to create Steam lobby"); + lobbyState = LobbyState.NotConnected; + return; + } + + DebugConsole.NewMessage("Lobby created!", Microsoft.Xna.Framework.Color.Lime); + + lobbyState = LobbyState.Owner; + lobbyID = (currentLobby?.Id).Value; + + if (serverSettings.IsPublic) + { + currentLobby?.SetPublic(); + } + else + { + currentLobby?.SetFriendsOnly(); + } + currentLobby?.SetJoinable(true); + + UpdateLobby(serverSettings); + }); + } + + public static void UpdateLobby(ServerSettings serverSettings) + { + if (GameMain.Client == null) + { + LeaveLobby(); + } + + if (lobbyState == LobbyState.NotConnected) + { + CreateLobby(serverSettings); + } + + if (lobbyState != LobbyState.Owner) + { + return; + } + + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); + + currentLobby?.SetData("name", serverSettings.ServerName); + currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); + currentLobby?.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); + //currentLobby?.SetData("hostipaddress", lobbyIP); + string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString(); + currentLobby?.SetData("pinglocation", pingLocation ?? ""); + currentLobby?.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); + currentLobby?.SetData("haspassword", serverSettings.HasPassword.ToString()); + + currentLobby?.SetData("message", serverSettings.ServerMessageText); + currentLobby?.SetData("version", GameMain.Version.ToString()); + + currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); + currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); + currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); + currentLobby?.SetData("usingwhitelist", (serverSettings.Whitelist != null && serverSettings.Whitelist.Enabled).ToString()); + currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); + currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); + currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); + currentLobby?.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); + currentLobby?.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); + currentLobby?.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); + currentLobby?.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); + currentLobby?.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); + currentLobby?.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); + currentLobby?.SetData("playstyle", serverSettings.PlayStyle.ToString()); + currentLobby?.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier.Value ?? ""); + + DebugConsole.Log("Lobby updated!"); + } + + public static void LeaveLobby() + { + if (lobbyState != LobbyState.NotConnected) + { + currentLobby?.Leave(); currentLobby = null; + lobbyState = LobbyState.NotConnected; + + lobbyID = 0; + + Steamworks.SteamMatchmaking.ResetActions(); + } + } + public static void JoinLobby(UInt64 id, bool joinServer) + { + if (currentLobby.HasValue && currentLobby.Value.Id == id) { return; } + if (lobbyID == id) { return; } + lobbyState = LobbyState.Joining; + lobbyID = id; + + TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), + (lobby) => + { + currentLobby = ((Task)lobby).Result; + lobbyState = LobbyState.Joined; + lobbyID = (currentLobby?.Id).Value; + if (joinServer) + { + GameMain.Instance.ConnectLobby = 0; + GameMain.Instance.ConnectName = currentLobby?.GetData("servername"); + GameMain.Instance.ConnectEndpoint = SteamIDUInt64ToString((currentLobby?.Owner.Id).Value); + } + }); + } + + public static bool GetServers(Action addToServerList, Action serverQueryFinished) + { + if (!IsInitialized) { return false; } + + int doneTasks = 0; + void taskDone() + { + doneTasks++; + if (doneTasks >= 2) + { + serverQueryFinished?.Invoke(); + serverQueryFinished = null; + } + } + + + Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => + { + DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); + }; + + TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), + (t) => + { + Steamworks.Dispatch.OnDebugCallback = null; + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies"); + taskDone(); + return; + } + var lobbies = ((Task>)t).Result; + if (lobbies != null) + { + foreach (var lobby in lobbies) + { + if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } + + ServerInfo serverInfo = new ServerInfo(); + serverInfo.ServerName = lobby.GetData("name"); + serverInfo.OwnerID = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); + serverInfo.LobbyID = lobby.Id; + bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword); + serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0; + serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1; + serverInfo.RespondedToSteamQuery = true; + + AssignLobbyDataToServerInfo(lobby, serverInfo); + + addToServerList(serverInfo); + } + } + taskDone(); + }); + + Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); + void onServer(Steamworks.Data.ServerInfo info, bool responsive) + { + if (string.IsNullOrEmpty(info.Name)) { return; } + + ServerInfo serverInfo = new ServerInfo + { + ServerName = info.Name, + HasPassword = info.Passworded, + IP = info.Address.ToString(), + Port = info.ConnectionPort.ToString(), + PlayerCount = info.Players, + MaxPlayers = info.MaxPlayers, + RespondedToSteamQuery = responsive + }; + + if (responsive) + { + TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(), + (t) => + { + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name); + return; + } + + var rules = ((Task>)t).Result; + AssignServerRulesToServerInfo(rules, serverInfo); + + CrossThread.RequestExecutionOnMainThread(() => + { + addToServerList(serverInfo); + }); + }); + } + else + { + CrossThread.RequestExecutionOnMainThread(() => + { + addToServerList(serverInfo); + }); + } + + } + serverQuery.OnResponsiveServer += (info) => onServer(info, true); + serverQuery.OnUnresponsiveServer += (info) => onServer(info, false); + + TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(), + (t) => + { + serverQuery.Dispose(); + taskDone(); + if (t.Status == TaskStatus.Faulted) + { + TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); + return; + } + }); + + return true; + } + + public static async Task> LobbyQueryRequest() + { + List allLobbies = new List(); + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() + .FilterDistanceWorldwide() + .WithMaxResults(50); + //steamworks seems to unable to retrieve more than 50 + //lobbies per request; to work around this, we'll make + //up to 10 requests, asking to ignore all previous results + //in each subsequent request + for (int i = 0; i < 10; i++) + { + Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); + if (lobbies == null) { break; } + foreach (var l in lobbies) + { + lobbyQuery = lobbyQuery + .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); + } + allLobbies.AddRange(lobbies); + } + + //make sure all returned lobbies are distinct, don't want any duplicates here + return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); + } + + public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) + { + serverInfo.OwnerVerified = true; + + serverInfo.ServerMessage = lobby.GetData("message"); + serverInfo.GameVersion = lobby.GetData("version"); + + serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); + serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); + + string workshopIdData = lobby.GetData("contentpackageid"); + if (!string.IsNullOrEmpty(workshopIdData)) + { + serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData)); + } + else + { + string[] workshopUrls = lobby.GetData("contentpackageurl").Split(','); + serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); + } + + serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); + if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } + if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } + + serverInfo.AllowSpectating = getLobbyBool("allowspectating"); + serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); + serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); + serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); + serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); + if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } + + serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; + serverInfo.GameMode = (lobby.GetData("gamemode") ?? "").ToIdentifier(); + if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); + } + + string pingLocation = lobby.GetData("pinglocation"); + if (!string.IsNullOrEmpty(pingLocation)) + { + serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation); + } + + bool? getLobbyBool(string key) + { + string data = lobby.GetData(key); + if (string.IsNullOrEmpty(data)) { return null; } + return data == "True" || data == "true"; + } + } + + public static void AssignServerRulesToServerInfo(Dictionary rules, ServerInfo serverInfo) + { + serverInfo.OwnerVerified = true; + + if (rules == null) { return; } + + if (rules.ContainsKey("message")) serverInfo.ServerMessage = rules["message"]; + if (rules.ContainsKey("version")) serverInfo.GameVersion = rules["version"]; + + if (rules.ContainsKey("playercount")) + { + if (int.TryParse(rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; + } + + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); + if (rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); + if (rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); + if (rules.ContainsKey("contentpackageid")) + { + serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"])); + } + else if (rules.ContainsKey("contentpackageurl")) + { + string[] workshopUrls = rules["contentpackageurl"].Split(','); + serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls)); + } + + if (rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = rules["usingwhitelist"] == "True"; + if (rules.ContainsKey("modeselectionmode")) + { + if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode; + } + if (rules.ContainsKey("subselectionmode")) + { + if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode; + } + if (rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = rules["allowspectating"] == "True"; + if (rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; + if (rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; + if (rules.ContainsKey("traitors")) + { + if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; + } + + if (rules.ContainsKey("gamestarted")) serverInfo.GameStarted = rules["gamestarted"] == "True"; + if (rules.ContainsKey("gamemode")) + { + serverInfo.GameMode = rules["gamemode"].ToIdentifier(); + } + if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle)) + { + serverInfo.PlayStyle = playStyle; + } + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopIds.Clear(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs new file mode 100644 index 000000000..e34fabd34 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -0,0 +1,531 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Mime; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Directory = Barotrauma.IO.Directory; +using ItemOrPackage = Barotrauma.Either; +using Path = Barotrauma.IO.Path; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private class LocalThumbnail : IDisposable + { + public Texture2D? Texture { get; private set; } = null; + public bool Loading = true; + + public LocalThumbnail(string path) + { + TaskPool.Add($"LocalThumbnail {path}", + Task.Run(async () => + { + await Task.Yield(); + return TextureLoader.FromFile(path, compress: false, mipmap: false); + }), + (t) => + { + Loading = false; + Task texTask = (t as Task)!; + if (disposed) + { + texTask.Result?.Dispose(); + } + else + { + Texture = texTask.Result; + } + }); + } + + ~LocalThumbnail() + { + Dispose(); + } + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + + disposed = true; + Texture?.Dispose(); + } + } + + private LocalThumbnail? localThumbnail = null; + + private void CreateLocalThumbnail(string path, GUIFrame thumbnailContainer) + { + thumbnailContainer.ClearChildren(); + localThumbnail?.Dispose(); + localThumbnail = new LocalThumbnail(path); + CreateAsyncThumbnailComponent(thumbnailContainer, () => localThumbnail?.Texture, () => localThumbnail is { Loading: true }); + } + + private static async Task<(int FileCount, int ByteCount)> GetModDirInfo(string dir, GUITextBlock label) + { + int fileCount = 0; + int byteCount = 0; + + var files = Directory.GetFiles(dir, pattern: "*", option: System.IO.SearchOption.AllDirectories); + foreach (var file in files) + { + await Task.Yield(); + fileCount++; + byteCount += (int)(new Barotrauma.IO.FileInfo(file).Length); + label.Text = TextManager.GetWithVariables( + "ModDirInfo", + ("[filecount]", fileCount.ToString(CultureInfo.InvariantCulture)), + ("[size]", MathUtils.GetBytesReadable(byteCount))); + } + + return (fileCount, byteCount); + } + + private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) + { + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.WorkshopPackages.Refresh(); + + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + + void deselectItem() + { + deselectAction?.Invoke(); + SelectTab(Tab.Publish); + } + + parentFrame.ClearChildren(); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform), + childAnchor: Anchor.TopCenter); + + Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default; + ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package) + ? package + : ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + ContentPackage? workshopPackage + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + if (localPackage is null) + { + new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null); + + //Local copy does not exist; check for Workshop copy + bool workshopCopyExists = + ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id); + + new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform), + TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"), + wrap: true); + + var buttonLayout = new GUILayoutGroup(new RectTransform((0.6f, 0.1f), mainLayout.RectTransform), + isHorizontal: true); + var yesButton = new GUIButton(new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform), + text: TextManager.Get("Yes")) + { + OnClicked = (button, o) => + { + CoroutineManager.StartCoroutine(MessageBoxCoroutine((currentStepText, messageBox) + => CreateLocalCopy(currentStepText, workshopItem, parentFrame)), + $"CreateLocalCopy {workshopItem.Id}"); + return false; + } + }; + var noButton = new GUIButton(new RectTransform((0.5f, 1.0f), buttonLayout.RectTransform), + text: TextManager.Get("No")) + { + OnClicked = (button, o) => + { + deselectItem(); + return false; + } + }; + } + else + { + if (!ContentPackageManager.LocalPackages.Contains(localPackage)) + { + throw new Exception($"Content package \"{localPackage.Name}\" is not a local package!"); + } + + var selectedTitle = + new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), workshopItem.Title ?? localPackage.Name, + font: GUIStyle.LargeFont); + if (workshopItem.Id != 0) + { + var showInSteamButton = CreateShowInSteamButton(workshopItem, new RectTransform((0.2f, 1.0f), selectedTitle.RectTransform, Anchor.CenterRight)); + } + + Spacer(mainLayout, height: 0.03f); + + var (leftTop, _, rightTop) + = CreateSidebars(mainLayout, leftWidth: 0.2f, centerWidth: 0.01f, rightWidth: 0.79f, + height: 0.4f); + leftTop.Stretch = true; + rightTop.Stretch = true; + + Label(leftTop, TextManager.Get("WorkshopItemPreviewImage"), GUIStyle.SubHeadingFont); + string? thumbnailPath = null; + var thumbnailContainer = CreateThumbnailContainer(leftTop, Vector2.One, ScaleBasis.BothWidth); + if (workshopItem.Id != 0) + { + CreateItemThumbnail(workshopItem, taskCancelSrc.Token, thumbnailContainer); + } + + var browseThumbnail = + new GUIButton(NewItemRectT(leftTop), + TextManager.Get("WorkshopItemBrowse"), style: "GUIButtonSmall") + { + OnClicked = (button, o) => + { + FileSelection.ClearFileTypeFilters(); + FileSelection.AddFileTypeFilter("PNG", "*.png"); + FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); + FileSelection.AddFileTypeFilter("All files", "*.*"); + FileSelection.SelectFileTypeFilter("*.png"); + FileSelection.CurrentDirectory + = Path.GetFullPath(Path.GetDirectoryName(localPackage.Path)!); + + FileSelection.OnFileSelected = (fn) => + { + thumbnailPath = fn; + CreateLocalThumbnail(thumbnailPath, thumbnailContainer); + }; + + FileSelection.Open = true; + + return false; + } + }; + + Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont); + var titleTextBox = new GUITextBox(NewItemRectT(rightTop), workshopItem.Title ?? localPackage.Name); + + Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont); + var descriptionTextBox + = ScrollableTextBox(rightTop, 6.0f, workshopItem.Description ?? string.Empty); + + var (leftBottom, _, rightBottom) + = CreateSidebars(mainLayout, leftWidth: 0.49f, centerWidth: 0.01f, rightWidth: 0.5f, height: 0.5f); + leftBottom.Stretch = true; + rightBottom.Stretch = true; + + Label(leftBottom, TextManager.Get("WorkshopItemVersion"), GUIStyle.SubHeadingFont); + var modVersion = localPackage.ModVersion; + if (workshopPackage is { ModVersion: { } workshopVersion } && + modVersion.Equals(workshopVersion, StringComparison.OrdinalIgnoreCase)) + { + modVersion = ModProject.IncrementModVersion(modVersion); + } + + char[] forbiddenVersionCharacters = { ';', '=' }; + var versionTextBox = new GUITextBox(NewItemRectT(leftBottom), modVersion); + versionTextBox.OnTextChanged += (box, text) => + { + if (text.Any(c => forbiddenVersionCharacters.Contains(c))) + { + foreach (var c in forbiddenVersionCharacters) + { + text = text.Replace($"{c}", ""); + } + + box.Text = text; + box.Flash(GUIStyle.Red); + } + + return true; + }; + + Label(leftBottom, TextManager.Get("WorkshopItemChangeNote"), GUIStyle.SubHeadingFont); + var changeNoteTextBox = ScrollableTextBox(leftBottom, 5.0f, ""); + + Label(rightBottom, TextManager.Get("WorkshopItemTags"), GUIStyle.SubHeadingFont); + var tagsList = CreateTagsList(SteamManager.Workshop.Tags, NewItemRectT(rightBottom, heightScale: 4.0f), + canBeFocused: true); + Dictionary tagButtons = tagsList.Content.Children.Cast() + .Select(b => ((Identifier)b.UserData, b)).ToDictionary(); + if (workshopItem.Tags != null) + { + foreach (Identifier tag in workshopItem.Tags.ToIdentifiers()) + { + if (tagButtons.TryGetValue(tag, out var button)) { button.Selected = true; } + } + } + + GUILayoutGroup visibilityLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true); + + var visibilityLabel = Label(visibilityLayout, TextManager.Get("WorkshopItemVisibility"), GUIStyle.SubHeadingFont); + visibilityLabel.RectTransform.RelativeSize = (0.6f, 1.0f); + visibilityLabel.TextAlignment = Alignment.CenterRight; + + Steamworks.Ugc.Visibility visibility = workshopItem.Visibility; + var visibilityDropdown = DropdownEnum( + visibilityLayout, + (v) => TextManager.Get($"WorkshopItemVisibility.{v}"), + visibility, + (v) => visibility = v); + visibilityDropdown.RectTransform.RelativeSize = (0.4f, 1.0f); + + var fileInfoLabel = Label(rightBottom, "", GUIStyle.Font, heightScale: 1.0f); + fileInfoLabel.TextAlignment = Alignment.CenterRight; + TaskPool.Add($"FileInfoLabel{workshopItem.Id}", GetModDirInfo(localPackage.Dir, fileInfoLabel), t => { }); + + GUILayoutGroup buttonLayout = new GUILayoutGroup(NewItemRectT(rightBottom), isHorizontal: true, childAnchor: Anchor.CenterRight); + + RectTransform newButtonRectT() + => new RectTransform((0.4f, 1.0f), buttonLayout.RectTransform); + + var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemPublish")) + { + OnClicked = (button, o) => + { + //Reload the package to force hash recalculation + string packageName = localPackage.Name; + localPackage = ContentPackageManager.ReloadContentPackage(localPackage); + if (localPackage is null) + { + throw new Exception($"\"{packageName}\" was removed upon reload"); + } + + //Set up the Ugc.Editor object that we'll need to publish + Steamworks.Ugc.Editor ugcEditor = + workshopItem.Id == 0 + ? Steamworks.Ugc.Editor.NewCommunityFile + : new Steamworks.Ugc.Editor(workshopItem.Id); + ugcEditor = ugcEditor.WithTitle(titleTextBox.Text) + .WithDescription(descriptionTextBox.Text) + .WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value)) + .WithChangeLog(changeNoteTextBox.Text) + .WithMetaData($"gameversion={localPackage.GameVersion};modversion={versionTextBox.Text}") + .WithVisibility(visibility) + .WithPreviewFile(thumbnailPath); + + CoroutineManager.StartCoroutine( + MessageBoxCoroutine((currentStepText, messageBox) + => PublishItem(currentStepText, messageBox, versionTextBox.Text, ugcEditor, localPackage))); + + return false; + } + }; + + if (workshopItem.Id != 0) + { + var deleteItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemDelete"), color: GUIStyle.Red) + { + OnClicked = (button, o) => + { + var confirmDeletion = new GUIMessageBox( + headerText: TextManager.Get("WorkshopItemDelete"), + text: TextManager.GetWithVariable("WorkshopItemDeleteVerification", "[itemname]", workshopItem.Title!), + buttons: new[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmDeletion.Buttons[0].OnClicked = (yesBuffer, o1) => + { + TaskPool.Add($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id), + t => + { + confirmDeletion.Close(); + deselectItem(); + }); + return false; + }; + confirmDeletion.Buttons[1].OnClicked = (noButton, o1) => + { + confirmDeletion.Close(); + return false; + }; + + return false; + }, + HoverColor = Color.Lerp(GUIStyle.Red, Color.White, 0.3f), + PressedColor = Color.Lerp(GUIStyle.Red, Color.Black, 0.3f), + }; + deleteItemButton.TextBlock.TextColor = Color.Black; + deleteItemButton.TextBlock.HoverTextColor = Color.Black; + } + } + } + + private IEnumerable MessageBoxCoroutine(Func> subcoroutine) + { + var messageBox = new GUIMessageBox("", "", relativeSize: (0.4f, 0.4f), buttons: new [] { TextManager.Get("Cancel") }); + messageBox.Buttons[0].OnClicked = (button, o) => + { + messageBox.Close(); + return false; + }; + + var currentStepText = new GUITextBlock(new RectTransform((1.0f, 0.8f), messageBox.InnerFrame.RectTransform), + "...", font: GUIStyle.Font) + { + CanBeFocused = false + }; + + foreach (var status in subcoroutine(currentStepText, messageBox)) + { + if (messageBox.Closed) + { + yield return CoroutineStatus.Success; + yield break; + } + else if (status == CoroutineStatus.Failure || status == CoroutineStatus.Success) + { + messageBox.Close(); + yield return status; + yield break; + } + else + { + yield return status; + } + } + } + + private IEnumerable CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) + { + ContentPackage? workshopCopy = + ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); + if (workshopCopy is null) + { + if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) + { + //Must download! + while (!SteamManager.Workshop.CanBeInstalled(workshopItem)) + { + bool shouldForceInstall = workshopItem.IsInstalled + && Directory.Exists(workshopItem.Directory) + && !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem); + shouldForceInstall |= workshopItem is + { IsDownloading: false, IsDownloadPending: false, IsInstalled: false }; + if (shouldForceInstall) + { + SteamManager.Workshop.ForceRedownload(workshopItem); + } + currentStepText.Text = $"Downloading {Percentage(workshopItem.DownloadAmount)}"; + yield return new WaitForSeconds(0.5f); + } + } + else + { + SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); + } + TaskPool.Add($"Install {workshopItem.Title}", + SteamManager.Workshop.WaitForInstall(workshopItem), + (t) => + { + ContentPackageManager.WorkshopPackages.Refresh(); + }); + while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + { + currentStepText.Text = $"Installing"; + yield return new WaitForSeconds(0.5f); + } + + workshopCopy = + ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id); + } + + bool localCopyMade = false; + TaskPool.Add($"Create local copy {workshopItem.Title}", + SteamManager.Workshop.CreateLocalCopy(workshopCopy), + (t) => + { + ContentPackageManager.LocalPackages.Refresh(); + localCopyMade = true; + }); + while (!localCopyMade) + { + currentStepText.Text = $"Creating local copy"; + yield return new WaitForSeconds(0.5f); + } + + PopulatePublishTab(workshopItem, parentFrame); + + yield return CoroutineStatus.Success; + } + + private IEnumerable PublishItem( + GUITextBlock currentStepText, GUIMessageBox messageBox, + string modVersion, Steamworks.Ugc.Editor editor, ContentPackage localPackage) + { + bool stagingReady = false; + TaskPool.Add("CreatePublishStagingCopy", + SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage), + (t) => + { + Exception? exception = t.Exception?.InnerException ?? t.Exception; + if (exception != null) + { + throw new Exception($"Failed to create staging copy: {exception.Message} {exception.StackTrace}"); + } + stagingReady = true; + }); + currentStepText.Text = "Copying item to staging folder..."; + while (!stagingReady) { yield return new WaitForSeconds(0.5f); } + + editor = editor + .WithContent(SteamManager.Workshop.PublishStagingDir) + .ForAppId(SteamManager.AppID); + + messageBox.Buttons[0].Enabled = false; + Steamworks.Ugc.PublishResult? result = null; + TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", + editor.SubmitAsync(), + (t) => + { + result = ((Task)t).Result; + }); + currentStepText.Text = "Submitting item to the Workshop..."; + while (!result.HasValue) { yield return new WaitForSeconds(0.5f); } + + if (result.Value.Success) + { + var resultId = result.Value.FileId; + Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId); + SteamManager.Workshop.ForceRedownload(resultItem); + while (!resultItem.IsInstalled) + { + currentStepText.Text = $"Downloading {Percentage(resultItem.DownloadAmount)}"; + yield return new WaitForSeconds(0.5f); + } + + bool installed = false; + TaskPool.Add( + "InstallNewlyPublished", + SteamManager.Workshop.WaitForInstall(resultItem), + (t) => + { + installed = true; + }); + while (!installed) + { + currentStepText.Text = $"Installing"; + yield return new WaitForSeconds(0.5f); + } + + var localModProject = new ModProject(localPackage) + { + SteamWorkshopId = resultId + }; + localModProject.Save(localPackage.Path); + ContentPackageManager.ReloadContentPackage(localPackage); + ContentPackageManager.WorkshopPackages.Refresh(); + + if (result.Value.NeedsWorkshopAgreement) + { + SteamManager.OverlayCustomURL(resultItem.Url); + } + } + SteamManager.Workshop.DeletePublishStagingCopy(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs new file mode 100644 index 000000000..e99288885 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/SteamManager.cs @@ -0,0 +1,143 @@ +using Barotrauma.Extensions; +using Barotrauma.IO; +using Barotrauma.Networking; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Color = Microsoft.Xna.Framework.Color; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + private static readonly List initializationErrors = new List(); + public static IReadOnlyList InitializationErrors => initializationErrors; + + private static void InitializeProjectSpecific() + { + if (IsInitialized) { return; } + + try + { + Steamworks.SteamClient.Init(AppID, false); + IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid; + + if (IsInitialized) + { + DebugConsole.NewMessage( + $"Logged in as {GetUsername()} (SteamID {SteamIDUInt64ToString(GetSteamID())})"); + + popularTags.Clear(); + int i = 0; + foreach (KeyValuePair commonness in tagCommonness) + { + popularTags.Insert(i, commonness.Key); + i++; + } + } + + Steamworks.SteamNetworkingUtils.OnDebugOutput += LogSteamworksNetworking; + } + catch (DllNotFoundException) + { + IsInitialized = false; + initializationErrors.Add("SteamDllNotFound".ToIdentifier()); + } + catch (Exception e) + { + DebugConsole.ThrowError("SteamManager initialization threw an exception", e); + IsInitialized = false; + initializationErrors.Add("SteamClientInitFailed".ToIdentifier()); + } + + if (!IsInitialized) + { + try + { + if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } + } + catch (Exception e) + { + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e); + } + } + else + { + //Steamworks is completely insane so the following needs comments: + + //This callback seems to take place when the item in question has not been downloaded recently + Steamworks.SteamUGC.GlobalOnItemInstalled = id => Workshop.OnItemDownloadComplete(id); + + //This callback seems to take place when the item has been downloaded recently and an update + //or a redownload has taken place + Steamworks.SteamUGC.OnDownloadItemResult += (result, id) => Workshop.OnItemDownloadComplete(id); + + //Maybe I'm completely wrong! All I know is that we need to handle both! + } + } + + public static bool NetworkingDebugLog { get; private set; } = false; + + private static void LogSteamworksNetworking(Steamworks.NetDebugOutput nType, string pszMsg) + { + DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); + } + + public static void SetSteamworksNetworkingDebugLog(bool enabled) + { + if (enabled == NetworkingDebugLog) { return; } + if (enabled) + { + Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.Everything; + } + else + { + Steamworks.SteamNetworkingUtils.DebugLevel = Steamworks.NetDebugOutput.None; + } + NetworkingDebugLog = enabled; + } + + public static async Task InitRelayNetworkAccess() + { + if (!IsInitialized) { return; } + + await Task.Yield(); + Steamworks.SteamNetworkingUtils.InitRelayNetworkAccess(); + + //SetSteamworksNetworkingDebugLog(true); + var status = Steamworks.SteamNetworkingUtils.Status; + while (status.Avail != Steamworks.SteamNetworkingAvailability.Current) + { + if (status.Avail == Steamworks.SteamNetworkingAvailability.CannotTry || + status.Avail == Steamworks.SteamNetworkingAvailability.Previously || + status.Avail == Steamworks.SteamNetworkingAvailability.Failed) + { + DebugConsole.ThrowError($"Failed to initialize Steamworks network relay: " + + $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + + $"{Steamworks.SteamNetworkingUtils.Status.AvailNetConfig}, " + + $"{Steamworks.SteamNetworkingUtils.Status.Avail}, " + + $"{Steamworks.SteamNetworkingUtils.Status.Msg}"); + break; + } + await Task.Delay(25); + status = Steamworks.SteamNetworkingUtils.Status; + } + //SetSteamworksNetworkingDebugLog(false); + } + + + public static bool OverlayCustomURL(string url) + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) + { + return false; + } + + Steamworks.SteamFriends.OpenWebOverlay(url); + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs new file mode 100644 index 000000000..75372311c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs @@ -0,0 +1,109 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + private static RectTransform NewItemRectT(GUILayoutGroup parent, float heightScale = 1.0f) + => new RectTransform((1.0f, 0.06f * heightScale), parent.RectTransform, Anchor.CenterLeft); + + private static void Spacer(GUILayoutGroup parent, float height = 0.03f) + { + new GUIFrame(new RectTransform((1.0f, height), parent.RectTransform, Anchor.CenterLeft), style: null); + } + + private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font, float heightScale = 1.0f) + { + return new GUITextBlock(NewItemRectT(parent, heightScale), str, font: font); + } + + private static GUITextBox ScrollableTextBox(GUILayoutGroup parent, float heightScale, string text) + { + var containingListBox = new GUIListBox(NewItemRectT(parent, heightScale)); + var textBox = new GUITextBox( + new RectTransform(Vector2.One, containingListBox.Content.RectTransform), + "", style: "GUITextBoxNoBorder", wrap: true, + textAlignment: Alignment.TopLeft); + textBox.OnTextChanged += (textBox, text) => + { + string wrappedText = textBox.TextBlock.WrappedText.Value; + int measuredHeight = (int)textBox.Font.MeasureString(wrappedText).Y; + textBox.RectTransform.NonScaledSize = + (containingListBox.Content.Rect.Width, + Math.Max(measuredHeight, containingListBox.Content.Rect.Height)); + containingListBox.UpdateScrollBarSize(); + + return true; + }; + textBox.OnEnterPressed += (textBox, text) => + { + string str = textBox.Text; + int cursorPos = textBox.CaretIndex; + textBox.Text = $"{str[..cursorPos]}\n{str[cursorPos..]}"; + textBox.CaretIndex = cursorPos + 1; + + return true; + }; + textBox.Text = text; + return textBox; + } + + private static GUIDropDown DropdownEnum( + GUILayoutGroup parent, Func textFunc, T currentValue, + Action setter) where T : Enum + => Dropdown(parent, textFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); + + private static GUIDropDown Dropdown( + GUILayoutGroup parent, Func textFunc, IReadOnlyList values, T currentValue, + Action setter, float heightScale = 1.0f) + { + var dropdown = new GUIDropDown(NewItemRectT(parent, heightScale)); + SwapDropdownValues(dropdown, textFunc, values, currentValue, setter); + return dropdown; + } + + private static void SwapDropdownValues( + GUIDropDown dropdown, Func textFunc, IReadOnlyList values, T currentValue, + Action setter) + { + if (dropdown.ListBox.Content.Children.Any(c => !(c.UserData is T))) + { + throw new Exception("SwapValues must preserve the type of the dropdown's userdata"); + } + + dropdown.OnSelected = null; + dropdown.ClearChildren(); + + values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v)); + dropdown.Select(values.IndexOf(currentValue)); + dropdown.OnSelected = (dd, obj) => + { + setter((T)obj); + return true; + }; + } + + private static int Round(float v) => (int)MathF.Round(v); + private static string Percentage(float v) => $"{Round(v * 100)}%"; + + private struct ActionCarrier + { + public readonly Identifier Id; + public readonly Action Action; + public ActionCarrier(Identifier id, Action action) + { + Id = id; + Action = action; + } + } + + private GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action) + => new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null) + { UserData = new ActionCarrier(id, action) }; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs new file mode 100644 index 000000000..25d4144aa --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -0,0 +1,285 @@ +#nullable enable +using Microsoft.Xna.Framework.Graphics; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Barotrauma.IO; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + public static partial class Workshop + { + public static readonly ImmutableArray Tags = new [] + { + "submarine", + "item", + "monster", + "art", + "mission", + "event set", + "total conversion", + "environment", + "item assembly", + "language", + }.ToIdentifiers().ToImmutableArray(); + + public class ItemThumbnail : IDisposable + { + private struct RefCounter + { + internal bool Loading; + internal Texture2D? Texture; + internal int Count; + } + private readonly static Dictionary TextureRefs + = new Dictionary(); + + public UInt64 ItemId { get; private set; } + public Texture2D? Texture + { + get + { + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(ItemId, out var refCounter)) + { + return refCounter.Texture; + } + } + return null; + } + } + + public bool Loading + { + get + { + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(ItemId, out var refCounter)) + { + return refCounter.Loading; + } + } + return false; + } + } + + public ItemThumbnail(in Steamworks.Ugc.Item item, CancellationToken cancellationToken) + { + ItemId = item.Id; + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(ItemId, out var refCounter)) + { + TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count + 1, Loading = refCounter.Loading }; + } + else + { + TextureRefs[ItemId] = new RefCounter { Texture = null, Count = 1, Loading = true }; + TaskPool.Add($"Workshop thumbnail {item.Title}", GetTexture(item, cancellationToken), SaveTextureToRefCounter(item.Id)); + } + } + } + + ~ItemThumbnail() + { + Dispose(); + } + + public void Dispose() + { + if (ItemId == 0) { return; } + lock (TextureRefs) + { + var refCounter = TextureRefs[ItemId]; + TextureRefs[ItemId] = new RefCounter { Texture = refCounter.Texture, Count = refCounter.Count - 1 }; + if (TextureRefs[ItemId].Count <= 0) + { + TextureRefs[ItemId].Texture?.Dispose(); + TextureRefs.Remove(ItemId); + } + ItemId = 0; + } + } + + private static async Task GetTexture(Steamworks.Ugc.Item item, CancellationToken cancellationToken) + { + await Task.Yield(); + + string thumbnailUrl = item.PreviewImageUrl; + if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } + var client = new RestClient(thumbnailUrl); + var request = new RestRequest(".", Method.GET); + IRestResponse response = await client.ExecuteTaskAsync(request, cancellationToken); + if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) + { + using var dataStream = new System.IO.MemoryStream(); + await dataStream.WriteAsync(response.RawBytes, cancellationToken); + dataStream.Seek(0, System.IO.SeekOrigin.Begin); + return TextureLoader.FromStream(dataStream, compress: false); + } + return null; + } + + private static Action SaveTextureToRefCounter(UInt64 itemId) + => (t) => + { + if (t.IsCanceled) { return; } + Texture2D? texture = ((Task)t).Result; + lock (TextureRefs) + { + if (TextureRefs.TryGetValue(itemId, out var refCounter)) + { + TextureRefs[itemId] = new RefCounter { Texture = texture, Count = refCounter.Count, Loading = false }; + } + else if (texture != null) + { + texture.Dispose(); + } + } + }; + + public override int GetHashCode() => (int)ItemId; + + public override bool Equals(object? obj) + => obj is ItemThumbnail { ItemId: UInt64 otherId } + && otherId == ItemId; + } + + public const string PublishStagingDir = "WorkshopStaging"; + + public static void DeletePublishStagingCopy() + { + if (Directory.Exists(PublishStagingDir)) { Directory.Delete(PublishStagingDir, recursive: true); } + } + + private static void RefreshLocalMods() + { + CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh()); + } + + public static async Task CreatePublishStagingCopy(string modVersion, ContentPackage contentPackage) + { + await Task.Yield(); + + if (!ContentPackageManager.LocalPackages.Contains(contentPackage)) + { + throw new Exception("Expected local package"); + } + + DeletePublishStagingCopy(); + Directory.CreateDirectory(PublishStagingDir); + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir); + + //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.ModVersion = modVersion; + modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); + } + + public static async Task CreateLocalCopy(ContentPackage contentPackage) + { + await Task.Yield(); + + if (!ContentPackageManager.WorkshopPackages.Contains(contentPackage)) + { + throw new Exception("Expected Workshop package"); + } + + if (contentPackage.SteamWorkshopId == 0) + { + throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}"); + } + + string sanitizedName = ToolBox.RemoveInvalidFileNameChars(contentPackage.Name).Trim(); + if (sanitizedName.IsNullOrWhiteSpace()) + { + throw new Exception($"Sanitized name for {contentPackage.Name} is empty"); + } + + string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; + if (File.Exists(newPath) || Directory.Exists(newPath)) + { + throw new Exception($"{newPath} already exists"); + } + + await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, newPath); + + ModProject modProject = new ModProject(contentPackage); + modProject.DiscardHashAndInstallTime(); + modProject.Save(Path.Combine(newPath, ContentPackage.FileListFileName)); + + RefreshLocalMods(); + + return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId); + } + + private struct InstallWaiter + { + private static readonly HashSet waitingIds = new HashSet(); + public ulong Id { get; private set; } + + public InstallWaiter(ulong id) + { + Id = id; + lock (waitingIds) { waitingIds.Add(Id); } + } + + public bool Waiting + { + get + { + if (Id == 0) { return false; } + + lock (waitingIds) + { + return waitingIds.Contains(Id); + } + } + } + + public static void StopWaiting(ulong id) + { + lock (waitingIds) + { + waitingIds.Remove(id); + } + } + } + + public static async Task WaitForInstall(Steamworks.Ugc.Item item) + => await WaitForInstall(item.Id); + + public static async Task WaitForInstall(ulong item) + { + var installWaiter = new InstallWaiter(item); + while (installWaiter.Waiting) { await Task.Delay(500); } + } + + public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) + { + if (!(Screen.Selected is MainMenuScreen) && !forceInstall) + { + if (!MainMenuScreen.WorkshopItemsToUpdate.Contains(id)) + { + MainMenuScreen.WorkshopItemsToUpdate.Enqueue(id); + } + return; + } + else if (CanBeInstalled(id) + && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id)) + { + TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs new file mode 100644 index 000000000..c921a71aa --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs @@ -0,0 +1,442 @@ +#nullable enable +using System; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Threading; +using System.Xml.Linq; +using Barotrauma.IO; +using Microsoft.Xna.Framework.Graphics; +using ItemOrPackage = Barotrauma.Either; + +namespace Barotrauma.Steam +{ + public partial class WorkshopMenu + { + public enum Tab + { + InstalledMods, + //Overrides, //TODO: implement later + PopularMods, + Publish + } + + private GUILayoutGroup tabber; + private Dictionary tabContents; + + private 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 CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); + private readonly HashSet itemThumbnails = new HashSet(); + + private readonly GUIListBox popularModsList; + private readonly GUIListBox selfModsList; + + public WorkshopMenu(GUIFrame parent) + { + var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); + + tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; + tabContents = new Dictionary(); + + contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); + + CreateInstalledModsTab(out enabledCoreDropdown, out enabledRegularModsList, out disabledRegularModsList, out onInstalledInfoButtonHit); + CreatePopularModsTab(out popularModsList); + CreatePublishTab(out selfModsList); + + SelectTab(Tab.InstalledMods); + } + + private void SwitchContent(GUIFrame newContent) + { + contentFrame.Children.ForEach(c => c.Visible = false); + newContent.Visible = true; + } + + public void SelectTab(Tab tab) + { + SwitchContent(tabContents[tab].Content); + tabber.Children.ForEach(c => + { + if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; } + }); + if (!taskCancelSrc.IsCancellationRequested) { taskCancelSrc.Cancel(); } + itemThumbnails.ForEach(t => t.Dispose()); + itemThumbnails.Clear(); + switch (tab) + { + case Tab.InstalledMods: + PopulateInstalledModLists(); + break; + case Tab.PopularMods: + PopulateItemList(popularModsList, SteamManager.Workshop.GetPopularItems(), includeSubscribeButton: true); + break; + case Tab.Publish: + PopulateItemList(selfModsList, SteamManager.Workshop.GetPublishedItems(), includeSubscribeButton: false, onFill: AddUnpublishedMods); + break; + } + } + + private void AddButtonToTabber(Tab tab, GUIFrame content) + { + var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get($"workshopmenutab.{tab}"), style: "GUITabButton") + { + OnClicked = (b, _) => + { + SelectTab(tab); + return false; + } + }; + button.RectTransform.MaxSize = RectTransform.MaxPoint; + button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); + + tabContents.Add(tab, (button, content)); + } + + private GUIFrame CreateNewContentFrame(Tab tab) + { + var content = new GUIFrame(new RectTransform(Vector2.One * 0.98f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); + AddButtonToTabber(tab, content); + 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 = to.BarScroll * (oldCount / newCount); + } + } + + private void CreateInstalledModsTab( + out GUIDropDown enabledCoreDropdown, + out GUIListBox enabledRegularModsList, + out GUIListBox disabledRegularModsList, + out Action onInstalledInfoButtonHit) + { + 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(); + GUILayoutGroup coreSelectionLayout = + new GUILayoutGroup(new RectTransform((0.5f, 0.15f), mainLayout.RectTransform)); + Label(coreSelectionLayout, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f / 0.15f); + enabledCoreDropdown = Dropdown(coreSelectionLayout, + (p) => p.Name, + ContentPackageManager.CorePackages.ToArray(), + ContentPackageManager.EnabledPackages.Core!, + (p) => { }, + heightScale: 1.0f / 0.15f); + + var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.78f); + right.ChildAnchor = Anchor.TopRight; + + Action swapFunc(GUIListBox from, GUIListBox to) + { + return () => + { + 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); + }; + } + + Action? currentCenterCallback = null; + + //enabled mods + Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.92f), 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.92f), 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) => + { + currentCenterCallback?.Invoke(); + return false; + } + }; + + enabledModsList.OnSelected = (frame, o) => + { + disabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); + + currentCenterCallback = swapFunc(enabledModsList, disabledModsList); + + return true; + }; + disabledModsList.OnSelected = (frame, o) => + { + enabledModsList.Deselect(); + + centerButton.Visible = true; + centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); + + currentCenterCallback = swapFunc(disabledModsList, enabledModsList); + + return true; + }; + + var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); + searchRectT.RelativeSize = (0.5f, searchRectT.RelativeSize.Y); + var searchHolder = new GUIFrame(searchRectT, style: null); + var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), ""); + var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchHolder.RectTransform) {Anchor = Anchor.TopLeft}, + textColor: Color.DarkGray * 0.6f, + text: TextManager.Get("Search") + "...", + textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); }; + + searchBox.OnTextChanged += (sender, str) => + { + enabledModsList.Content.Children.Concat(disabledModsList.Content.Children) + .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() + || (c.UserData is ContentPackage p + && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + return true; + }; + + new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), + onUpdate: (f, component) => + { + HandleDraggingAcrossModLists(enabledModsList, disabledModsList); + HandleDraggingAcrossModLists(disabledModsList, enabledModsList); + }, + onDraw: (spriteBatch, component) => + { + enabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + disabledModsList.DraggedElement?.DrawManually(spriteBatch, true, true); + }); + } + + private void PopulateInstalledModLists() + { + 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 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.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: "GUIDragIndicator") + { + CanBeFocused = false + }; + + var modNameScissor + = new GUIScissorComponent(new RectTransform((0.8f, 1.0f), frameContent.RectTransform)); + var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), text: mod.Name); + 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; + } + }; + } + else if (ContentPackageManager.WorkshopPackages.Contains(mod)) + { + var infoButton = new GUIButton( + new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", + style: "WorkshopMenu.InfoButton") + { + OnClicked = (button, o) => + { + 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); + }); + return false; + } + }; + TaskPool.Add( + $"DetermineUpdateRequired{mod.SteamWorkshopId}", + mod.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + if (!isUpToDate) + { + infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + } + }); + } + } + + enabledRegularModsList.ClearChildren(); + for (int i = 0; i < ContentPackageManager.EnabledPackages.Regular.Count; i++) + { + var mod = ContentPackageManager.EnabledPackages.Regular[i]; + addRegularModToList(mod, enabledRegularModsList); + } + + disabledRegularModsList.ClearChildren(); + foreach (var mod in ContentPackageManager.RegularPackages) + { + if (ContentPackageManager.EnabledPackages.Regular.Contains(mod)) { continue; } + addRegularModToList(mod, disabledRegularModsList); + } + } + + private void CreatePopularModsTab(out GUIListBox popularModsList) + { + GUIFrame content = CreateNewContentFrame(Tab.PopularMods); + + CreateWorkshopItemList(content, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); + } + + private void CreatePublishTab(out GUIListBox selfModsList) + { + GUIFrame content = CreateNewContentFrame(Tab.Publish); + + CreateWorkshopItemOrPackageList(content, out _, out selfModsList, onSelected: PopulatePublishTab); + } + + public void Apply() + { + ContentPackageManager.EnabledPackages.SetCore(enabledCorePackage); + ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children + .Where(c => c.UserData is RegularPackage).Select(c => (RegularPackage)c.UserData).ToArray()); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index c4c40edb9..3b9f2241e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -27,7 +27,7 @@ namespace Barotrauma internal abstract partial class Command { - public abstract string GetDescription(); + public abstract LocalizedString GetDescription(); } /// @@ -88,7 +88,7 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { if (Resized) { @@ -303,7 +303,7 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { if (WasDeleted) { @@ -364,7 +364,7 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { if (wasDropped) { @@ -379,8 +379,8 @@ namespace Barotrauma } return Receivers.Count > 1 - ? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", new[] { "[count]", "[container]" }, new[] { Receivers.Count.ToString(), container }) - : TextManager.GetWithVariables("Undo.ContainedItem", new[] { "[item]", "[container]" }, new[] { Receivers.FirstOrDefault().Item.Name, container }); + ? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", ("[count]", Receivers.Count.ToString()), ("[container]", container)) + : TextManager.GetWithVariables("Undo.ContainedItem", ("[item]", Receivers.FirstOrDefault().Item.Name), ("[container]", container)); } } @@ -391,7 +391,7 @@ namespace Barotrauma { private Dictionary> OldProperties; private readonly List Receivers; - private readonly string PropertyName; + private readonly Identifier PropertyName; private readonly object NewProperties; private string sanitizedProperty; @@ -404,7 +404,7 @@ namespace Barotrauma /// Real property name, not all lowercase /// /// - public PropertyCommand(List receivers, string propertyName, object newData, Dictionary> oldData) + public PropertyCommand(List receivers, Identifier propertyName, object newData, Dictionary> oldData) { Receivers = receivers; PropertyName = propertyName; @@ -414,7 +414,7 @@ namespace Barotrauma SanitizeProperty(); } - public PropertyCommand(ISerializableEntity receiver, string propertyName, object newData, object oldData) + public PropertyCommand(ISerializableEntity receiver, Identifier propertyName, object newData, object oldData) { Receivers = new List { receiver }; PropertyName = propertyName; @@ -485,9 +485,9 @@ namespace Barotrauma if (receiver.SerializableProperties != null) { - Dictionary props = receiver.SerializableProperties; + Dictionary props = receiver.SerializableProperties; - if (props.TryGetValue(PropertyName.ToLowerInvariant(), out SerializableProperty prop)) + if (props.TryGetValue(PropertyName, out SerializableProperty prop)) { prop.TrySetValue(receiver, data); // Update the editing hud @@ -512,11 +512,17 @@ namespace Barotrauma } } - public override string GetDescription() + public override LocalizedString GetDescription() { return Receivers.Count > 1 - ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple", new[] { "[property]", "[count]", "[value]" }, new[] { PropertyName, Receivers.Count.ToString(), sanitizedProperty }) - : TextManager.GetWithVariables("Undo.ChangedProperty", new[] { "[property]", "[item]", "[value]" }, new[] { PropertyName, Receivers.FirstOrDefault()?.Name, sanitizedProperty }); + ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple", + ("[property]", PropertyName.Value), + ("[count]", Receivers.Count.ToString()), + ("[value]", sanitizedProperty)) + : TextManager.GetWithVariables("Undo.ChangedProperty", + ("[property]", PropertyName.Value), + ("[item]", Receivers.FirstOrDefault()?.Name), + ("[value]", sanitizedProperty)); } } @@ -560,7 +566,7 @@ namespace Barotrauma public override void Cleanup() { } - public override string GetDescription() + public override LocalizedString GetDescription() { return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs new file mode 100644 index 000000000..8e652c5cd --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/LimitLString.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace Barotrauma +{ + public class LimitLString : LocalizedString + { + private readonly LocalizedString nestedStr; + private readonly GUIFont font; + private readonly int maxWidth; + + private ScalableFont? cachedFont = null; + private uint cachedFontSize = 0; + + public LimitLString(LocalizedString text, GUIFont font, int maxWidth) + { + this.nestedStr = text; + this.font = font; + this.maxWidth = maxWidth; + } + + public override bool Loaded => nestedStr.Loaded; + protected override bool MustRetrieveValue() + { + return base.MustRetrieveValue() || cachedFont != font.Value || cachedFont.Size != font.Size; + } + + public override void RetrieveValue() + { + cachedValue = ToolBox.LimitString(nestedStr.Value, font.Value, maxWidth); + cachedFont = font.Value; + cachedFontSize = font.Size; + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs new file mode 100644 index 000000000..f13fdd117 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Text/LocalizedString/WrappedLString.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace Barotrauma +{ + public class WrappedLString : LocalizedString + { + private readonly LocalizedString nestedStr; + private readonly float lineLength; + private readonly GUIFont font; + private readonly float textScale; + + public WrappedLString(LocalizedString text, float lineLength, GUIFont font, float textScale = 1.0f) + { + this.nestedStr = text; + this.lineLength = lineLength; + this.font = font; + this.textScale = textScale; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.Value, textScale); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs index deed55665..c427adf4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs @@ -6,35 +6,16 @@ using System.Xml.Linq; namespace Barotrauma { - class TraitorMissionPrefab + class TraitorMissionPrefab : Prefab { - public static readonly List List = new List(); - - public readonly string Identifier; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public readonly Sprite Icon; public readonly Color IconColor; - public static void Init() + public TraitorMissionPrefab(ContentXElement element, TraitorMissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { - List.Clear(); - var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) { continue; } - - foreach (XElement element in doc.Root.Elements()) - { - List.Add(new TraitorMissionPrefab(element)); - } - } - } - - private TraitorMissionPrefab(XElement element) - { - Identifier = element.GetAttributeString("identifier", ""); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { @@ -43,5 +24,10 @@ namespace Barotrauma } } } + + public override void Dispose() + { + Icon?.Remove(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs index 8e2c5e73d..eaea08977 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionResult.cs @@ -7,7 +7,7 @@ namespace Barotrauma { public TraitorMissionResult(IReadMessage inc) { - MissionIdentifier = inc.ReadString(); + MissionIdentifier = inc.ReadIdentifier(); EndMessage = inc.ReadString(); Success = inc.ReadBoolean(); byte characterCount = inc.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs index 7f2243b45..7443204bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Upgrades/UpgradePrefab.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { partial class UpgradePrefab { - public readonly List DecorativeSprites = new List(); + public readonly ImmutableArray DecorativeSprites = new ImmutableArray(); public Sprite Sprite { get; private set; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs index f6c015be6..3f0b622bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs @@ -36,26 +36,15 @@ using System.Configuration; using System.Globalization; using Barotrauma.IO; using System.Text; -#if NET_4_0 -using System.Web.Configuration; -#endif namespace RestSharp.Contrib { -#if NET_4_0 - public -#endif class HttpEncoder { static char[] hexChars = "0123456789abcdef".ToCharArray(); static object entitiesLock = new object(); static SortedDictionary entities; -#if NET_4_0 - static Lazy defaultEncoder; - static Lazy currentEncoderLazy; -#else static HttpEncoder defaultEncoder; -#endif static HttpEncoder currentEncoder; static IDictionary Entities @@ -76,53 +65,29 @@ namespace RestSharp.Contrib { get { -#if NET_4_0 - if (currentEncoder == null) - currentEncoder = currentEncoderLazy.Value; -#endif return currentEncoder; } -#if NET_4_0 - set { - if (value == null) - throw new ArgumentNullException ("value"); - currentEncoder = value; - } -#endif } public static HttpEncoder Default { get { -#if NET_4_0 - return defaultEncoder.Value; -#else return defaultEncoder; -#endif } } static HttpEncoder() { -#if NET_4_0 - defaultEncoder = new Lazy (() => new HttpEncoder ()); - currentEncoderLazy = new Lazy (new Func (GetCustomEncoderFromConfig)); -#else defaultEncoder = new HttpEncoder(); currentEncoder = defaultEncoder; -#endif } public HttpEncoder() { } -#if NET_4_0 - protected internal virtual -#else - internal static -#endif - void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) + + internal static void HeaderNameValueEncode(string headerName, string headerValue, out string encodedHeaderName, out string encodedHeaderValue) { if (String.IsNullOrEmpty(headerName)) encodedHeaderName = headerName; @@ -161,66 +126,8 @@ namespace RestSharp.Contrib return input; } -#if NET_4_0 - protected internal virtual void HtmlAttributeEncode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - if (String.IsNullOrEmpty (value)) - return; - - output.Write (HtmlAttributeEncode (value)); - } - - protected internal virtual void HtmlDecode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlDecode (value)); - } - - protected internal virtual void HtmlEncode (string value, TextWriter output) - { - if (output == null) - throw new ArgumentNullException ("output"); - - output.Write (HtmlEncode (value)); - } - - protected internal virtual byte[] UrlEncode (byte[] bytes, int offset, int count) - { - return UrlEncodeToBytes (bytes, offset, count); - } - - static HttpEncoder GetCustomEncoderFromConfig () - { - var cfg = WebConfigurationManager.GetSection ("system.web/httpRuntime") as HttpRuntimeSection; - string typeName = cfg.EncoderType; - - if (String.Compare (typeName, "System.Web.Util.HttpEncoder", StringComparison.OrdinalIgnoreCase) == 0) - return Default; - - Type t = Type.GetType (typeName, false); - if (t == null) - throw new ConfigurationErrorsException (String.Format ("Could not load type '{0}'.", typeName)); - - if (!typeof (HttpEncoder).IsAssignableFrom (t)) - throw new ConfigurationErrorsException ( - String.Format ("'{0}' is not allowed here because it does not extend class 'System.Web.Util.HttpEncoder'.", typeName) - ); - - return Activator.CreateInstance (t, false) as HttpEncoder; - } -#endif -#if NET_4_0 - protected internal virtual -#else - internal static -#endif - string UrlPathEncode(string value) + internal static string UrlPathEncode(string value) { if (String.IsNullOrEmpty(value)) return value; @@ -240,7 +147,7 @@ namespace RestSharp.Contrib int blen = bytes.Length; if (blen == 0) - return new byte[0]; + return Array.Empty(); if (offset < 0 || offset >= blen) throw new ArgumentOutOfRangeException("offset"); @@ -268,11 +175,7 @@ namespace RestSharp.Contrib for (int i = 0; i < s.Length; i++) { char c = s[i]; - if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159 -#if NET_4_0 - || c == '\'' -#endif -) + if (c == '&' || c == '"' || c == '<' || c == '>' || c > 159) { needEncode = true; break; @@ -302,11 +205,6 @@ namespace RestSharp.Contrib case '"': output.Append("""); break; -#if NET_4_0 - case '\'': - output.Append ("'"); - break; -#endif case '\uff1c': output.Append("<"); break; @@ -334,25 +232,17 @@ namespace RestSharp.Contrib internal static string HtmlAttributeEncode(string s) { -#if NET_4_0 - if (String.IsNullOrEmpty (s)) - return String.Empty; -#else if (s == null) return null; if (s.Length == 0) return String.Empty; -#endif + bool needEncode = false; for (int i = 0; i < s.Length; i++) { char c = s[i]; - if (c == '&' || c == '"' || c == '<' -#if NET_4_0 - || c == '\'' -#endif -) + if (c == '&' || c == '"' || c == '<') { needEncode = true; break; @@ -376,11 +266,6 @@ namespace RestSharp.Contrib case '<': output.Append("<"); break; -#if NET_4_0 - case '\'': - output.Append ("'"); - break; -#endif default: output.Append(s[i]); break; @@ -399,9 +284,7 @@ namespace RestSharp.Contrib if (s.IndexOf('&') == -1) return s; -#if NET_4_0 - StringBuilder rawEntity = new StringBuilder (); -#endif + StringBuilder entity = new StringBuilder(); StringBuilder output = new StringBuilder(); int len = s.Length; @@ -422,9 +305,6 @@ namespace RestSharp.Contrib if (c == '&') { entity.Append(c); -#if NET_4_0 - rawEntity.Append (c); -#endif state = 1; } else @@ -471,9 +351,6 @@ namespace RestSharp.Contrib state = 3; } entity.Append(c); -#if NET_4_0 - rawEntity.Append (c); -#endif } } else if (state == 2) @@ -488,20 +365,12 @@ namespace RestSharp.Contrib output.Append(key); state = 0; entity.Length = 0; -#if NET_4_0 - rawEntity.Length = 0; -#endif } } else if (state == 3) { if (c == ';') { -#if NET_4_0 - if (number == 0) - output.Append (rawEntity.ToString () + ";"); - else -#endif if (number > 65535) { output.Append("&#"); @@ -514,33 +383,21 @@ namespace RestSharp.Contrib } state = 0; entity.Length = 0; -#if NET_4_0 - rawEntity.Length = 0; -#endif have_trailing_digits = false; } else if (is_hex_value && Uri.IsHexDigit(c)) { number = number * 16 + Uri.FromHex(c); have_trailing_digits = true; -#if NET_4_0 - rawEntity.Append (c); -#endif } else if (Char.IsDigit(c)) { number = number * 10 + ((int)c - '0'); have_trailing_digits = true; -#if NET_4_0 - rawEntity.Append (c); -#endif } else if (number == 0 && (c == 'x' || c == 'X')) { is_hex_value = true; -#if NET_4_0 - rawEntity.Append (c); -#endif } else { @@ -568,11 +425,7 @@ namespace RestSharp.Contrib internal static bool NotEncoded(char c) { - return (c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_' -#if !NET_4_0 - || c == '\'' -#endif -); + return (c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_'); } internal static void UrlEncodeChar(char c, System.IO.Stream result, bool isUnicode) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index 34aa5e0ca..b4662d858 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -26,7 +26,7 @@ namespace Barotrauma public static void Convert() { - if (TextManager.Language != "English") + if (GameSettings.CurrentConfig.Language != TextManager.DefaultLanguage) { DebugConsole.ThrowError("Use the english localization when converting .csv to allow copying values"); return; @@ -123,8 +123,10 @@ namespace Barotrauma private static List ConvertInfoTextToXML(string[] csvContent, string language) { - List xmlContent = new List(); - xmlContent.Add(xmlHeader); + List xmlContent = new List + { + xmlHeader + }; string translatedName = GetTranslatedName(language); bool nowhitespace = TextManager.IsCJK(translatedName); @@ -151,6 +153,7 @@ namespace Barotrauma split[1] = split[2]; split[2] = string.Empty; } + split[1] = split[1].Replace(" & ", " & "); xmlContent.Add($"<{split[0]}>{split[1]}"); } else if (split[0].Contains(".") && !split[0].Any(char.IsUpper)) // An empty field @@ -220,15 +223,16 @@ namespace Barotrauma } //DebugConsole.NewMessage("Count: " + NPCPersonalityTrait.List.Count); - for (int i = 0; i < NPCPersonalityTrait.List.Count; i++) // Traits + var traits = NPCPersonalityTrait.GetAll(language.ToLanguageIdentifier()).ToArray(); + for (int i = 0; i < traits.Length; i++) // Traits { //string[] split = SplitCSV(csvContent[traitStart + i].Trim(separator)); string[] split = csvContent[traitStart + i].Split(separator); xmlContent.Add( $""); + $"{GetVariable("alloweddialogtags", string.Join(",", traits[i].AllowedDialogTags))}" + + $"{GetVariable("commonness", traits[i].Commonness.ToString(CultureInfo.InvariantCulture))}/>"); } xmlContent.Add(string.Empty); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 6327b9e26..2d4a1ffb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -53,7 +53,7 @@ namespace Barotrauma private static XElement ParseRecipe(ItemPrefab prefab) { - FabricationRecipe? recipe = prefab.FabricationRecipes.FirstOrDefault(); + FabricationRecipe? recipe = prefab.FabricationRecipes.Values.FirstOrDefault(); List ingredients = recipe?.RequiredItems.SelectMany(ri => ri.ItemPrefabs).Distinct().ToList() ?? new List(); Skill? skill = recipe?.RequiredSkills.FirstOrDefault(); @@ -61,7 +61,7 @@ namespace Barotrauma return new XElement("Recipe", new XAttribute("amount", recipe?.Amount ?? 0), new XAttribute("time", recipe?.RequiredTime ?? 0), - new XAttribute("skillname", skill?.Identifier ?? ""), + new XAttribute("skillname", skill?.Identifier.Value ?? ""), new XAttribute("skillamount", (int?) skill?.Level ?? 0), new XAttribute("ingredients", FormatArray(ingredients.Select(ip => ip.Name))), new XAttribute("values", FormatArray(ingredients.Select(ip => ip.DefaultPrice?.Price ?? 0))) @@ -80,15 +80,15 @@ namespace Barotrauma private static XElement ParseMedical(ItemPrefab prefab) { - XElement? itemMeleeWeapon = prefab.ConfigElement.GetChildElement(nameof(MeleeWeapon)); + ContentXElement? itemMeleeWeapon = prefab.ConfigElement.GetChildElement(nameof(MeleeWeapon)); // affliction, amount, duration - List> onSuccessAfflictions = new List>(); - List> onFailureAfflictions = new List>(); + List<(LocalizedString Name, float Amount, float Duration)> onSuccessAfflictions = new List<(LocalizedString Name, float Amount, float Duration)>(); + List<(LocalizedString Name, float Amount, float Duration)> onFailureAfflictions = new List<(LocalizedString Name, float Amount, float Duration)>(); int medicalRequiredSkill = 0; if (itemMeleeWeapon != null) { List statusEffects = new List(); - foreach (XElement subElement in itemMeleeWeapon.Elements()) + foreach (var subElement in itemMeleeWeapon.Elements()) { string name = subElement.Name.ToString(); if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) @@ -110,15 +110,15 @@ namespace Barotrauma foreach (StatusEffect statusEffect in successEffects) { float duration = statusEffect.Duration; - onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); - onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(ra => (GetAfflictionName(ra.AfflictionIdentifier), -ra.ReduceAmount, duration))); + onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => (affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } foreach (StatusEffect statusEffect in failureEffects) { float duration = statusEffect.Duration; - onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); - onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(ra => (GetAfflictionName(ra.AfflictionIdentifier), -ra.ReduceAmount, duration))); + onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => (affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } } @@ -141,15 +141,15 @@ namespace Barotrauma int skillRequirement = 0; // affliction, amount - List> damages = new List>(); + List<(LocalizedString Name, float Amount)> damages = new List<(LocalizedString Name, float Amount)>(); string[] validNames = { nameof(Projectile), nameof(MeleeWeapon), nameof(RepairTool), nameof(ItemComponent), nameof(RangedWeapon) }; - foreach (XElement icElement in prefab.ConfigElement.Elements()) + foreach (var icElement in prefab.ConfigElement.Elements()) { string icName = icElement.Name.ToString(); if (!validNames.Any(name => icName.Equals(name, StringComparison.OrdinalIgnoreCase))) { continue; } - foreach (XElement icChildElement in icElement.Elements()) + foreach (var icChildElement in icElement.Elements()) { string name = icChildElement.Name.ToString(); if (IsRequiredSkill(icChildElement, out Skill? skill) && skill != null) @@ -208,7 +208,7 @@ namespace Barotrauma continue; } - damages.Add(Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength)); + damages.Add((affliction.Prefab.Name, affliction.NonClampedStrength)); } } } @@ -224,9 +224,9 @@ namespace Barotrauma ); } - private static string GetAfflictionName(string identifier) + private static LocalizedString GetAfflictionName(Identifier identifier) { - return AfflictionPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))?.Name ?? CultureInfo.CurrentCulture.TextInfo.ToTitleCase(identifier.ToLower()); + return AfflictionPrefab.Prefabs.Find(prefab => prefab.Identifier == identifier)?.Name ?? CultureInfo.CurrentCulture.TextInfo.ToTitleCase(identifier.Value!.ToLower()); } private static string FormatFloat(float value) @@ -239,7 +239,7 @@ namespace Barotrauma return string.Join(separator, array); } - private static bool IsRequiredSkill(XElement element, out Skill? skill) + private static bool IsRequiredSkill(ContentXElement element, out Skill? skill) { string name = element.Name.ToString(); bool isSkill = name.Equals("RequiredSkill", StringComparison.OrdinalIgnoreCase) || @@ -247,7 +247,7 @@ namespace Barotrauma if (isSkill) { - string identifier = element.GetAttributeString(nameof(Skill.Identifier).ToLowerInvariant(), string.Empty); + Identifier identifier = element.GetAttributeIdentifier(nameof(Skill.Identifier), Identifier.Empty); float level = element.GetAttributeFloat(nameof(Skill.Level).ToLowerInvariant(), 0f); skill = new Skill(identifier, level); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs index ed5a63cac..7990dee16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpriteRecorder.cs @@ -283,7 +283,7 @@ namespace Barotrauma indexBuffer?.Dispose(); indexBuffer = new IndexBuffer(gfxDevice, IndexElementSize.SixteenBits, requiredIndexCount * 2, BufferUsage.WriteOnly); ushort[] indices = new ushort[requiredIndexCount * 2]; - for (int i=0;i LimitString((LocalizedString)str, font, maxWidth); + public static string LimitString(string str, ScalableFont font, int maxWidth) { if (maxWidth <= 0 || string.IsNullOrWhiteSpace(str)) return ""; @@ -434,6 +443,11 @@ namespace Barotrauma return Color.Lerp(gradient[(int)scaledT], gradient[(int)Math.Min(scaledT + 1, gradient.Length - 1)], (scaledT - (int)scaledT)); } + public static LocalizedString WrapText(LocalizedString text, float lineLength, GUIFont font, float textScale = 1.0f) + { + return new WrappedLString(text, lineLength, font, textScale); + } + public static string WrapText(string text, float lineLength, ScalableFont font, float textScale = 1.0f) => font.WrapText(text, lineLength / textScale); @@ -464,5 +478,15 @@ namespace Barotrauma if (b.Build < a.Build) { return false; } return false; } + + public static void OpenFileWithShell(string filename) + { + ProcessStartInfo startInfo = new ProcessStartInfo() + { + FileName = filename, + UseShellExecute = true + }; + Process.Start(startInfo); + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 9948738a1..31debdb9c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,24 +6,36 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 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 @@ -95,7 +107,7 @@ Icon.bmp - + @@ -140,7 +152,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 4d84c5305..6f1f04661 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,13 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico - 0.9.703.0 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 @@ -62,6 +62,12 @@ + + SharedSource\Prefabs\PrefabSelector.cs + + + SharedSource\Prefabs\PrefabCollectionSubset.cs + diff --git a/Barotrauma/BarotraumaClient/Properties/launchSettings.json b/Barotrauma/BarotraumaClient/Properties/launchSettings.json index cc9cae3a3..45cb9cb6b 100644 --- a/Barotrauma/BarotraumaClient/Properties/launchSettings.json +++ b/Barotrauma/BarotraumaClient/Properties/launchSettings.json @@ -3,6 +3,14 @@ "WindowsClient": { "commandName": "Project", "nativeDebugging": false + }, + "MacClient": { + "commandName": "Project", + "nativeDebugging": false + }, + "LinuxClient": { + "commandName": "Project", + "nativeDebugging": false } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index afa31a7c3..ca8ab114b 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,13 +6,14 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 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 @@ -65,6 +66,12 @@ + + SharedSource\Steam\AuthTicket.cs + + + SharedSource\Utils\Result.cs + @@ -120,6 +127,9 @@ + + SharedSource\Utils\Result + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c60226b92..996817458 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,24 +6,36 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 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 diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 42d0facc0..296dc0707 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,13 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico - 0.9.0.0 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 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 35047f575..4a9e5f918 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -7,8 +7,6 @@ namespace Barotrauma { public static Character Controlled = null; - partial void InitProjSpecific(XElement mainElement) { } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, stun, attackResult.Afflictions); @@ -20,7 +18,7 @@ namespace Barotrauma { if (causeOfDeath == CauseOfDeathType.Affliction) { - GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); + GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name.Value + ")", ServerLog.MessageType.Attack); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 91e38cf04..c3d14550f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -8,9 +8,9 @@ namespace Barotrauma { partial class CharacterInfo { - private readonly Dictionary prevSentSkill = new Dictionary(); + private readonly Dictionary prevSentSkill = new Dictionary(); - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel) + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel) { if (Character == null || Character.Removed) { return; } if (!prevSentSkill.ContainsKey(skillIdentifier)) @@ -45,16 +45,18 @@ namespace Barotrauma msg.Write(ID); msg.Write(Name); msg.Write(OriginalName); - msg.Write((byte)Gender); - msg.Write((byte)Race); - msg.Write((byte)HeadSpriteId); - msg.Write((byte)HairIndex); - msg.Write((byte)BeardIndex); - msg.Write((byte)MoustacheIndex); - msg.Write((byte)FaceAttachmentIndex); - msg.WriteColorR8G8B8(SkinColor); - msg.WriteColorR8G8B8(HairColor); - msg.WriteColorR8G8B8(FacialHairColor); + msg.Write((byte)Head.Preset.TagSet.Count); + foreach (Identifier tag in Head.Preset.TagSet) + { + msg.Write(tag); + } + msg.Write((byte)Head.HairIndex); + msg.Write((byte)Head.BeardIndex); + msg.Write((byte)Head.MoustacheIndex); + msg.Write((byte)Head.FaceAttachmentIndex); + msg.WriteColorR8G8B8(Head.SkinColor); + msg.WriteColorR8G8B8(Head.HairColor); + msg.WriteColorR8G8B8(Head.FacialHairColor); msg.Write(ragdollFileName); if (Job != null) @@ -73,20 +75,9 @@ namespace Barotrauma msg.Write(""); msg.Write((byte)0); } - // TODO: animations - msg.Write((byte)SavedStatValues.SelectMany(s => s.Value).Count()); - foreach (var savedStatValuePair in SavedStatValues) - { - foreach (var savedStatValue in savedStatValuePair.Value) - { - msg.Write((byte)savedStatValuePair.Key); - msg.Write(savedStatValue.StatIdentifier); - msg.Write(savedStatValue.StatValue); - msg.Write(savedStatValue.RemoveOnDeath); - } - } + msg.Write((ushort)ExperiencePoints); - msg.Write((ushort)AdditionalTalentPoints); + msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7eb20a6f5..a7c694e5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -283,12 +283,12 @@ namespace Barotrauma // get the full list of talents from the player, only give the ones // that are not already given (or otherwise not viable) ushort talentCount = msg.ReadUInt16(); - List talentSelection = new List(); + List talentSelection = new List(); for (int i = 0; i < talentCount; i++) { UInt32 talentIdentifier = msg.ReadUInt32(); - var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UIntIdentifier == talentIdentifier); - if (prefab == null) { continue; } + var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier); + if (prefab == null) { continue; } if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) { @@ -381,28 +381,27 @@ namespace Barotrauma if (type == 1) { var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); - bool validOrder = currentOrderInfo.HasValue; + bool validOrder = currentOrderInfo != null; msg.Write(validOrder); if (!validOrder) { break; } - var orderPrefab = currentOrderInfo.Value.Order.Prefab; - int orderIndex = Order.PrefabList.IndexOf(orderPrefab); - msg.WriteRangedInteger(orderIndex, 0, Order.PrefabList.Count); + var orderPrefab = currentOrderInfo.Prefab; + msg.Write(orderPrefab.UintIdentifier); if (!orderPrefab.HasOptions) { break; } - int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Value.OrderOption); + int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); if (optionIndex == -1) { - DebugConsole.AddWarning($"Error while writing order data. Order option \"{(currentOrderInfo.Value.OrderOption ?? null)}\" not found in the order prefab \"{orderPrefab.Name}\"."); + DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\"."); } msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); } else if (type == 2) { var objective = controller.ObjectiveManager.CurrentObjective; - bool validObjective = !string.IsNullOrEmpty(objective?.Identifier); + bool validObjective = objective != null && objective.Identifier != Identifier.Empty; msg.Write(validObjective); if (!validObjective) { break; } msg.Write(objective.Identifier); - msg.Write(objective.Option ?? ""); + msg.Write(objective.Option); UInt16 targetEntityId = 0; if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) { @@ -435,7 +434,7 @@ namespace Barotrauma foreach (var unlockedTalent in characterTalents) { msg.Write(unlockedTalent.AddedThisRound); - msg.Write(unlockedTalent.Prefab.UIntIdentifier); + msg.Write(unlockedTalent.Prefab.UintIdentifier); } break; case NetEntityEvent.Type.UpdateMoney: @@ -623,9 +622,9 @@ namespace Barotrauma public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMessageSize) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } - int msgLength = msg.LengthBytes; + int initialMsgLength = msg.LengthBytes; msg.Write(Info == null); msg.Write(entityId); @@ -671,43 +670,60 @@ namespace Barotrauma msg.Write((byte)TeamID); msg.Write(this is AICharacter); msg.Write(info.SpeciesName); + int msgLengthBeforeInfo = msg.LengthBytes; info.ServerWrite(msg); + int infoLength = msg.LengthBytes - msgLengthBeforeInfo; msg.Write((byte)CampaignInteractionType); - + int msgLengthBeforeOrders = msg.LengthBytes; // Current orders - msg.Write((byte)info.CurrentOrders.Count(o => o.Order != null)); + msg.Write((byte)info.CurrentOrders.Count(o => o != null)); foreach (var orderInfo in info.CurrentOrders) { - if (orderInfo.Order == null) { continue; } - msg.Write((byte)Order.PrefabList.IndexOf(orderInfo.Order.Prefab)); - msg.Write(orderInfo.Order.TargetEntity == null ? (UInt16)0 : orderInfo.Order.TargetEntity.ID); - var hasOrderGiver = orderInfo.Order.OrderGiver != null; + if (orderInfo == null) { continue; } + msg.Write(orderInfo.Prefab.UintIdentifier); + msg.Write(orderInfo.TargetEntity == null ? (UInt16)0 : orderInfo.TargetEntity.ID); + var hasOrderGiver = orderInfo.OrderGiver != null; msg.Write(hasOrderGiver); - if (hasOrderGiver) { msg.Write(orderInfo.Order.OrderGiver.ID); } - msg.Write((byte)(string.IsNullOrWhiteSpace(orderInfo.OrderOption) ? 0 : Array.IndexOf(orderInfo.Order.Prefab.Options, orderInfo.OrderOption))); + if (hasOrderGiver) { msg.Write(orderInfo.OrderGiver.ID); } + msg.Write((byte)(orderInfo.Option == Identifier.Empty ? 0 : orderInfo.Prefab.Options.IndexOf(orderInfo.Option))); msg.Write((byte)orderInfo.ManualPriority); - var hasTargetPosition = orderInfo.Order.TargetPosition != null; + var hasTargetPosition = orderInfo.TargetPosition != null; msg.Write(hasTargetPosition); if (hasTargetPosition) { - msg.Write(orderInfo.Order.TargetPosition.Position.X); - msg.Write(orderInfo.Order.TargetPosition.Position.Y); - msg.Write(orderInfo.Order.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.Order.TargetPosition.Hull.ID); + msg.Write(orderInfo.TargetPosition.Position.X); + msg.Write(orderInfo.TargetPosition.Position.Y); + msg.Write(orderInfo.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.TargetPosition.Hull.ID); } } + int ordersLength = msg.LengthBytes - msgLengthBeforeOrders; + + if (msg.LengthBytes - initialMsgLength >= 255 && restrictMessageSize) + { + string errorMsg = $"Error when writing character spawn data: data exceeded 255 bytes (info: {infoLength}, orders: {ordersLength}, total: {msg.LengthBytes - initialMsgLength})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchData", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } TryWriteStatus(msg); void TryWriteStatus(IWriteMessage msg) { + int msgLengthBeforeStatus = msg.LengthBytes - initialMsgLength; + var tempBuffer = new ReadWriteMessage(); WriteStatus(tempBuffer); - if (msg.LengthBytes + tempBuffer.LengthBytes >= 255 && restrictMessageSize) + if (msgLengthBeforeStatus + tempBuffer.LengthBytes >= 255 && restrictMessageSize) { msg.Write(false); - DebugConsole.ThrowError($"Error when writing character spawn data: status data caused the length of the message to exceed 255 bytes ({msg.LengthBytes} + {tempBuffer.LengthBytes})"); + if (msgLengthBeforeStatus < 255) + { + string errorMsg = $"Error when writing character spawn data: status data caused the length of the message to exceed 255 bytes ({msgLengthBeforeStatus} + {tempBuffer.LengthBytes})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.WriteSpawnData:TooMuchDataForStatus", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } } else { @@ -716,7 +732,7 @@ namespace Barotrauma } } - DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - msgLength)); + DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - initialMsgLength)); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 1ff909090..c3ad6d244 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -98,7 +98,7 @@ namespace Barotrauma { ColoredText msg = queuedMessages.Dequeue(); Messages.Add(msg); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(msg); if (unsavedMessages.Count >= messagesPerFile) @@ -281,7 +281,7 @@ namespace Barotrauma { var msg = queuedMessages.Dequeue(); Messages.Add(msg); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging) { unsavedMessages.Add(msg); if (unsavedMessages.Count >= messagesPerFile) @@ -392,7 +392,7 @@ namespace Barotrauma if (float.TryParse(args[0], out seconds)) { seconds = Math.Max(0, seconds); - GameMain.Server.SendConsoleMessage("Set kill disconnected timer to " + ToolBox.SecondsToReadableTime(seconds), client); + GameMain.Server.SendConsoleMessage("Set kill disconnected timer to " + ToolBox.SecondsToReadableTime(seconds).Value, client); NewMessage(client.Name + " set kill disconnected timer to " + ToolBox.SecondsToReadableTime(seconds), Color.White); } else @@ -955,7 +955,7 @@ namespace Barotrauma return; } client.Muted = true; - GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer"), client, ChatMessageType.MessageBox); + GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox); }, () => { @@ -975,7 +975,7 @@ namespace Barotrauma return; } client.Muted = false; - GameMain.Server.SendDirectChatMessage(TextManager.Get("UnmutedByServer"), client, ChatMessageType.MessageBox); + GameMain.Server.SendDirectChatMessage(TextManager.Get("UnmutedByServer").Value, client, ChatMessageType.MessageBox); }, () => { @@ -1102,7 +1102,7 @@ namespace Barotrauma TraitorManager traitorManager = GameMain.Server.TraitorManager; if (traitorManager == null || traitorManager.Traitors == null || !traitorManager.Traitors.Any()) { - GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", "", TraitorMessageType.Console); + GameMain.Server.SendTraitorMessage(client, "There are no traitors at the moment.", Identifier.Empty, TraitorMessageType.Console); return; } foreach (Traitor t in traitorManager.Traitors) @@ -1116,11 +1116,11 @@ namespace Barotrauma $"[traitorgoals]={traitorGoals.Substring(traitorGoalsStart)}", $"[traitorname]={t.Character.Name}", "Traitor [traitorname]'s current goals are:\n[traitorgoals]" - }.Where(s => !string.IsNullOrEmpty(s))), t.Mission?.Identifier, TraitorMessageType.Console); + }.Where(s => !string.IsNullOrEmpty(s))), t.Mission.Identifier, TraitorMessageType.Console); } else { - GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", "", t.Character.Name), "", TraitorMessageType.Console); + GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", "", t.Character.Name), Identifier.Empty, TraitorMessageType.Console); } } //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); @@ -1296,7 +1296,7 @@ namespace Barotrauma { return new string[][] { - GameModePreset.List.Select(gm => gm.Name).ToArray() + GameModePreset.List.Select(gm => gm.Name.Value).ToArray() }; })); @@ -1668,7 +1668,7 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + a.Identifier == args[0]); if (afflictionPrefab == null) { GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client, Color.Red); @@ -1756,7 +1756,7 @@ namespace Barotrauma if (targetCharacter == null) { return; } TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => - c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + c.Identifier == args[0] || c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { @@ -1789,7 +1789,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage($"Failed to find the job \"{args[0]}\".", client, Color.Red); return; } - if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + if (!TalentTree.JobTalentTrees.TryGet(job.Identifier, out TalentTree talentTree)) { GameMain.Server.SendConsoleMessage($"No talents configured for the job \"{args[0]}\".", client, Color.Red); return; @@ -2010,7 +2010,7 @@ namespace Barotrauma client.SetPermissions(preset.Permissions, preset.PermittedCommands); GameMain.Server.UpdateClientPermissions(client); - GameMain.Server.SendConsoleMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", senderClient); + GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient); NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); } ); @@ -2158,7 +2158,7 @@ namespace Barotrauma foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { if (permission == ClientPermissions.None || !client.HasPermission(permission)) { continue; } - GameMain.Server.SendConsoleMessage(" - " + TextManager.Get("ClientPermission." + permission), senderClient); + GameMain.Server.SendConsoleMessage($" - {TextManager.Get("ClientPermission." + permission)}", senderClient); } if (client.HasPermission(ClientPermissions.ConsoleCommands)) { @@ -2257,7 +2257,7 @@ namespace Barotrauma var tagList = MapEntityPrefab.List.SelectMany(p => p.Tags.Select(t => t)).Distinct(); foreach (var tag in tagList) { - NewMessage(tag, Color.Yellow); + NewMessage(tag.Value, Color.Yellow); } })); @@ -2298,7 +2298,7 @@ namespace Barotrauma return; } - string skillIdentifier = args[0]; + Identifier skillIdentifier = args[0].ToIdentifier(); string levelString = args[1]; Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : senderClient.Character; @@ -2313,7 +2313,7 @@ namespace Barotrauma if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax) { if (isMax) { level = 100; } - if (skillIdentifier.Equals("all", StringComparison.OrdinalIgnoreCase)) + if (skillIdentifier == "all") { foreach (Skill skill in character.Info.Job.Skills) { @@ -2397,7 +2397,7 @@ namespace Barotrauma { GameMain.Server.CreateEntityEvent(c, new object[] { NetEntityEvent.Type.Status }); }*/ - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { GameMain.Server.CreateEntityEvent(hull); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs index 7569c73c0..718c588b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/EventActions/ConversationAction.cs @@ -80,7 +80,7 @@ namespace Barotrauma partial void ShowDialog(Character speaker, Character targetCharacter) { targetClients.Clear(); - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { IEnumerable entities = ParentEvent.GetTargets(TargetTag); foreach (Entity e in entities) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs index 99e79fc47..1ab83b7ab 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -15,7 +15,7 @@ namespace Barotrauma msg.Write((ushort)spawnedItems.Count); foreach (Item item in spawnedItems) { - item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } msg.Write((byte)characters.Count); @@ -27,7 +27,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 10f5ce5ad..6b54790d7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -13,7 +13,8 @@ namespace Barotrauma item.WriteSpawnData(msg, item.ID, parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, - parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); + parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0, + inventorySlotIndices.ContainsKey(item) ? inventorySlotIndices[item] : -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 5ac067bf3..5446c04e1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -11,11 +11,11 @@ namespace Barotrauma private bool initialized = false; - public override string Description + public override LocalizedString Description { get { - if (descriptions == null) return ""; + if (descriptions == null) { return ""; } //non-team-specific description return descriptions[0]; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs index 060369f9a..dea3acc83 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -24,7 +24,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index b3dd48bff..ada2e763a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -16,11 +16,11 @@ namespace Barotrauma foreach (var kvp in spawnedResources) { msg.Write((byte)kvp.Value.Count); - var rotation = resourceClusters[kvp.Key].rotation; + var rotation = resourceClusters[kvp.Key].Rotation; msg.Write(rotation); foreach (var r in kvp.Value) { - r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0); + r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index fc1b5041c..064ccd3e3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -7,13 +7,13 @@ namespace Barotrauma partial void ShowMessageProjSpecific(int missionState) { int messageIndex = missionState - 1; - if (messageIndex >= Headers.Count && messageIndex >= Messages.Count) { return; } + if (messageIndex >= Headers.Length && messageIndex >= Messages.Length) { return; } if (messageIndex < 0) { return; } - string header = messageIndex < Headers.Count ? Headers[messageIndex] : ""; - string message = messageIndex < Messages.Count ? Messages[messageIndex] : ""; + LocalizedString header = messageIndex < Headers.Length ? Headers[messageIndex] : ""; + LocalizedString message = messageIndex < Messages.Length ? Messages[messageIndex] : ""; - GameServer.Log(TextManager.Get("MissionInfo") + ": " + header + " - " + message, ServerLog.MessageType.ServerMessage); + GameServer.Log($"{TextManager.Get("MissionInfo")}: {header} - {message}", ServerLog.MessageType.ServerMessage); } public virtual void ServerWriteInitial(IWriteMessage msg, Client c) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index 7d5c88bb1..f8e974834 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -15,7 +15,7 @@ namespace Barotrauma msg.Write((ushort)items.Count); foreach (Item item in items) { - item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs index 4eb529c2e..c04b48601 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -24,7 +24,7 @@ namespace Barotrauma msg.Write((ushort)characterItems[character].Count()); foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 292d53ce0..cdc8e3622 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -10,6 +10,7 @@ namespace Barotrauma private UInt16 originalInventoryID; private byte originalItemContainerIndex; + private int originalSlotIndex; private readonly List> executedEffectIndices = new List>(); @@ -24,7 +25,7 @@ namespace Barotrauma } else { - item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex); + item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex); } msg.Write((byte)executedEffectIndices.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs index dc5dbff31..010ed0224 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs @@ -13,7 +13,8 @@ namespace Barotrauma item.WriteSpawnData(msg, item.ID, parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, - parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); + parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0, + inventorySlotIndices.ContainsKey(item) ? inventorySlotIndices[item] : -1); } ServerWriteScanTargetStatus(msg); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 048ef43a9..236257000 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -20,7 +21,6 @@ namespace Barotrauma public static bool IsSingleplayer => NetworkMember == null; public static bool IsMultiplayer => NetworkMember != null; - private static World world; public static World World { @@ -31,7 +31,6 @@ namespace Barotrauma } set { world = value; } } - public static GameSettings Config; public static GameServer Server; public static NetworkMember NetworkMember @@ -58,28 +57,14 @@ namespace Barotrauma //TODO: maybe clean up instead of having these constants public static readonly Screen SubEditorScreen = UnimplementedScreen.Instance; - public static DecalManager DecalManager; - public static bool ShouldRun = true; private static Stopwatch stopwatch; - private static Queue prevUpdateRates = new Queue(); + private static readonly Queue prevUpdateRates = new Queue(); private static int updateCount = 0; - - private static ContentPackage vanillaContent; - public static ContentPackage VanillaContent - { - get - { - if (vanillaContent == null) - { - // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.CorePackages.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); - } - return vanillaContent; - } - } + + public static ContentPackage VanillaContent => ContentPackageManager.VanillaCorePackage; public readonly string[] CommandLineArgs; @@ -96,13 +81,14 @@ namespace Barotrauma FarseerPhysics.Settings.PositionIterations = 1; Console.WriteLine("Loading game settings"); - Config = new GameSettings(); + GameSettings.Init(); Console.WriteLine("Loading MD5 hash cache"); - Md5Hash.LoadCache(); + Md5Hash.Cache.Load(); Console.WriteLine("Initializing SteamManager"); SteamManager.Initialize(); + //TODO: figure out how consent is supposed to work for servers //Console.WriteLine("Initializing GameAnalytics"); //GameAnalyticsManager.InitIfConsented(); @@ -115,36 +101,11 @@ namespace Barotrauma public void Init() { - NPCSet.LoadSets(); - FactionPrefab.LoadFactions(); - CharacterPrefab.LoadAll(); - MissionPrefab.Init(); - TraitorMissionPrefab.Init(); - MapEntityPrefab.Init(); - MapGenerationParams.Init(); - LevelGenerationParams.LoadPresets(); - CaveGenerationParams.LoadPresets(); - OutpostGenerationParams.LoadPresets(); - EventSet.LoadPrefabs(); - Order.Init(); - EventManagerSettings.Init(); - ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); - AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); - SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); - StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); - UpgradePrefab.LoadAll(GetFilesOfType(ContentType.UpgradeModules)); - JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); - CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); - NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); - ItemAssemblyPrefab.LoadAll(); - LevelObjectPrefab.LoadAll(); - BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); - TalentPrefab.LoadAll(GetFilesOfType(ContentType.Talents)); - TalentTree.LoadAll(GetFilesOfType(ContentType.TalentTrees)); + CoreEntityPrefab.InitCorePrefabs(); GameModePreset.Init(); - DecalManager = new DecalManager(); - LocationType.Init(); + + ContentPackageManager.Init().Consume(); SubmarineInfo.RefreshSavedSubs(); @@ -180,23 +141,6 @@ namespace Barotrauma }*/ } - /// - /// Returns the file paths of all files of the given type in the content packages. - /// - /// - /// If true, also returns files in content packages that are installed but not currently selected. - public IEnumerable GetFilesOfType(ContentType type, bool searchAllContentPackages = false) - { - if (searchAllContentPackages) - { - return ContentPackage.GetFilesOfType(ContentPackage.AllPackages, type); - } - else - { - return ContentPackage.GetFilesOfType(Config.AllEnabledPackages, type); - } - } - public bool TryStartChildServerRelay() { for (int i = 0; i < CommandLineArgs.Length; i++) @@ -383,6 +327,9 @@ namespace Barotrauma //otherwise it snowballs and becomes unplayable Timing.Accumulator = Timing.Step; } + + CrossThread.ProcessTasks(); + prevTicks = currTicks; while (Timing.Accumulator >= Timing.Step) { @@ -455,7 +402,8 @@ namespace Barotrauma SaveUtil.CleanUnnecessarySaveFiles(); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameAnalyticsManager.SendUserStatistics) { GameAnalyticsManager.ShutDown(); } MainThread = null; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 1fe87b072..b888b2139 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -63,12 +63,12 @@ namespace Barotrauma if (!item.Removed && canAddToRemoveQueue && Entity.FindEntityByID(item.ID) is Item entity) { item.Removed = true; - Entity.Spawner.AddToRemoveQueue(entity); + Entity.Spawner.AddItemToRemoveQueue(entity); } SoldItems.Add(item); Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; - GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); } OnSoldItemsChanged?.Invoke(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 3dcc595ba..541435d72 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -46,14 +46,14 @@ namespace Barotrauma public void ServerWriteActiveOrders(IWriteMessage msg) { - ushort count = (ushort)ActiveOrders.Count(o => o.First != null && !o.Second.HasValue); + ushort count = (ushort)ActiveOrders.Count(o => o.Order != null && !o.FadeOutTime.HasValue); msg.Write(count); if (count > 0) { foreach (var activeOrder in ActiveOrders) { - if (!(activeOrder?.First is Order order) || activeOrder.Second.HasValue) { continue; } - OrderChatMessage.WriteOrder(msg, order, targetCharacter: null, order.TargetSpatialEntity, orderOption: null, orderPriority: 0, order.WallSectionIndex, isNewOrder: true); + if (!(activeOrder?.Order is Order order) || activeOrder.FadeOutTime.HasValue) { continue; } + OrderChatMessage.WriteOrder(msg, order, null, isNewOrder: true); bool hasOrderGiver = order.OrderGiver != null; msg.Write(hasOrderGiver); if (hasOrderGiver) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 9c3d14d77..a2bfc7450 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -14,8 +14,8 @@ namespace Barotrauma { foreach (Mission mission in Missions) { - GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, ServerLog.MessageType.ServerMessage); - GameServer.Log(mission.Description, ServerLog.MessageType.ServerMessage); + GameServer.Log($"{TextManager.Get("Mission")}: {mission.Name}", ServerLog.MessageType.ServerMessage); + GameServer.Log(mission.Description.Value, ServerLog.MessageType.ServerMessage); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 8013b965b..f8c39e187 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -94,7 +94,7 @@ namespace Barotrauma { throw new System.InvalidOperationException($"Failed to spawn inventory items for the character \"{character.Name}\". No saved inventory data."); } - character.SpawnInventoryItems(inventory, itemData); + character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); } public void ApplyHealthData(Character character) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs index 51ad5e730..56c27906a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs @@ -6,8 +6,8 @@ { foreach (Mission mission in missions) { - Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); - Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log($"{TextManager.Get("Mission")}: {mission.Name}", Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log(mission.Description.Value, Networking.ServerLog.MessageType.ServerMessage); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index fc2eaf90e..f736290f3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -607,7 +607,7 @@ namespace Barotrauma foreach (var itemSwap in UpgradeManager.PurchasedItemSwaps) { msg.Write(itemSwap.ItemToRemove.ID); - msg.Write(itemSwap.ItemToInstall?.Identifier ?? string.Empty); + msg.Write(itemSwap.ItemToInstall?.Identifier ?? Identifier.Empty); } var characterData = GetClientCharacterData(c); @@ -681,10 +681,10 @@ namespace Barotrauma List purchasedUpgrades = new List(); for (int i = 0; i < purchasedUpgradeCount; i++) { - string upgradeIdentifier = msg.ReadString(); + Identifier upgradeIdentifier = msg.ReadIdentifier(); UpgradePrefab prefab = UpgradePrefab.Find(upgradeIdentifier); - string categoryIdentifier = msg.ReadString(); + Identifier categoryIdentifier = msg.ReadIdentifier(); UpgradeCategory category = UpgradeCategory.Find(categoryIdentifier); int upgradeLevel = msg.ReadByte(); @@ -698,8 +698,8 @@ namespace Barotrauma for (int i = 0; i < purchasedItemSwapCount; i++) { UInt16 itemToRemoveID = msg.ReadUInt16(); - string itemToInstallIdentifier = msg.ReadString(); - ItemPrefab itemToInstall = string.IsNullOrEmpty(itemToInstallIdentifier) ? null : ItemPrefab.Find(string.Empty, itemToInstallIdentifier); + 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)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs index 460c5ec46..6548e6046 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -9,11 +9,11 @@ namespace Barotrauma.Items.Components msg.Write(tainted); if (tainted) { - msg.Write(selectedTaintedEffect?.UIntIdentifier ?? 0); + msg.Write(selectedTaintedEffect?.UintIdentifier ?? 0); } else { - msg.Write(selectedEffect?.UIntIdentifier ?? 0); + msg.Write(selectedEffect?.UintIdentifier ?? 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index b4490c121..cac426158 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -9,9 +9,9 @@ namespace Barotrauma.Items.Components private const int serverHealthUpdateDelay = 10; private int serverHealthUpdateTimer; - partial void LoadVines(XElement element) + partial void LoadVines(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs index 7235d11c7..25d01e567 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class ItemComponent : ISerializableEntity { - private bool LoadElemProjSpecific(XElement subElement) + private bool LoadElemProjSpecific(ContentXElement subElement) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index be9d09f71..912920409 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -11,28 +11,28 @@ namespace Barotrauma.Items.Components private string lastSentText; private float sendStateTimer; - [Serialize("", true, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)] + [Serialize("", IsPropertySaveable.Yes, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { get; set; } - [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label.", alwaysUseInstanceValues: true)] + [Editable, Serialize("0,0,0,255", IsPropertySaveable.Yes, description: "The color of the text displayed on the label.", alwaysUseInstanceValues: true)] public Color TextColor { get; set; } - [Editable, Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] + [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { get; set; } - [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] + [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] public Vector4 Padding { get; @@ -44,7 +44,7 @@ namespace Barotrauma.Items.Components //do nothing } - public ItemLabel(Item item, XElement element) + public ItemLabel(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 335137727..baddfb0d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -11,23 +11,23 @@ namespace Barotrauma.Items.Components { public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - int itemIndex = msg.ReadRangedInteger(-1, fabricationRecipes.Count - 1); + uint recipeHash = msg.ReadUInt32(); item.CreateServerEvent(this); if (!item.CanClientAccess(c)) return; - if (itemIndex == -1) + if (recipeHash == 0) { CancelFabricating(c.Character); } else { //if already fabricating the selected item, return - if (fabricatedItem != null && fabricationRecipes.IndexOf(fabricatedItem) == itemIndex) return; - if (itemIndex < 0 || itemIndex >= fabricationRecipes.Count) return; + if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; } + if (recipeHash == 0) { return; } - StartFabricating(fabricationRecipes[itemIndex], c.Character); + StartFabricating(fabricationRecipes[recipeHash], c.Character); } } @@ -48,9 +48,9 @@ namespace Barotrauma.Items.Components FabricatorState stateAtEvent = (FabricatorState)extraData[3]; msg.Write((byte)stateAtEvent); msg.Write(timeUntilReady); - int itemIndex = fabricatedItem == null ? -1 : fabricationRecipes.IndexOf(fabricatedItem); - msg.WriteRangedInteger(itemIndex, -1, fabricationRecipes.Count - 1); - UInt16 userID = fabricatedItem == null || user == null ? (UInt16)0 : user.ID; + uint recipeHash = fabricatedItem?.RecipeHash ?? 0; + msg.Write(recipeHash); + UInt16 userID = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; msg.Write(userID); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 5f9b0385b..673f53972 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -4,7 +4,10 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { - void InitProjSpecific() + private Character prevLoggedFixer; + private FixActions prevLoggedFixAction; + + partial void InitProjSpecific(ContentXElement _) { //let the clients know the initial deterioration delay item.CreateServerEvent(this); @@ -19,7 +22,7 @@ namespace Barotrauma.Items.Components { if (!c.Character.IsTraitor && requestedFixAction == FixActions.Sabotage) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log($"Non traitor \"{c.Character.Name}\" attempted to sabotage item."); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 338c9ce7f..ccf34fa50 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -33,7 +33,7 @@ namespace Barotrauma { accessible = false; } - else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead) + else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead && !characterInventory.AccessibleByOwner) { accessible = false; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 918bfde23..e229c8ed9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -2,6 +2,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -14,7 +15,7 @@ namespace Barotrauma public override Sprite Sprite { - get { return prefab?.sprite; } + get { return base.Prefab?.Sprite; } } partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) @@ -232,14 +233,14 @@ namespace Barotrauma } } - public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex) + public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex) { if (GameMain.Server == null) { return; } msg.Write(Prefab.OriginalName); msg.Write(Prefab.Identifier); - msg.Write(Description != prefab.Description); - if (Description != prefab.Description) + msg.Write(Description != base.Prefab.Description); + if (Description != base.Prefab.Description) { msg.Write(Description); } @@ -259,9 +260,7 @@ namespace Barotrauma { msg.Write(originalInventoryID); msg.Write(originalItemContainerIndex); - - int slotIndex = ParentInventory.FindIndex(this); - msg.Write(slotIndex < 0 ? (byte)255 : (byte)slotIndex); + msg.Write(originalSlotIndex < 0 ? (byte)255 : (byte)originalSlotIndex); } msg.Write(body == null ? (byte)0 : (byte)body.BodyType); @@ -285,13 +284,13 @@ namespace Barotrauma } msg.Write(teamID); - bool tagsChanged = tags.Count != prefab.Tags.Count || !tags.All(t => prefab.Tags.Contains(t)); + bool tagsChanged = tags.Count != base.Prefab.Tags.Count || !tags.All(t => base.Prefab.Tags.Contains(t)); msg.Write(tagsChanged); if (tagsChanged) { - string[] splitTags = Tags.Split(','); - msg.Write(string.Join(',', splitTags.Where(t => !prefab.Tags.Contains(t)))); - msg.Write(string.Join(',', prefab.Tags.Where(t => !splitTags.Contains(t)))); + IEnumerable splitTags = Tags.Split(',').ToIdentifiers(); + msg.Write(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t)))); + msg.Write(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t)))); } var nameTag = GetComponent(); msg.Write(nameTag != null); @@ -386,7 +385,7 @@ namespace Barotrauma if (!ItemList.Contains(this)) { string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.ThrowError(errorMsg); + DebugConsole.AddWarning(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 6c7bf6616..65beaf5ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -7,9 +7,11 @@ namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { - partial void LoadPrefab(XElement element) + private float damageUpdateTimer; + + partial void LoadPrefab(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -29,7 +31,24 @@ namespace Barotrauma.MapCreatures.Behavior } } - + + partial void UpdateDamage(float deltaTime) + { + if (damageUpdateTimer <= 0) + { + foreach (BallastFloraBranch branch in Branches) + { + if (Math.Abs(branch.AccumulatedDamage) > 1.0f) + { + SendNetworkMessage(this, NetworkHeader.BranchDamage, branch); + branch.AccumulatedDamage = 0f; + } + } + damageUpdateTimer = 1f; + } + damageUpdateTimer -= deltaTime; + } + public void ServerWriteSpawn(IWriteMessage msg) { msg.Write(Prefab.Identifier); @@ -49,12 +68,12 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write((ushort)branch.MaxHealth); msg.Write((int)(x / VineTile.Size)); msg.Write((int)(y / VineTile.Size)); + msg.Write(branch.ParentBranch == null ? -1 : Branches.IndexOf(branch.ParentBranch)); } - public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch, float damage) + public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) { msg.Write((int)branch.ID); - msg.Write(damage); msg.Write(branch.Health); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 827171ade..861494dc6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -82,12 +82,13 @@ namespace Barotrauma behavior.ServerWriteSpawn(message); break; case BallastFloraBehavior.NetworkHeader.Kill: + case BallastFloraBehavior.NetworkHeader.Remove: break; case BallastFloraBehavior.NetworkHeader.BranchCreate when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is int parentId: behavior.ServerWriteBranchGrowth(message, branch, parentId); break; - case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is float damage: - behavior.ServerWriteBranchDamage(message, branch, damage); + case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch: + behavior.ServerWriteBranchDamage(message, branch); break; case BallastFloraBehavior.NetworkHeader.BranchRemove when extraData.Length >= 3 && extraData[2] is BallastFloraBranch branch: behavior.ServerWriteBranchRemove(message, branch); @@ -147,7 +148,7 @@ namespace Barotrauma message.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); foreach (Decal decal in decals) { - message.Write(decal.Prefab.UIntIdentifier); + message.Write(decal.Prefab.UintIdentifier); message.Write((byte)decal.SpriteIndex); float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(0.0f, rect.Width, decal.CenterPosition.X), 0.0f, 1.0f); float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(-rect.Height, 0.0f, decal.CenterPosition.Y), 0.0f, 1.0f); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 16829a04f..edd4690ca 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -20,12 +20,13 @@ namespace Barotrauma.Networking OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = Order.OrderTargetType.Entity; int? wallSectionIndex = null; + Order order = null; if (type == ChatMessageType.Order) { var orderMessageInfo = OrderChatMessage.ReadOrder(msg); - if (orderMessageInfo.OrderIndex < 0 || orderMessageInfo.OrderIndex >= Order.PrefabList.Count) + if (orderMessageInfo.OrderIdentifier == Identifier.Empty) { - DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderMessageInfo.OrderIndex})."); + DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order identifier is empty."); if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } @@ -34,14 +35,29 @@ namespace Barotrauma.Networking orderTargetPosition = orderMessageInfo.TargetPosition; orderTargetType = orderMessageInfo.TargetType; wallSectionIndex = orderMessageInfo.WallSectionIndex; - var orderPrefab = orderMessageInfo.OrderPrefab ?? Order.PrefabList[orderMessageInfo.OrderIndex]; - string orderOption = orderMessageInfo.OrderOption ?? - (orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? - "" : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]); - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderMessageInfo.Priority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character, isNewOrder: orderMessageInfo.IsNewOrder) + var orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; + Identifier orderOption = orderMessageInfo.OrderOption; + if (orderOption.IsEmpty) { - WallSectionIndex = wallSectionIndex - }; + orderOption = orderMessageInfo.OrderOptionIndex == null || orderMessageInfo.OrderOptionIndex < 0 || orderMessageInfo.OrderOptionIndex >= orderPrefab.Options.Length ? + Identifier.Empty : orderPrefab.Options[orderMessageInfo.OrderOptionIndex.Value]; + } + if (orderTargetType == Order.OrderTargetType.Position) + { + order = new Order(orderPrefab, orderOption, orderTargetPosition, orderGiver: c.Character) + .WithManualPriority(orderMessageInfo.Priority); + } + else if (orderTargetType == Order.OrderTargetType.WallSection) + { + order = new Order(orderPrefab, orderOption, orderTargetEntity as Structure, wallSectionIndex, orderGiver: c.Character) + .WithManualPriority(orderMessageInfo.Priority); + } + else + { + order = new Order(orderPrefab, orderOption, orderTargetEntity, orderPrefab.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: c.Character) + .WithManualPriority(orderMessageInfo.Priority); + } + orderMsg = new OrderChatMessage(order, orderTargetCharacter, c.Character); txt = orderMsg.Text; } else @@ -95,11 +111,11 @@ namespace Barotrauma.Networking if (c.ChatSpamCount > 3) { //kick for spamming too much - GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked")); + GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked").Value); } else { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null); + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; GameMain.Server.SendDirectChatMessage(denyMsg, c); } @@ -110,7 +126,7 @@ namespace Barotrauma.Networking if (c.ChatSpamTimer > 0.0f && !isOwner) { - ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null); + ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); c.ChatSpamTimer = 10.0f; GameMain.Server.SendDirectChatMessage(denyMsg, c); return; @@ -123,16 +139,6 @@ namespace Barotrauma.Networking { HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order); } - Order order = orderTargetType switch - { - Order.OrderTargetType.Entity => - new Order(orderMsg.Order, orderTargetEntity, orderMsg.Order?.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: orderMsg.Sender), - Order.OrderTargetType.Position => - new Order(orderMsg.Order, orderTargetPosition, orderGiver: orderMsg.Sender), - Order.OrderTargetType.WallSection when orderTargetEntity is Structure s && wallSectionIndex.HasValue => - new Order(orderMsg.Order, s, wallSectionIndex, orderGiver: orderMsg.Sender), - _ => throw new NotImplementedException() - }; if (order != null) { if (order.TargetAllCharacters) @@ -159,7 +165,7 @@ namespace Barotrauma.Networking } else if (orderTargetCharacter != null) { - orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.OrderPriority, orderMsg.Sender); + orderTargetCharacter.SetOrder(order); } } GameMain.Server.SendOrderChatMessage(orderMsg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 86c7ac808..b9daadc66 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -57,8 +57,8 @@ namespace Barotrauma.Networking public bool ReadyToStart; - public List> JobPreferences; - public Pair AssignedJob; + public List JobPreferences; + public JobVariant AssignedJob; public float DeleteDisconnectedTimer; @@ -104,7 +104,7 @@ namespace Barotrauma.Networking partial void InitProjSpecific() { - JobPreferences = new List>(); + JobPreferences = new List(); VoipQueue = new VoipQueue(ID, true, true); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); @@ -149,7 +149,7 @@ namespace Barotrauma.Networking foreach (char character in name) { - if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) { return false; } + if (!serverSettings.AllowedClientNameChars.Any(charRange => (int)character >= charRange.Start && (int)character <= charRange.End)) { return false; } } return true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index 9d15c0e82..fc56ade32 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -12,15 +11,21 @@ namespace Barotrauma partial void CreateNetworkEventProjSpecific(Entity entity, bool remove) { - if (GameMain.Server != null && entity != null) + if (GameMain.Server == null || entity == null) { return; } + + GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); + if (entity is Character character && character.Info != null) { - GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); - } + foreach (var statKey in character.Info.SavedStatValues.Keys) + { + GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statKey }); + } + } } public void ServerWrite(IWriteMessage message, Client client, object[] extraData = null) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } SpawnOrRemove entities = (SpawnOrRemove)extraData[0]; @@ -31,17 +36,17 @@ namespace Barotrauma } else { - if (entities.Entity is Item) + if (entities.Entity is Item item) { message.Write((byte)SpawnableType.Item); DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - ((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex); + item.WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex, entities.OriginalSlotIndex); } - else if (entities.Entity is Character) + else if (entities.Entity is Character character) { message.Write((byte)SpawnableType.Character); DebugConsole.Log("Writing character spawn data: " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - ((Character)entities.Entity).WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true); + character.WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index e1a4c0d60..4c1b87008 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -36,12 +36,25 @@ namespace Barotrauma.Networking get { return KnownReceivedOffset / (float)Data.Length; } } + private float waitTimer; public float WaitTimer { - get; - set; + get => waitTimer; + set + { + if (value > 0.0f) + { + //setting a wait timer means that network conditions + //aren't ideal, slow down the packet rate + PacketsPerUpdate = Math.Max(PacketsPerUpdate / 2.0f, 1.0f); + } + waitTimer = value; + } } + public const int MaxPacketsPerUpdate = 4; + public float PacketsPerUpdate { get; set; } = 1.0f; + public byte[] Data { get; } public bool Acknowledged; @@ -112,10 +125,7 @@ namespace Barotrauma.Networking public float StallPacketsTime { get; set; } #endif - public List ActiveTransfers - { - get { return activeTransfers; } - } + public IReadOnlyList ActiveTransfers => activeTransfers; public FileSender(ServerPeer serverPeer, int mtu) { @@ -197,9 +207,6 @@ namespace Barotrauma.Networking private void Send(FileTransferOut transfer) { // send another part of the file - long remaining = transfer.Data.Length - transfer.SentOffset; - int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); - IWriteMessage message; try @@ -234,7 +241,7 @@ namespace Barotrauma.Networking transfer.Status = FileTransferStatus.Sending; - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.Log("Sending file transfer initiation message: "); DebugConsole.Log(" File: " + transfer.FileName); @@ -246,28 +253,44 @@ namespace Barotrauma.Networking return; } - message = new WriteOnlyMessage(); - message.Write((byte)ServerPacketHeader.FILE_TRANSFER); - message.Write((byte)FileTransferMessageType.Data); - - message.Write((byte)transfer.ID); - message.Write(transfer.SentOffset); - - byte[] sendBytes = new byte[sendByteCount]; - Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount); - - message.Write((ushort)sendByteCount); - message.Write(sendBytes, 0, sendByteCount); - - transfer.SentOffset += sendByteCount; - if (transfer.SentOffset > transfer.KnownReceivedOffset + chunkLen * 10 || - transfer.SentOffset >= transfer.Data.Length) + for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++) { - transfer.SentOffset = transfer.KnownReceivedOffset; - transfer.WaitTimer = 0.5f; - } + long remaining = transfer.Data.Length - transfer.SentOffset; + int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); + + message = new WriteOnlyMessage(); + message.Write((byte)ServerPacketHeader.FILE_TRANSFER); + message.Write((byte)FileTransferMessageType.Data); - peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable); + message.Write((byte)transfer.ID); + message.Write(transfer.SentOffset); + + message.Write((ushort)sendByteCount); + int chunkDestPos = message.BytePosition; + message.BitPosition += sendByteCount * 8; + message.LengthBits = Math.Max(message.LengthBits, message.BitPosition); + Array.Copy(transfer.Data, transfer.SentOffset, message.Buffer, chunkDestPos, sendByteCount); + + transfer.SentOffset += sendByteCount; + if (transfer.SentOffset >= transfer.Data.Length) + { + transfer.SentOffset = transfer.KnownReceivedOffset; + transfer.WaitTimer = 0.5f; + } + + peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable, compressPastThreshold: false); + + if (GameSettings.CurrentConfig.VerboseLogging) + { + DebugConsole.Log($"Sending {sendByteCount} bytes of the file {transfer.FileName} ({transfer.SentOffset / 1000}/{transfer.Data.Length / 1000} kB sent)"); + } + + //try to increase the packet rate so large files get sent faster, + //this gets reset when packet loss or disorder sets in + transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate, + transfer.PacketsPerUpdate + 0.05f); + } + #if DEBUG transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); #endif @@ -283,11 +306,6 @@ namespace Barotrauma.Networking transfer.Status = FileTransferStatus.Error; return; } - - if (GameSettings.VerboseLogging) - { - DebugConsole.Log($"Sending {sendByteCount} bytes of the file {transfer.FileName} ({transfer.SentOffset / 1000}/{transfer.Data.Length / 1000} kB sent)"); - } } public void CancelTransfer(FileTransferOut transfer) @@ -302,9 +320,9 @@ namespace Barotrauma.Networking public void ReadFileRequest(IReadMessage inc, Client client) { - byte messageType = inc.ReadByte(); + FileTransferMessageType messageType = (FileTransferMessageType)inc.ReadByte(); - if (messageType == (byte)FileTransferMessageType.Cancel) + if (messageType == FileTransferMessageType.Cancel) { byte transferId = inc.ReadByte(); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); @@ -312,20 +330,28 @@ namespace Barotrauma.Networking return; } - else if (messageType == (byte)FileTransferMessageType.Data) + else if (messageType == FileTransferMessageType.Data) { byte transferId = inc.ReadByte(); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); if (matchingTransfer != null) { matchingTransfer.Acknowledged = true; - int offset = inc.ReadInt32(); - matchingTransfer.KnownReceivedOffset = offset > matchingTransfer.KnownReceivedOffset ? offset : matchingTransfer.KnownReceivedOffset; + int expecting = inc.ReadInt32(); //the offset the client is waiting for + int lastSeen = Math.Min(matchingTransfer.SentOffset, inc.ReadInt32()); //the last offset the client got from us + matchingTransfer.KnownReceivedOffset = Math.Max(expecting, matchingTransfer.KnownReceivedOffset); if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset) { matchingTransfer.WaitTimer = 0.0f; matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; } + + if (lastSeen - matchingTransfer.KnownReceivedOffset >= chunkLen * 10 || + matchingTransfer.SentOffset >= matchingTransfer.Data.Length) + { + matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; + matchingTransfer.WaitTimer = 0.5f; + } if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) { @@ -334,20 +360,20 @@ namespace Barotrauma.Networking } } - byte fileType = inc.ReadByte(); + FileTransferType fileType = (FileTransferType)inc.ReadByte(); switch (fileType) { - case (byte)FileTransferType.Submarine: + case FileTransferType.Submarine: string fileName = inc.ReadString(); string fileHash = inc.ReadString(); - var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash); + var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.StringRepresentation == fileHash); if (requestedSubmarine != null) { StartTransfer(inc.Sender, FileTransferType.Submarine, requestedSubmarine.FilePath); } break; - case (byte)FileTransferType.CampaignSave: + case FileTransferType.CampaignSave: if (GameMain.GameSession != null && !ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave)) { @@ -357,6 +383,23 @@ namespace Barotrauma.Networking client.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now); } } + break; + case FileTransferType.Mod: + string modName = inc.ReadString(); + Md5Hash modHash = Md5Hash.StringAsHash(inc.ReadString()); + + if (!GameMain.Server.ServerSettings.AllowModDownloads) { return; } + if (!(GameMain.Server.ModSender is { Ready: true })) { return; } + + ContentPackage mod = ContentPackageManager.AllPackages.FirstOrDefault(p => p.Hash.Equals(modHash)); + + if (mod is null) { return; } + + string modCompressedPath = ModSender.GetCompressedModPath(mod); + if (!File.Exists(modCompressedPath)) { return; } + + StartTransfer(inc.Sender, FileTransferType.Mod, modCompressedPath); + break; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs new file mode 100644 index 000000000..9d32305d1 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Barotrauma.Networking +{ + class ModSender : IDisposable + { + public const string UploadFolder = "TempMods_Upload"; + public const string Extension = ".barodir.gz"; + + public bool Ready { get; private set; } = false; + + public ModSender() + { + DeleteDir(); + Directory.CreateDirectory(UploadFolder); + TaskPool.Add( + "ModSender", + Task.WhenAll( + ContentPackageManager.EnabledPackages.All + .Where(p => p != ContentPackageManager.VanillaCorePackage && p.HasMultiplayerIncompatibleContent) + .Select(CompressMod)), + (t) => Ready = true); + } + + public static string GetCompressedModPath(ContentPackage mod) + { + string dir = mod.Dir; + string resultFileName = dir.Replace('\\', '_').Replace('/', '_'); + resultFileName = $"{resultFileName}{Extension}"; + return Path.Combine(UploadFolder, resultFileName); + } + + public async Task CompressMod(ContentPackage mod) + { + await Task.Yield(); + string dir = mod.Dir; + SaveUtil.CompressDirectory(dir, GetCompressedModPath(mod), fileName => { }); + } + + private void DeleteDir() + { + if (Directory.Exists(UploadFolder)) { Directory.Delete(UploadFolder, recursive: true); } + } + + public bool IsDisposed { get; private set; } = false; + public void Dispose() + { + IsDisposed = true; + DeleteDir(); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index d2ff2efe6..ebd958713 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -7,6 +7,7 @@ using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; @@ -16,13 +17,9 @@ namespace Barotrauma.Networking { partial class GameServer : NetworkMember { - public override bool IsServer - { - get { return true; } - } + public override bool IsServer => true; private string serverName; - public string ServerName { get { return serverName; } @@ -74,16 +71,14 @@ namespace Barotrauma.Networking private readonly ServerEntityEventManager entityEventManager; - private FileSender fileSender; + public FileSender FileSender { get; private set; } - public FileSender FileSender - { - get { return fileSender; } - } + public ModSender ModSender { get; private set; } + #if DEBUG public void PrintSenderTransters() { - foreach (var transfer in fileSender.ActiveTransfers) + foreach (var transfer in FileSender.ActiveTransfers) { DebugConsole.NewMessage(transfer.FileName + " " + transfer.Progress.ToString()); } @@ -167,9 +162,11 @@ namespace Barotrauma.Networking serverPeer.OnShutdown = GameMain.Instance.CloseServer; serverPeer.OnOwnerDetermined = OnOwnerDetermined; - fileSender = new FileSender(serverPeer, MsgConstants.MTU); - fileSender.OnEnded += FileTransferChanged; - fileSender.OnStarted += FileTransferChanged; + FileSender = new FileSender(serverPeer, MsgConstants.MTU); + FileSender.OnEnded += FileTransferChanged; + FileSender.OnStarted += FileTransferChanged; + + if (serverSettings.AllowModDownloads) { ModSender = new ModSender(); } serverPeer.Start(); @@ -344,7 +341,7 @@ namespace Barotrauma.Networking base.Update(deltaTime); - fileSender.Update(deltaTime); + FileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); UpdatePing(); @@ -455,7 +452,7 @@ namespace Barotrauma.Networking #if !DEBUG if (endRoundTimer <= 0.0f) { - SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60"), ChatMessageType.Server); + SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60").Value, ChatMessageType.Server); } endRoundDelay = 60.0f; endRoundTimer += deltaTime; @@ -645,10 +642,10 @@ namespace Barotrauma.Networking if (registeredToMaster && (DateTime.Now > refreshMasterTimer || serverSettings.ServerDetailsChanged)) { - if (GameMain.Config.UseSteamMatchmaking) + if (GameSettings.CurrentConfig.UseSteamMatchmaking) { bool refreshSuccessful = SteamManager.RefreshServerDetails(this); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { Log(refreshSuccessful ? "Refreshed server info on the server list." : @@ -668,7 +665,7 @@ namespace Barotrauma.Networking if (Timing.TotalTime > lastPingTime + 1.0) { lastPingData ??= new byte[64]; - for (int i=0;i s.Name == subName && s.MD5Hash.Hash == subHash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); if (gameStarted) { - SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning"), connectedClient, ChatMessageType.MessageBox); + SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } if (matchingSub == null) { SendDirectChatMessage( - TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName), + TextManager.GetWithVariable("CampaignStartFailedSubNotFound", "[subname]", subName).Value, connectedClient, ChatMessageType.MessageBox); } else @@ -787,7 +784,7 @@ namespace Barotrauma.Networking string saveName = inc.ReadString(); if (gameStarted) { - SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning"), connectedClient, ChatMessageType.MessageBox); + SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); return; } if (connectedClient.HasPermission(ClientPermissions.SelectMode) || connectedClient.HasPermission(ClientPermissions.ManageCampaign)) { MultiPlayerCampaign.LoadCampaign(saveName); } @@ -829,7 +826,7 @@ namespace Barotrauma.Networking case ClientPacketHeader.FILE_REQUEST: if (serverSettings.AllowFileTransfers) { - fileSender.ReadFileRequest(inc, connectedClient); + FileSender.ReadFileRequest(inc, connectedClient); } break; case ClientPacketHeader.EVENTMANAGER_RESPONSE: @@ -881,12 +878,15 @@ namespace Barotrauma.Networking { errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } - var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); - if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) + if (gameStarted) { - string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)})."; - errorStr += subErrorStr; - errorStrNoName += subErrorStr; + var serverSubNames = Submarine.Loaded.Select(s => s.Info.Name); + if (subCount != Submarine.Loaded.Count || !subNames.SequenceEqual(serverSubNames)) + { + string subErrorStr = $" Loaded submarines don't match (client: {string.Join(", ", subNames)}, server: {string.Join(", ", serverSubNames)})."; + errorStr += subErrorStr; + errorStrNoName += subErrorStr; + } } break; } @@ -922,7 +922,7 @@ namespace Barotrauma.Networking Directory.CreateDirectory(ServerLog.SavePath); } - string filePath = "event_error_log_server_" + client.Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + string filePath = $"event_error_log_server_{client.Name}_{DateTime.UtcNow.ToShortTimeString()}.log"; filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (File.Exists(filePath)) { return; } @@ -933,7 +933,7 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode != null) { - errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name); + errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name.Value); if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) { errorLines.Add("Campaign ID: " + campaign.CampaignID); @@ -957,19 +957,18 @@ namespace Barotrauma.Networking errorLines.Add("Level: " + Level.Loaded.Seed + ", " + string.Join(", ", Level.Loaded.EqualityCheckValues.Select(cv => cv.ToString("X")))); errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); errorLines.Add("Entities:"); - foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate) + foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate.OrderBy(e => e.CreationIndex)) { - errorLines.Add(" " + e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate); } errorLines.Add("Entity IDs:"); - List sortedEntities = Entity.GetEntities().ToList(); - sortedEntities.Sort((e1, e2) => e1.ID.CompareTo(e2.ID)); + Entity[] sortedEntities = Entity.GetEntities().OrderBy(e => e.CreationIndex).ToArray(); foreach (Entity e in sortedEntities) { - errorLines.Add(e.ID + ": " + e.ToString()); + errorLines.Add(e.ErrorLine); } errorLines.Add(""); @@ -1160,7 +1159,7 @@ namespace Barotrauma.Networking { c.LastRecvChatMsgID = lastRecvChatMsgID; } - else if (lastRecvChatMsgID != c.LastRecvChatMsgID && GameSettings.VerboseLogging) + else if (lastRecvChatMsgID != c.LastRecvChatMsgID && GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError( "Invalid lastRecvChatMsgID " + lastRecvChatMsgID + @@ -1179,8 +1178,13 @@ namespace Barotrauma.Networking } c.LastRecvEntityEventID = lastRecvEntityEventID; + #warning TODO: remove this later + /*if (!CoroutineManager.IsCoroutineRunning("RoundRestartLoop")) + { + CoroutineManager.StartCoroutine(RoundRestartLoop(), "RoundRestartLoop"); + }*/ } - else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.VerboseLogging) + else if (lastRecvEntityEventID != c.LastRecvEntityEventID && GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError( "Invalid lastRecvEntityEventID " + lastRecvEntityEventID + @@ -1224,6 +1228,16 @@ namespace Barotrauma.Networking } } + #warning TODO: remove this later + /*private IEnumerable RoundRestartLoop() + { + yield return new WaitForSeconds(8.0f); + EndGame(); + yield return new WaitForSeconds(8.0f); + StartGame(); + yield return CoroutineStatus.Success; + }*/ + private void ReadCrewMessage(IReadMessage inc, Client sender) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) @@ -1304,7 +1318,7 @@ namespace Barotrauma.Networking } else { - SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={kickedName}"), sender, ChatMessageType.Console); + SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={kickedName}").Value, sender, ChatMessageType.Console); } break; case ClientPermissions.Ban: @@ -1332,7 +1346,7 @@ namespace Barotrauma.Networking } else { - SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}"), sender, ChatMessageType.Console); + SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}").Value, sender, ChatMessageType.Console); } } break; @@ -1433,9 +1447,9 @@ namespace Barotrauma.Networking case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); GameMain.NetLobbyScreen.SelectedModeIndex = modeIndex; - Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name, ServerLog.MessageType.ServerMessage); + Log("Gamemode changed to " + GameMain.NetLobbyScreen.GameModes[GameMain.NetLobbyScreen.SelectedModeIndex].Name.Value, ServerLog.MessageType.ServerMessage); - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) + if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign") { string[] saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false).ToArray(); for (int i = 0; i < saveFiles.Length; i++) @@ -1446,9 +1460,9 @@ namespace Barotrauma.Networking saveFiles[i] = string.Join(";", saveFiles[i].Replace(';', ' '), - doc.Root.GetAttributeString("submarine", ""), - doc.Root.GetAttributeString("savetime", ""), - doc.Root.GetAttributeString("selectedcontentpackages", "")); + doc.Root.GetAttributeStringUnrestricted("submarine", ""), + doc.Root.GetAttributeStringUnrestricted("savetime", ""), + doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", "")); } } @@ -1543,9 +1557,9 @@ namespace Barotrauma.Networking } } - if (!fileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) + if (!FileSender.ActiveTransfers.Any(t => t.Connection == c.Connection && t.FileType == FileTransferType.CampaignSave)) { - fileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); + FileSender.StartTransfer(c.Connection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath); c.LastCampaignSaveSendTime = new Pair(campaign.LastSaveID, (float)NetTime.Now); } } @@ -1556,7 +1570,7 @@ namespace Barotrauma.Networking /// private void ClientWriteInitial(Client c, IWriteMessage outmsg) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Sending initial lobby update", Color.Gray); } @@ -1701,15 +1715,15 @@ namespace Barotrauma.Networking { var entity = c.PendingPositionUpdates.Peek(); if (entity == null || entity.Removed || - (entity is Item item && item.PositionUpdateInterval == float.PositiveInfinity)) + (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { c.PendingPositionUpdates.Dequeue(); continue; } IWriteMessage tempBuffer = new ReadWriteMessage(); - tempBuffer.Write((byte)ServerNetObject.ENTITY_POSITION); - tempBuffer.Write(entity is Item); + tempBuffer.Write(entity is Item); tempBuffer.WritePadBits(); + tempBuffer.Write(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); if (entity is Item) { ((Item)entity).ServerWritePosition(tempBuffer, c); @@ -1725,6 +1739,8 @@ namespace Barotrauma.Networking break; } + outmsg.Write((byte)ServerNetObject.ENTITY_POSITION); + outmsg.WritePadBits(); //padding is required here to make sure any padding bits within tempBuffer are read correctly outmsg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); outmsg.WritePadBits(); @@ -1805,7 +1821,7 @@ namespace Barotrauma.Networking outmsg.Write(client.SteamID); outmsg.Write(client.NameID); outmsg.Write(client.Name); - outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : (client.PreferredJob ?? "")); + outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : client.PreferredJob); outmsg.Write((byte)client.PreferredTeam); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); if (c.HasPermission(ClientPermissions.ServerLog)) @@ -1953,7 +1969,7 @@ namespace Barotrauma.Networking #if DEBUG || UNSTABLE DebugConsole.ThrowError(warningMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.AddWarning(warningMsg); } #endif GameAnalyticsManager.AddErrorEventOnce("GameServer.ClientWriteIngame1:ClientWriteLobby" + outmsg.LengthBytes, GameAnalyticsManager.ErrorSeverity.Warning, warningMsg); } @@ -2043,11 +2059,11 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.QUERY_STARTGAME); msg.Write(selectedSub.Name); - msg.Write(selectedSub.MD5Hash.Hash); + msg.Write(selectedSub.MD5Hash.StringRepresentation); msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); msg.Write(selectedShuttle.Name); - msg.Write(selectedShuttle.MD5Hash.Hash); + msg.Write(selectedShuttle.MD5Hash.StringRepresentation); var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; msg.Write(campaign == null ? (byte)0 : campaign.CampaignID); @@ -2069,10 +2085,10 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; } - if (fileSender.ActiveTransfers.Count > 0) + if (FileSender.ActiveTransfers.Count > 0) { float waitForTransfersTimer = 20.0f; - while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) + while (FileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f) { waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime; yield return CoroutineStatus.Running; @@ -2156,7 +2172,7 @@ namespace Barotrauma.Networking GameMain.GameSession.StartRound(campaign.NextLevel, mirrorLevel: campaign.MirrorLevel); SubmarineSwitchLoad = false; campaign.AssignClientCharacterInfos(connectedClients); - Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); + Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.NextLevel.Seed, ServerLog.MessageType.ServerMessage); } @@ -2164,14 +2180,14 @@ namespace Barotrauma.Networking { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); - Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); + Log("Game mode: " + selectedMode.Name.Value, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); } foreach (Mission mission in GameMain.GameSession.Missions) { - Log("Mission: " + mission.Prefab.Name, ServerLog.MessageType.ServerMessage); + Log("Mission: " + mission.Prefab.Name.Value, ServerLog.MessageType.ServerMessage); } if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) @@ -2256,9 +2272,9 @@ namespace Barotrauma.Networking client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } characterInfos.Add(client.CharacterInfo); - if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) + if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.Prefab) { - client.CharacterInfo.Job = new Job(client.AssignedJob.First, Rand.RandSync.Unsynced, client.AssignedJob.Second); + client.CharacterInfo.Job = new Job(client.AssignedJob.Prefab, Rand.RandSync.Unsynced, client.AssignedJob.Variant); } } @@ -2305,7 +2321,7 @@ namespace Barotrauma.Networking wp.SpawnType == SpawnType.Human && wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull?.OutpostModuleTags != null && - wp.CurrentHull.OutpostModuleTags.Contains("airlock")); + wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -2481,14 +2497,14 @@ namespace Barotrauma.Networking msg.Write(levelSeed); msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write(gameSession.SubmarineInfo.Name); - msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); + msg.Write(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; msg.Write(selectedShuttle.Name); - msg.Write(selectedShuttle.MD5Hash.Hash); + msg.Write(selectedShuttle.MD5Hash.StringRepresentation); msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); foreach (Mission mission in GameMain.GameSession.GameMode.Missions) { - msg.Write((short)MissionPrefab.List.IndexOf(mission.Prefab)); + msg.Write(mission.Prefab.UintIdentifier); } } else @@ -2526,8 +2542,7 @@ namespace Barotrauma.Networking msg.Write((ushort)contentToPreload.Count()); foreach (ContentFile contentFile in contentToPreload) { - msg.Write((byte)contentFile.Type); - msg.Write(contentFile.Path); + msg.Write(contentFile.Path.Value); } msg.Write(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); msg.Write((byte)GameMain.GameSession.Missions.Count()); @@ -2555,7 +2570,7 @@ namespace Barotrauma.Networking return; } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage); @@ -2664,7 +2679,7 @@ namespace Barotrauma.Networking { UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); - string newJob = inc.ReadString(); + Identifier newJob = inc.ReadIdentifier(); CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } @@ -3150,7 +3165,7 @@ namespace Barotrauma.Networking if (type.Value != ChatMessageType.MessageBox) { - string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message) : message; + string myReceivedMessage = type == ChatMessageType.Server || type == ChatMessageType.Error ? TextManager.GetServerMessage(message).Value : message; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderClient, senderCharacter); @@ -3169,11 +3184,11 @@ namespace Barotrauma.Networking //too far to hear the msg -> don't send if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client); } if (!string.IsNullOrWhiteSpace(message.Text)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.Text, message.TargetEntity, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); + AddChatMessage(new OrderChatMessage(message.Order, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder)); } } @@ -3355,9 +3370,8 @@ namespace Barotrauma.Networking serverPeer.Send(msg, recipient.Connection, DeliveryMethod.Reliable); } - public void GiveAchievement(Character character, string achievementIdentifier) + public void GiveAchievement(Character character, Identifier achievementIdentifier) { - achievementIdentifier = achievementIdentifier.ToLowerInvariant(); foreach (Client client in connectedClients) { if (client.Character == character) @@ -3368,9 +3382,8 @@ namespace Barotrauma.Networking } } - public void IncrementStat(Character character, string achievementIdentifier, int amount) + public void IncrementStat(Character character, Identifier achievementIdentifier, int amount) { - achievementIdentifier = achievementIdentifier.ToLowerInvariant(); foreach (Client client in connectedClients) { if (client.Character == character) @@ -3381,7 +3394,7 @@ namespace Barotrauma.Networking } } - public void GiveAchievement(Client client, string achievementIdentifier) + public void GiveAchievement(Client client, Identifier achievementIdentifier) { if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } client.GivenAchievements.Add(achievementIdentifier); @@ -3394,7 +3407,7 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void IncrementStat(Client client, string achievementIdentifier, int amount) + public void IncrementStat(Client client, Identifier achievementIdentifier, int amount) { if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } @@ -3406,13 +3419,13 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } - public void SendTraitorMessage(Client client, string message, string missionIdentifier, TraitorMessageType messageType) + public void SendTraitorMessage(Client client, string message, Identifier missionIdentifier, TraitorMessageType messageType) { if (client == null) { return; } var msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.TRAITOR_MESSAGE); msg.Write((byte)messageType); - msg.Write(missionIdentifier ?? ""); + msg.Write(missionIdentifier); msg.Write(message); serverPeer.Send(msg, client.Connection, DeliveryMethod.ReliableOrdered); } @@ -3484,18 +3497,11 @@ namespace Barotrauma.Networking return; } - Gender gender = Gender.Male; - Race race = Race.White; - int headSpriteId = 0; - try + int tagCount = message.ReadByte(); + HashSet tagSet = new HashSet(); + for (int i = 0; i < tagCount; i++) { - gender = (Gender)message.ReadByte(); - race = (Race)message.ReadByte(); - headSpriteId = message.ReadByte(); - } - catch (Exception e) - { - DebugConsole.Log("Received invalid characterinfo from \"" + sender.Name + "\"! { " + e.Message + " }"); + tagSet.Add(message.ReadIdentifier()); } int hairIndex = message.ReadByte(); int beardIndex = message.ReadByte(); @@ -3505,7 +3511,7 @@ namespace Barotrauma.Networking Color hairColor = message.ReadColorR8G8B8(); Color facialHairColor = message.ReadColorR8G8B8(); - List> jobPreferences = new List>(); + List jobPreferences = new List(); int count = message.ReadByte(); // TODO: modding support? for (int i = 0; i < Math.Min(count, 3); i++) @@ -3514,15 +3520,15 @@ namespace Barotrauma.Networking int variant = message.ReadByte(); if (JobPrefab.Prefabs.ContainsKey(jobIdentifier)) { - jobPreferences.Add(new Pair(JobPrefab.Prefabs[jobIdentifier], variant)); + jobPreferences.Add(new JobVariant(JobPrefab.Prefabs[jobIdentifier], variant)); } } sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); - sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); - sender.CharacterInfo.SkinColor = skinColor; - sender.CharacterInfo.HairColor = hairColor; - sender.CharacterInfo.FacialHairColor = facialHairColor; + sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + sender.CharacterInfo.Head.SkinColor = skinColor; + sender.CharacterInfo.Head.HairColor = hairColor; + sender.CharacterInfo.Head.FacialHairColor = facialHairColor; if (jobPreferences.Count > 0) { @@ -3556,7 +3562,7 @@ namespace Barotrauma.Networking foreach (KeyValuePair clientJob in campaignAssigned) { assignedClientCount[clientJob.Value.Prefab]++; - clientJob.Key.AssignedJob = new Pair(clientJob.Value.Prefab, clientJob.Value.Variant); + clientJob.Key.AssignedJob = new JobVariant(clientJob.Value.Prefab, clientJob.Value.Variant); } } @@ -3574,7 +3580,7 @@ namespace Barotrauma.Networking for (int i = unassigned.Count - 1; i >= 0; i--) { if (unassigned[i].JobPreferences.Count == 0) { continue; } - if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].First.AllowAlways) { continue; } + if (!unassigned[i].JobPreferences.Any() || !unassigned[i].JobPreferences[0].Prefab.AllowAlways) { continue; } unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); } @@ -3611,8 +3617,8 @@ namespace Barotrauma.Networking void AssignJob(Client client, JobPrefab jobPrefab) { client.AssignedJob = - client.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? - new Pair(jobPrefab, Rand.Int(jobPrefab.Variants)); + client.JobPreferences.FirstOrDefault(jp => jp.Prefab == jobPrefab) ?? + new JobVariant(jobPrefab, Rand.Int(jobPrefab.Variants)); assignedClientCount[jobPrefab]++; unassigned.Remove(client); @@ -3657,7 +3663,7 @@ namespace Barotrauma.Networking Client client = unassigned[i]; if (preferenceIndex >= client.JobPreferences.Count) { continue; } var preferredJob = client.JobPreferences[preferenceIndex]; - JobPrefab jobPrefab = preferredJob.First; + JobPrefab jobPrefab = preferredJob.Prefab; if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) { //can't assign this job if maximum number has reached or the clien't karma is too low @@ -3690,24 +3696,24 @@ namespace Barotrauma.Networking if (skips >= jobList.Count) { break; } } c.AssignedJob = - c.JobPreferences.FirstOrDefault(jp => jp.First == jobList[jobIndex]) ?? - new Pair(jobList[jobIndex], 0); - assignedClientCount[c.AssignedJob.First]++; + c.JobPreferences.FirstOrDefault(jp => jp.Prefab == jobList[jobIndex]) ?? + new JobVariant(jobList[jobIndex], 0); + assignedClientCount[c.AssignedJob.Prefab]++; } //if one of the client's preferences is still available, give them that job - else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.First))) + else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.Prefab))) { - foreach (Pair preferredJob in c.JobPreferences) + foreach (JobVariant preferredJob in c.JobPreferences) { c.AssignedJob = preferredJob; - assignedClientCount[preferredJob.First]++; + assignedClientCount[preferredJob.Prefab]++; break; } } else //none of the client's preferred jobs available, choose a random job { - c.AssignedJob = new Pair(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); - assignedClientCount[c.AssignedJob.First]++; + c.AssignedJob = new JobVariant(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); + assignedClientCount[c.AssignedJob.Prefab]++; } } } @@ -3751,11 +3757,11 @@ namespace Barotrauma.Networking { if (unassignedBots.Count == 0) { break; } - JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandom(); + JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandomUnsynced(); if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } - var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.Server); - unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.Server, variant); + var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.ServerAndClient); + unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.ServerAndClient, variant); assignedPlayerCount[jobPrefab]++; unassignedBots.Remove(unassignedBots[0]); canAssign = true; @@ -3768,15 +3774,16 @@ namespace Barotrauma.Networking //find all jobs that are still available var remainingJobs = JobPrefab.Prefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber); //all jobs taken, give a random job - if (remainingJobs.Count() == 0) + if (remainingJobs.None()) { DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); - c.Job = Job.Random(); + #warning TODO: is this randsync correct? + c.Job = Job.Random(Rand.RandSync.ServerAndClient); assignedPlayerCount[c.Job.Prefab]++; } else //some jobs still left, choose one of them by random { - var job = remainingJobs.GetRandom(); + var job = remainingJobs.GetRandomUnsynced(); var variant = Rand.Range(0, job.Variants); c.Job = new Job(job, Rand.RandSync.Unsynced, variant); assignedPlayerCount[c.Job.Prefab]++; @@ -3791,7 +3798,7 @@ namespace Barotrauma.Networking foreach (Client c in clients) { if (ServerSettings.KarmaEnabled && c.Karma < job.MinKarma) { continue; } - int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.First == job)); + int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.Prefab == job)); if (index > -1 && index < bestPreference) { bestPreference = index; @@ -3867,6 +3874,8 @@ namespace Barotrauma.Networking serverSettings.SaveSettings(); + ModSender.Dispose(); + if (serverSettings.SaveServerLogs) { Log("Shutting down the server...", ServerLog.MessageType.ServerMessage); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index a0bab780e..8fe44eb71 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -128,7 +128,7 @@ namespace Barotrauma clientMemory.PreviousNotifiedKarma >= KickBanThreshold + KarmaNotificationInterval && client.Karma < KickBanThreshold + KarmaNotificationInterval) { - GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client); + GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning").Value, client); GameServer.Log(GameServer.ClientLogName(client) + " has been warned for having dangerously low karma.", ServerLog.MessageType.Karma); clientMemory.PreviousNotifiedKarma = client.Karma; clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime; @@ -170,7 +170,7 @@ namespace Barotrauma existingAffliction.Strength = herpesStrength; if (herpesStrength <= 0.0f) { - client.Character.CharacterHealth.ReduceAffliction(null, "invertcontrols", 100.0f); + client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f); } } @@ -283,7 +283,7 @@ namespace Barotrauma if (foundItem == null) { return; } - bool isIdCard = foundItem.prefab.Identifier == "idcard"; + bool isIdCard = ((MapEntity)foundItem).Prefab.Identifier == "idcard"; bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; if (isIdCard) @@ -394,8 +394,8 @@ namespace Barotrauma } //attacking/healing clowns has a smaller effect on karma - if (target.HasEquippedItem("clownmask") && - target.HasEquippedItem("clowncostume")) + if (target.HasEquippedItem("clownmask".ToIdentifier()) && + target.HasEquippedItem("clowncostume".ToIdentifier())) { damage *= 0.5f; stun *= 0.5f; @@ -604,8 +604,8 @@ namespace Barotrauma if (client == null) { return; } //all penalties/rewards are halved when wearing a clown costume - if (target.HasEquippedItem("clownmask") && - target.HasEquippedItem("clowncostume")) + if (target.HasEquippedItem("clownmask".ToIdentifier()) && + target.HasEquippedItem("clowncostume".ToIdentifier())) { amount *= 0.5f; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index d50e6209f..b11c6bcf4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -179,7 +179,7 @@ namespace Barotrauma.Networking catch (Exception e) { string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString(); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { string errorMsg = "Failed to read server event for entity \"" + entityName + "\"!"; GameServer.Log(errorMsg + "\n" + e.StackTrace.CleanupStackTrace(), ServerLog.MessageType.Error); @@ -347,7 +347,7 @@ namespace Barotrauma.Networking count++; if (count > 3) { break; } } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { GameServer.Log(warningMsg, ServerLog.MessageType.Error); } @@ -482,7 +482,7 @@ namespace Barotrauma.Networking //skip the event if we've already received it if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1)) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Received msg " + thisEventID + ", expecting " + sender.LastSentEntityEventID, Color.Red); } @@ -493,7 +493,7 @@ namespace Barotrauma.Networking //entity not found -> consider the even read and skip over it //(can happen, for example, when a client uses a medical item repeatedly //and creates an event for it before receiving the event about it being removed) - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage( "Received msg " + thisEventID + ", entity " + entityID + " not found", @@ -504,7 +504,7 @@ namespace Barotrauma.Networking } else { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index 629a767cf..16cfd6683 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -129,7 +129,7 @@ namespace Barotrauma.Networking #if DEBUG DebugConsole.ThrowError(errorMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif } @@ -326,7 +326,7 @@ namespace Barotrauma.Networking } } - public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (netServer == null) { return; } @@ -353,7 +353,7 @@ namespace Barotrauma.Networking NetOutgoingMessage lidgrenMsg = netServer.CreateMessage(); byte[] msgData = new byte[msg.LengthBytes]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); lidgrenMsg.Write((byte)(isCompressed ? PacketHeader.IsCompressed : PacketHeader.None)); lidgrenMsg.Write((UInt16)length); lidgrenMsg.Write(msgData, 0, length); @@ -422,7 +422,7 @@ namespace Barotrauma.Networking { if (pendingClient.SteamID == null) { - bool requireSteamAuth = GameMain.Config.RequireSteamAuthentication; + bool requireSteamAuth = GameSettings.CurrentConfig.RequireSteamAuthentication; #if DEBUG requireSteamAuth = false; #endif diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 546ba9543..1e2d3e6d5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -123,7 +123,7 @@ namespace Barotrauma.Networking return; } - string language = inc.ReadString(); + LanguageIdentifier language = inc.ReadIdentifier().ToLanguageIdentifier(); pendingClient.Connection.Language = language; Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); @@ -246,12 +246,14 @@ namespace Barotrauma.Networking case ConnectionInitialization.ContentPackageOrder: outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); for (int i = 0; i < mpContentPackages.Count; i++) { outMsg.Write(mpContentPackages[i].Name); - outMsg.Write(mpContentPackages[i].MD5hash.Hash); + byte[] hashBytes = mpContentPackages[i].Hash.ByteRepresentation; + outMsg.WriteVariableUInt32((UInt32)hashBytes.Length); + outMsg.Write(hashBytes, 0, hashBytes.Length); outMsg.Write(mpContentPackages[i].SteamWorkshopId); UInt32 installTimeDiffSeconds = (UInt32)((mpContentPackages[i].InstallTime ?? DateTime.UtcNow) - DateTime.UtcNow).TotalSeconds; outMsg.Write(installTimeDiffSeconds); @@ -294,7 +296,7 @@ namespace Barotrauma.Networking } } - public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod); + public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void Disconnect(NetworkConnection conn, string msg = null); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 6af0c5221..2d1cb634c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -106,7 +106,7 @@ namespace Barotrauma.Networking #if DEBUG DebugConsole.ThrowError(errorMsg); #else - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif } @@ -223,7 +223,7 @@ namespace Barotrauma.Networking string ownerName = inc.ReadString(); OwnerConnection = new SteamP2PConnection(ownerName, OwnerSteamID) { - Language = GameMain.Config.Language + Language = GameSettings.CurrentConfig.Language }; OwnerConnection.SetOwnerSteamIDIfUnknown(OwnerSteamID); @@ -250,7 +250,7 @@ namespace Barotrauma.Networking throw new InvalidOperationException("Called InitializeSteamServerCallbacks on SteamP2PServerPeer!"); } - public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod) + public override void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) { if (!started) { return; } @@ -263,7 +263,7 @@ namespace Barotrauma.Networking IWriteMessage msgToSend = new WriteOnlyMessage(); byte[] msgData = new byte[16]; - msg.PrepareForSending(ref msgData, out bool isCompressed, out int length); + msg.PrepareForSending(ref msgData, compressPastThreshold, out bool isCompressed, out int length); msgToSend.Write(conn.SteamID); msgToSend.Write((byte)deliveryMethod); msgToSend.Write((byte)((isCompressed ? PacketHeader.IsCompressed : PacketHeader.None) | PacketHeader.IsServerMessage)); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index d81789f78..38252b58f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -227,7 +227,7 @@ namespace Barotrauma.Networking } var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null); - shuttleGaps.ForEach(g => Spawner.AddToRemoveQueue(g)); + shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g)); var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == RespawnShuttle && i.GetComponent() != null); dockingPorts.ForEach(d => d.GetComponent().Undock()); @@ -355,7 +355,7 @@ namespace Barotrauma.Networking { if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null) { - c.CharacterInfo.Job = new Job(c.AssignedJob.First, Rand.RandSync.Unsynced, c.AssignedJob.Second); + c.CharacterInfo.Job = new Job(c.AssignedJob.Prefab, Rand.RandSync.Unsynced, c.AssignedJob.Variant); } } @@ -369,17 +369,17 @@ namespace Barotrauma.Networking if ((shuttlePos != null && Level.Loaded.GetRealWorldDepth(shuttlePos.Value.Y) > Level.DefaultRealWorldCrushDepth) || Level.Loaded.GetRealWorldDepth(Submarine.MainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth) { - divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t.Equals("respawnsuitdeep", StringComparison.OrdinalIgnoreCase))); + divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuitdeep")); } if (divingSuitPrefab == null) { divingSuitPrefab = - ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t.Equals("respawnsuit", StringComparison.OrdinalIgnoreCase))) ?? - ItemPrefab.Find(null, "divingsuit"); + ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuit")) ?? + ItemPrefab.Find(null, "divingsuit".ToIdentifier()); } - ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank"); - ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter"); - ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell"); + ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank".ToIdentifier()); + ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter".ToIdentifier()); + ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell".ToIdentifier()); var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo); @@ -522,7 +522,7 @@ namespace Barotrauma.Networking if (characterInfo?.Job == null) { return; } foreach (Skill skill in characterInfo.Job.Skills) { - var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier.Equals(s.Identifier, StringComparison.OrdinalIgnoreCase)); + var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); if (skillPrefab == null) { continue; } skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.Start, SkillReductionOnCampaignMidroundRespawn); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 9f9e10a7c..334e62414 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -268,7 +268,7 @@ namespace Barotrauma.Networking doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); - doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => c.First + "-" + c.Second))); + doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => $"{c.Start}-{c.End}"))); SerializableProperty.SerializeProperties(this, doc.Root, true); @@ -307,7 +307,7 @@ namespace Barotrauma.Networking if (string.IsNullOrEmpty(doc.Root.GetAttributeString("losmode", ""))) { - LosMode = GameMain.Config.LosMode; + LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); @@ -370,7 +370,12 @@ namespace Barotrauma.Networking } } - if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Pair(min, max)); } + if (min > max) + { + //swap min and max + (min, max) = (max, min); + } + if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Range(min, max)); } } AllowedRandomMissionTypes = new List(); @@ -399,12 +404,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); GameMain.NetLobbyScreen.SetBotCount(BotCount); - List monsterNames = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList(); - MonsterEnabled = new Dictionary(); - foreach (string s in monsterNames) - { - if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true); - } + MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); } public string SelectNonHiddenSubmarine(string current = null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 5868b7235..290833c32 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -85,7 +85,7 @@ namespace Barotrauma string hash = equalityCheckVal > 0 ? string.Empty : inc.ReadString(); SubmarineInfo sub = equalityCheckVal > 0 ? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.EqualityCheckVal == equalityCheckVal) : - SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.MD5Hash.Hash == hash); + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == SubmarineType.Player && s.MD5Hash.StringRepresentation == hash); sender.SetVote(voteType, sub); break; case VoteType.Mode: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 97461c229..0712c25b6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -108,13 +108,10 @@ namespace Barotrauma sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); sb.AppendLine("\n"); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); - if (GameMain.Config != null) + sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); + if (ContentPackageManager.EnabledPackages.All != null) { - sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); - if (GameMain.Config.AllEnabledPackages != null) - { - sb.AppendLine("Selected content packages: " + (!GameMain.Config.AllEnabledPackages.Any() ? "None" : string.Join(", ", GameMain.Config.AllEnabledPackages.Select(c => c.Name)))); - } + sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); @@ -176,7 +173,8 @@ namespace Barotrauma File.WriteAllText(filePath, sb.ToString()); - if (GameSettings.SaveDebugConsoleLogs || GameSettings.VerboseLogging) { DebugConsole.SaveLogs(); } + if (GameSettings.CurrentConfig.SaveDebugConsoleLogs + || GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.SaveLogs(); } if (GameAnalyticsManager.SendUserStatistics) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index cc3baa6d0..aaccf295c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -54,14 +54,14 @@ namespace Barotrauma } } - public string SelectedModeIdentifier + public Identifier SelectedModeIdentifier { get { return GameModes[SelectedModeIndex].Identifier; } set { for (int i = 0; i < GameModes.Length; i++) { - if (GameModes[i].Identifier.ToLower() == value.ToLower()) + if (GameModes[i].Identifier == value) { SelectedModeIndex = i; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs similarity index 86% rename from Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs rename to Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index beb842ec4..1b2c09f73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -4,13 +4,11 @@ namespace Barotrauma.Steam { partial class SteamManager { - #region Server - - private static void InitializeProjectSpecific() { isInitialized = true; } + private static void InitializeProjectSpecific() { IsInitialized = true; } public static bool CreateServer(Networking.GameServer server, bool isPublic) { - isInitialized = true; + IsInitialized = true; Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") { @@ -39,26 +37,26 @@ namespace Barotrauma.Steam public static bool RefreshServerDetails(Networking.GameServer server) { - if (!isInitialized || !Steamworks.SteamServer.IsValid) + if (!IsInitialized || !Steamworks.SteamServer.IsValid) { return false; } - var contentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); // These server state variables may be changed at any time. Note that there is no longer a mechanism - // to send the player count. The player count is maintained by steam and you should use the player + // to send the player count. The player count is maintained by Steam and you should use the player // creation/authentication functions to maintain your player count. Steamworks.SteamServer.ServerName = server.ServerName; Steamworks.SteamServer.MaxPlayers = server.ServerSettings.MaxPlayers; Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; - Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName ?? ""; + Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? ""; Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString()); Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); - Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.MD5hash.Hash))); + Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId))); Steamworks.SteamServer.SetKey("usingwhitelist", (server.ServerSettings.Whitelist != null && server.ServerSettings.Whitelist.Enabled).ToString()); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); @@ -68,7 +66,7 @@ namespace Barotrauma.Steam Steamworks.SteamServer.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString()); Steamworks.SteamServer.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString()); - Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier); + Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value); Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString()); Steamworks.SteamServer.DedicatedServer = true; @@ -78,7 +76,7 @@ namespace Barotrauma.Steam public static Steamworks.BeginAuthResult StartAuthSession(byte[] authTicketData, ulong clientSteamID) { - if (!isInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; + if (!IsInitialized || !Steamworks.SteamServer.IsValid) return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID); Steamworks.BeginAuthResult startResult = Steamworks.SteamServer.BeginAuthSession(authTicketData, clientSteamID); @@ -92,7 +90,7 @@ namespace Barotrauma.Steam public static void StopAuthSession(ulong clientSteamID) { - if (!isInitialized || !Steamworks.SteamServer.IsValid) return; + if (!IsInitialized || !Steamworks.SteamServer.IsValid) return; DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamID); Steamworks.SteamServer.EndSession(clientSteamID); @@ -100,13 +98,11 @@ namespace Barotrauma.Steam public static bool CloseServer() { - if (!isInitialized || !Steamworks.SteamServer.IsValid) return false; + if (!IsInitialized || !Steamworks.SteamServer.IsValid) return false; Steamworks.SteamServer.Shutdown(); return true; } - - #endregion } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs index 93806e349..37894b947 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Goal.cs @@ -29,7 +29,8 @@ namespace Barotrauma public virtual IEnumerable CompletedTextKeys => new string[] { }; public virtual IEnumerable CompletedTextValues(Traitor traitor) => new string[] { }; - protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => TextManager.FormatServerMessageWithGenderPronouns(traitor?.Character?.Info?.Gender ?? Gender.None, textId, keys, values); + protected virtual string FormatText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) + => TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, textId, keys.Zip(values, (k,v) => (k,v)).ToArray()); protected internal virtual string GetStatusText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); protected internal virtual string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) => FormatText(traitor, textId, keys, values); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs index 17e4ae552..6fd116862 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -51,11 +51,11 @@ namespace Barotrauma { continue; } - var identifierMatches = matchIdentifier && item.prefab.Identifier == tag; + var identifierMatches = matchIdentifier && ((MapEntity)item).Prefab.Identifier == tag; if (identifierMatches && tagPrefabName == null) { var textId = item.Prefab.GetItemNameTextId(); - tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name; + tagPrefabName = textId != null ? TextManager.FormatServerMessage(textId) : item.Prefab.Name.Value; } if (identifierMatches || (matchTag && item.HasTag(tag))) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs index 5bfff56a5..e9977fa73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs @@ -28,7 +28,7 @@ namespace Barotrauma private enum EntityTypes { Character, Item } - private string[] entities; + private Identifier[] entities; private EntityTypes[] entityTypes; public override void Update(float deltaTime) @@ -67,7 +67,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase) && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) + if (character.SpeciesName == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) { activeEntity = character; transformationTime = 0.0; @@ -82,7 +82,7 @@ namespace Barotrauma { continue; } - if (item.prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) + if (((MapEntity)item).Prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) { activeEntity = item; transformationTime = 0.0; @@ -117,7 +117,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase)) + if (character.SpeciesName == entities[activeEntityIndex]) { activeEntity = character; break; @@ -131,7 +131,7 @@ namespace Barotrauma { continue; } - if (item.prefab.Identifier.Equals(entities[0], StringComparison.OrdinalIgnoreCase)) + if (((MapEntity)item).Prefab.Identifier == entities[0]) { activeEntity = item; break; @@ -146,7 +146,7 @@ namespace Barotrauma public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base() { - this.entities = entities; + this.entities = entities.ToIdentifiers().ToArray(); this.entityTypes = new EntityTypes[entityTypes.Length]; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs index 06f43f91b..57871e224 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs @@ -15,7 +15,7 @@ namespace Barotrauma private readonly bool preferNew; private readonly bool allowNew; private readonly bool allowExisting; - private readonly HashSet allowedContainerIdentifiers = new HashSet(); + private readonly HashSet allowedContainerIdentifiers = new HashSet(); private ItemPrefab targetPrefab; private ItemPrefab containedPrefab; @@ -83,7 +83,7 @@ namespace Barotrauma { continue; } - if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(item.prefab.Identifier)) + if (item.GetComponent() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier)) { if ((includeNew && !item.OwnInventory.IsFull()) || (includeExisting && item.OwnInventory.FindItemByIdentifier(targetPrefabCandidate.Identifier) != null)) { @@ -166,7 +166,7 @@ namespace Barotrauma targetPrefabTextId = targetPrefab.GetItemNameTextId(); } - targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name; + targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name.Value; targetContainer = FindTargetContainer(Traitors, targetPrefab); if (targetContainer == null) { @@ -175,9 +175,9 @@ namespace Barotrauma return false; } var containerPrefabTextId = targetContainer.Prefab.GetItemNameTextId(); - targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name; - var targetHullTextId = targetContainer.CurrentHull?.prefab.GetHullNameTextId(); - targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName ?? ""; + targetContainerNameText = containerPrefabTextId != null ? TextManager.FormatServerMessage(containerPrefabTextId) : targetContainer.Prefab.Name.Value; + var targetHullTextId = targetContainer.CurrentHull?.Prefab.GetHullNameTextId(); + targetHullNameText = targetHullTextId != null ? TextManager.FormatServerMessage(targetHullTextId) : targetContainer?.CurrentHull?.DisplayName.Value ?? ""; if (allowNew && !targetContainer.OwnInventory.IsFull()) { existingItems.Clear(); @@ -185,7 +185,7 @@ namespace Barotrauma { existingItems.Add(item); } - Entity.Spawner.AddToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item => + Entity.Spawner.AddItemToSpawnQueue(targetPrefab, targetContainer.OwnInventory, onSpawned: item => { item.AddTag("traitormissionitem"); }); @@ -216,7 +216,7 @@ namespace Barotrauma { for (int i = 0; i < spawnAmount; i++) { - Entity.Spawner.AddToSpawnQueue(containedPrefab, target.OwnInventory); + Entity.Spawner.AddItemToSpawnQueue(containedPrefab, target.OwnInventory); } } existingItems.Clear(); @@ -224,7 +224,7 @@ namespace Barotrauma } } - public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params string[] allowedContainerIdentifiers) + public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params Identifier[] allowedContainerIdentifiers) { this.filter = filter; this.identifier = identifier; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs index cceda84c4..31aad8c96 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs @@ -21,7 +21,7 @@ namespace Barotrauma base.Update(deltaTime); var validHullsCount = 0; var floodingAmount = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs index 8d4500fa8..32b024770 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs @@ -15,7 +15,7 @@ namespace Barotrauma private bool isCompleted; private const float gracePeriod = 1f; - private string speciesId; + private Identifier speciesId; private string targetCharacterName; private Character targetCharacter; private float timer; @@ -52,7 +52,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.Equals(speciesId, StringComparison.OrdinalIgnoreCase)) + if (character.SpeciesName == speciesId) { targetCharacter = character; break; @@ -64,9 +64,9 @@ namespace Barotrauma return targetCharacter != null; } - public GoalKeepTransformedAlive(string speciesId) : base() + public GoalKeepTransformedAlive(Identifier speciesId) : base() { - this.speciesId = speciesId.ToLowerInvariant(); + this.speciesId = speciesId; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs index ca486e419..6cb4c70e6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKillTarget.cs @@ -13,12 +13,12 @@ namespace Barotrauma public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" }); public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] - { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath(), targetHull != null ? TextManager.Get($"roomname.{targetHull}") : string.Empty }); + { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath().Value, targetHull != null ? TextManager.Get($"roomname.{targetHull}").Value : string.Empty }); private bool isCompleted = false; public override bool IsCompleted => isCompleted; - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); private CauseOfDeathType requiredCauseOfDeath; private string afflictionId; @@ -102,7 +102,7 @@ namespace Barotrauma return true; } - private string GetCauseOfDeath() + private LocalizedString GetCauseOfDeath() { if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs index 3ffb3953c..fe4edd2d4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs @@ -8,8 +8,8 @@ namespace Barotrauma { public class GoalReplaceInventory : HumanoidGoal { - private readonly HashSet sabotageContainerIds = new HashSet(); - private readonly HashSet validReplacementIds = new HashSet(); + private readonly HashSet sabotageContainerIds = new HashSet(); + private readonly HashSet validReplacementIds = new HashSet(); private readonly float replaceAmount; @@ -33,7 +33,7 @@ namespace Barotrauma { continue; } - if (sabotageContainerIds.Contains(item.prefab.Identifier)) + if (sabotageContainerIds.Contains(((MapEntity)item).Prefab.Identifier)) { ++totalAmount; if (item.OwnInventory.AllItems.All(containedItem => !validReplacementIds.Contains(containedItem.Prefab.Identifier))) @@ -59,7 +59,7 @@ namespace Barotrauma return true; } - public GoalReplaceInventory(string[] containerIds, string[] replacementIds, float replaceAmount) + public GoalReplaceInventory(Identifier[] containerIds, Identifier[] replacementIds, float replaceAmount) { sabotageContainerIds.UnionWith(containerIds); validReplacementIds.UnionWith(replacementIds); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs index 7c51dc64c..0175429b9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalSabotageItems.cs @@ -37,15 +37,10 @@ namespace Barotrauma targetItems.Add(item); } } - //only target items in the main sub if there are any - if (targetItems.Count > 1 && targetItems.Any(it => it.Submarine == Submarine.MainSub)) - { - targetItems.RemoveAll(it => it.Submarine != Submarine.MainSub); - } if (targetItems.Count > 0) { var textId = targetItems[0].Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name; + targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetItems[0].Prefab.Name.Value; } return targetItems.Count > 0; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs index bcc4717aa..2f1314d49 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalUnwiring.cs @@ -46,7 +46,7 @@ namespace Barotrauma if (targetConnectionPanels.Count > 0) { var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId(); - targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name; + targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name.Value; } return targetConnectionPanels.Count > 0; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs index c33841af9..f4b5d894d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasDuration.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Barotrauma @@ -14,12 +15,13 @@ namespace Barotrauma public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[duration]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString() }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { requiredDuration.ToString(CultureInfo.InvariantCulture) }); protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, new[] { "[infotext]", "[duration]" }, new[] { infoText, requiredDuration.ToString() }) : infoText; + return !string.IsNullOrEmpty(durationInfoTextId) && !infoText.Contains("[duration]") ? TextManager.FormatServerMessage(durationInfoTextId, + ("[infotext]", infoText), ("[duration]", requiredDuration.ToString(CultureInfo.InvariantCulture))) : infoText; } private bool isCompleted = false; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs index 48e38f060..f2603e594 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalHasTimeLimit.cs @@ -17,7 +17,7 @@ namespace Barotrauma protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, new[] { "[infotext]", "[timelimit]" }, new[] { infoText, $"{TimeSpan.FromSeconds(timeLimit):g}" }) : infoText; + return !string.IsNullOrEmpty(timeLimitInfoTextId) ? TextManager.FormatServerMessage(timeLimitInfoTextId, ("[infotext]", infoText), ("[timelimit]", $"{TimeSpan.FromSeconds(timeLimit):g}")) : infoText; } public override bool CanBeCompleted(ICollection traitors) => base.CanBeCompleted(traitors) && (!Traitors.Any(IsStarted) || timeRemaining > 0.0f); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs index e749f0a3f..22b5a0a74 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/GoalIsOptional.cs @@ -14,7 +14,7 @@ namespace Barotrauma public override IEnumerable StatusTextValues(Traitor traitor) { var values = base.StatusTextValues(traitor).ToArray(); - values[1] = TextManager.GetServerMessage(StatusValueTextId); + values[1] = TextManager.FormatServerMessage(StatusValueTextId); return values; } @@ -24,7 +24,7 @@ namespace Barotrauma protected internal override string GetInfoText(Traitor traitor, string textId, IEnumerable keys, IEnumerable values) { var infoText = base.GetInfoText(traitor, textId, keys, values); - return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, new[] { "[infotext]" }, new[] { infoText }) : infoText; + return !string.IsNullOrEmpty(optionalInfoTextId) ? TextManager.FormatServerMessage(optionalInfoTextId, ("[infotext]", infoText)) : infoText; } public GoalIsOptional(Goal goal, string optionalInfoTextId) : base(goal) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs index 522e5d661..2a48fe8ae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/Modifiers/Modifier.cs @@ -30,7 +30,7 @@ namespace Barotrauma } public override IEnumerable StatusTextKeys => Goal.StatusTextKeys; - public override IEnumerable StatusTextValues(Traitor traitor) => new [] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; + public override IEnumerable StatusTextValues(Traitor traitor) => new string[] { InfoText(traitor), TextManager.FormatServerMessage(StatusValueTextId) }; public override IEnumerable InfoTextKeys => Goal.InfoTextKeys; public override IEnumerable InfoTextValues(Traitor traitor) => Goal.InfoTextValues(traitor); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs index b8eaf65dc..8c09c73ef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Objective.cs @@ -39,7 +39,7 @@ namespace Barotrauma { var statusText = goal.StatusText(Traitor); var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}"; + return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; }).ToArray()), string.Join("", activeGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); @@ -49,7 +49,7 @@ namespace Barotrauma { var statusText = goal.StatusText(Traitor); var startIndex = statusText.LastIndexOf('/') + 1; - return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, new string[] { "[statustext]" }, new string[] { $"[{index}.st]" })}"; + return $"{statusText.Substring(0, startIndex)}[{index}.st]={statusText.Substring(startIndex)}/[{index}.sl]={TextManager.FormatServerMessage(GoalInfoFormatId, ("[statustext]", $"[{index}.st]"))}"; }).ToArray()), string.Join("", allGoals.Select((goal, index) => $"[{index}.sl]").ToArray())); @@ -57,13 +57,14 @@ namespace Barotrauma public virtual IEnumerable StartMessageKeys => new string[] { "[traitorgoalinfos]" }; public virtual IEnumerable StartMessageValues => new string[] { GoalInfos }; - public virtual string StartMessageText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageTextId, StartMessageKeys, StartMessageValues); + public virtual LocalizedString StartMessageText + => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageTextId, StartMessageKeys.Zip(StartMessageValues, (k,v) => (k,v)).ToArray()); public virtual string StartMessageServerTextId { get; set; } = "TraitorObjectiveStartMessageServer"; public virtual IEnumerable StartMessageServerKeys => StartMessageKeys.Concat(new string[] { "[traitorname]" }); public virtual IEnumerable StartMessageServerValues => StartMessageValues.Concat(new string[] { Traitor?.Character?.Name ?? "(unknown)" }); - public virtual string StartMessageServerText => TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, StartMessageServerTextId, StartMessageServerKeys, StartMessageServerValues); + public virtual LocalizedString StartMessageServerText => TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, StartMessageServerTextId, StartMessageServerKeys.Zip(StartMessageServerValues, (k,v) => (k,v)).ToArray()); public virtual string EndMessageSuccessTextId { get; set; } = "TraitorObjectiveEndMessageSuccess"; public virtual string EndMessageSuccessDeadTextId { get; set; } = "TraitorObjectiveEndMessageSuccessDead"; @@ -83,7 +84,7 @@ namespace Barotrauma var messageId = IsCompleted ? (traitorIsDead ? EndMessageSuccessDeadTextId : traitorIsDetained ? EndMessageSuccessDetainedTextId : EndMessageSuccessTextId) : (traitorIsDead ? EndMessageFailureDeadTextId : traitorIsDetained ? EndMessageFailureDetainedTextId : EndMessageFailureTextId); - return TextManager.FormatServerMessageWithGenderPronouns(Traitor?.Character?.Info?.Gender ?? Gender.None, messageId, EndMessageKeys.ToArray(), EndMessageValues.ToArray()); + return TextManager.FormatServerMessageWithPronouns(Traitor.Character.Info, messageId, EndMessageKeys.Zip(EndMessageValues, (k,v)=>(k,v)).ToArray()); } } @@ -133,21 +134,21 @@ namespace Barotrauma IsStarted = true; - traitor.SendChatMessageBox(StartMessageText, traitor.Mission?.Identifier); - traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission?.Identifier); + traitor.SendChatMessageBox(StartMessageText.Value, traitor.Mission.Identifier); + traitor.UpdateCurrentObjective(GoalInfos, traitor.Mission.Identifier); return true; } public void StartMessage() { - Traitor.SendChatMessage(StartMessageText, Traitor.Mission?.Identifier); + Traitor.SendChatMessage(StartMessageText.Value, Traitor.Mission.Identifier); } public void EndMessage() { - Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission?.Identifier); - Traitor.SendChatMessage(EndMessageText, Traitor.Mission?.Identifier); + Traitor.SendChatMessageBox(EndMessageText, Traitor.Mission.Identifier); + Traitor.SendChatMessage(EndMessageText, Traitor.Mission.Identifier); } public void Update(float deltaTime) @@ -170,12 +171,12 @@ namespace Barotrauma pendingGoals.RemoveAt(i); if (GameMain.Server != null) { - Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission?.Identifier); + Traitor.SendChatMessage(goal.CompletedText(Traitor), Traitor.Mission.Identifier); if (pendingGoals.Count > 0) { - Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission?.Identifier); + Traitor.SendChatMessageBox(goal.CompletedText(Traitor), Traitor.Mission.Identifier); } - Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission?.Identifier); + Traitor.UpdateCurrentObjective(GoalInfos, Traitor.Mission.Identifier); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index 7e42cff1b..ece1c941b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -22,37 +22,35 @@ namespace Barotrauma public delegate void MessageSender(string message); public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender) { - string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, new string[] { - "[codewords]", "[coderesponse]" - }, new string[] { - codeWords, codeResponse - }); + string greetingMessage = TextManager.FormatServerMessage(Mission.StartText, + ("[codewords]", codeWords), + ("[coderesponse]", codeResponse)); messageSender(greetingMessage); Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character); Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection); if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null) { - GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText, Mission?.Identifier, TraitorMessageType.ServerMessageBox); + GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText.Value, Mission.Identifier, TraitorMessageType.ServerMessageBox); } } - public void SendChatMessage(string serverText, string iconIdentifier) + public void SendChatMessage(string serverText, Identifier iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.Server); } - public void SendChatMessageBox(string serverText, string iconIdentifier) + public void SendChatMessageBox(string serverText, Identifier iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); GameMain.Server.SendTraitorMessage(traitorClient, serverText, iconIdentifier, TraitorMessageType.ServerMessageBox); } - public void UpdateCurrentObjective(string objectiveText, string iconIdentifier) + public void UpdateCurrentObjective(string objectiveText, Identifier iconIdentifier) { Client traitorClient = GameMain.Server.ConnectedClients.Find(c => c.Character == Character); Character.TraitorCurrentObjective = objectiveText; - GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective, iconIdentifier, TraitorMessageType.Objective); + GameMain.Server.SendTraitorMessage(traitorClient, Character.TraitorCurrentObjective.Value, iconIdentifier, TraitorMessageType.Objective); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index d35216f45..fe1c89f73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -169,7 +169,8 @@ namespace Barotrauma else { var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); - if (mission != null) { + if (mission != null) + { if (mission.CanBeStarted(server, this, CharacterTeamType.None)) { if (mission.Start(server, this, CharacterTeamType.None)) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index 1e16d660f..d2397ec46 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -43,7 +43,7 @@ namespace Barotrauma public string GlobalEndMessageFailureDeadTextId { get; private set; } public string GlobalEndMessageFailureDetainedTextId { get; private set; } - public readonly string Identifier; + public readonly Identifier Identifier; public virtual IEnumerable GlobalEndMessageKeys => new string[] { "[traitorname]", "[traitorgoalinfos]" }; public virtual IEnumerable GlobalEndMessageValues { @@ -60,7 +60,7 @@ namespace Barotrauma { get { - if (Traitors.Any() && allObjectives.Count > 0) + if (Traitors.Any() && allObjectives.Count > 0) { return TextManager.JoinServerMessages("\n", Traitors.Values.Select(traitor => @@ -71,7 +71,7 @@ namespace Barotrauma var messageId = isSuccess ? (traitorIsDead ? GlobalEndMessageSuccessDeadTextId : traitorIsDetained ? GlobalEndMessageSuccessDetainedTextId : GlobalEndMessageSuccessTextId) : (traitorIsDead ? GlobalEndMessageFailureDeadTextId : traitorIsDetained ? GlobalEndMessageFailureDetainedTextId : GlobalEndMessageFailureTextId); - return TextManager.FormatServerMessageWithGenderPronouns(traitor.Character?.Info?.Gender ?? Gender.None, messageId, GlobalEndMessageKeys.ToArray(), GlobalEndMessageValues.ToArray()); + return TextManager.FormatServerMessageWithPronouns(traitor.Character.Info, messageId, GlobalEndMessageKeys.Zip(GlobalEndMessageValues).ToArray()); }).ToArray()); } return ""; @@ -376,7 +376,7 @@ namespace Barotrauma } } - public TraitorMission(string identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) + public TraitorMission(Identifier identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) { Identifier = identifier; StartText = startText; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs index cecc35b87..a9aa33810 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMissionPrefab.cs @@ -9,38 +9,27 @@ namespace Barotrauma { class TraitorMissionPrefab { - public class TraitorMissionEntry + public class TraitorMissionEntry : Prefab { + public static PrefabCollection Prefabs => TraitorMissionPrefab.Prefabs; + public readonly TraitorMissionPrefab Prefab; public float SelectedWeight; - public TraitorMissionEntry(XElement element) + public TraitorMissionEntry(ContentXElement element, TraitorMissionsFile file) : base(file, element) { Prefab = new TraitorMissionPrefab(element); } - } - public static readonly List List = new List(); - public static void Init() - { - var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) continue; - - foreach (XElement element in doc.Root.Elements()) - { - List.Add(new TraitorMissionEntry(element)); - } - } + public override void Dispose() { } } + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static TraitorMissionPrefab RandomPrefab() { - var selected = ToolBox.SelectWeightedRandom(List, List.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); + var selected = ToolBox.SelectWeightedRandom(Prefabs.ToList(), Prefabs.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); //the weight of the missions that didn't get selected keeps growing the make them more likely to get picked - foreach (var mission in List) + foreach (var mission in Prefabs) { mission.SelectedWeight += 10; } @@ -103,7 +92,7 @@ namespace Barotrauma private delegate bool TargetFilter(string value, Character character); private static Dictionary targetFilters = new Dictionary() { - { "job", (value, character) => value.Equals(character.Info.Job.Prefab.Identifier, StringComparison.OrdinalIgnoreCase) }, + { "job", (value, character) => value == character.Info.Job.Prefab.Identifier }, { "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) } }; @@ -180,12 +169,29 @@ namespace Barotrauma itemCountFilters.Add((character) => filter(attribute.Value, character)); } } - goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), Config.GetAttributeString("identifier", null), Config.GetAttributeBool("preferNew", true), Config.GetAttributeBool("allowNew", true), Config.GetAttributeBool("allowExisting", true), Config.GetAttributeFloat("percentage", -1f), Config.GetAttributeStringArray("allowedContainers", new string[] {"steelcabinet", "mediumsteelcabinet", "suppliescabinet"})); + goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), + Config.GetAttributeString("identifier", + null), + Config.GetAttributeBool("preferNew", + true), + Config.GetAttributeBool("allowNew", + true), + Config.GetAttributeBool("allowExisting", + true), + Config.GetAttributeFloat("percentage", + -1f), + Config.GetAttributeIdentifierArray("allowedContainers", + new string[] + { + "steelcabinet", + "mediumsteelcabinet", + "suppliescabinet" + }.ToIdentifiers())); break; case "replaceinventory": checker.Required("containers", "replacements"); checker.Optional("percentage"); - goal = new Traitor.GoalReplaceInventory(Config.GetAttributeStringArray("containers", new string[] { }), Config.GetAttributeStringArray("replacements", new string[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); + goal = new Traitor.GoalReplaceInventory(Config.GetAttributeIdentifierArray("containers", new Identifier[] { }), Config.GetAttributeIdentifierArray("replacements", new Identifier[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f); break; case "reachdistancefromsub": checker.Optional("distance"); @@ -221,7 +227,7 @@ namespace Barotrauma break; case "keeptransformedalive": checker.Required("speciesname"); - goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeString("speciesname", null)); + goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeIdentifier("speciesname", Identifier.Empty)); break; default: GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error); @@ -425,7 +431,7 @@ namespace Barotrauma } public readonly Dictionary Roles = new Dictionary(); - public readonly string Identifier; + public readonly Identifier Identifier; public readonly string StartText; public readonly string EndMessageSuccessText; public readonly string EndMessageSuccessDeadText; @@ -575,14 +581,14 @@ namespace Barotrauma if (jobs != null) { var jobsSet = new HashSet(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture))); - filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower(CultureInfo.InvariantCulture))); + filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower().Value)); } return new Role(filters); } - public TraitorMissionPrefab(XElement missionRoot) + public TraitorMissionPrefab(ContentXElement missionRoot) { - Identifier = missionRoot.GetAttributeString("identifier", null); + Identifier = missionRoot.GetAttributeIdentifier("identifier", Identifier.Empty); foreach (var element in missionRoot.Elements()) { using (var checker = new AttributeChecker(element)) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ff47e4232..299d02f04 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,12 +6,13 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.16.7.0 + 0.17.0.0 Copyright © FakeFish 2018-2020 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 @@ -63,7 +64,6 @@ - diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml deleted file mode 100644 index 0dae570e8..000000000 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub new file mode 100644 index 0000000000000000000000000000000000000000..cdd96a32b9a24bd1cf245f725b8808d2b2afb6ae GIT binary patch literal 9564 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 + + + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/info.txt b/Barotrauma/BarotraumaShared/LocalMods/info.txt similarity index 97% rename from Barotrauma/BarotraumaShared/Mods/info.txt rename to Barotrauma/BarotraumaShared/LocalMods/info.txt index d3ce905ba..290565fb3 100644 --- a/Barotrauma/BarotraumaShared/Mods/info.txt +++ b/Barotrauma/BarotraumaShared/LocalMods/info.txt @@ -1,4 +1,5 @@ ------------------------------------------------------------------------- +TODO: THIS IS VERY OUTDATED +------------------------------------------------------------------------ General ------------------------------------------------------------------------ diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Humpback2.sub deleted file mode 100644 index 38b34627ae871fd94db734dd37cac14214edb888..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 340685 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 H9_ar8ABH2o diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png b/Barotrauma/BarotraumaShared/Mods/ExampleMod/PreviewImage.png deleted file mode 100644 index c76c81542e665ed3d30b49f37c3890639afed56f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 337280 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 - - - - \ 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 a5d3ca142..f13ea197e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -89,8 +89,8 @@ namespace Barotrauma set; } - public string SonarLabel; - public string SonarIconIdentifier; + public LocalizedString SonarLabel; + public Identifier SonarIconIdentifier; private bool inDetectable; @@ -172,13 +172,9 @@ namespace Barotrauma } SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f); string label = element.GetAttributeString("sonarlabel", ""); - SonarLabel = TextManager.Get(label, returnNull: true) ?? label; - SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); - string typeString = element.GetAttributeString("type", "Any"); - if (Enum.TryParse(typeString, out TargetType t)) - { - Type = t; - } + SonarLabel = TextManager.Get(label).Fallback(label); + SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", Identifier.Empty); + Type = element.GetAttributeEnum("type", TargetType.Any); Reset(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 4b2dc74dc..14c4333de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -239,7 +239,7 @@ namespace Barotrauma { throw new Exception($"Tried to create an enemy ai controller for human!"); } - if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase)) + if (Character.Params.Group == "human") { // Pet Character.TeamID = CharacterTeamType.FriendlyNPC; @@ -252,7 +252,7 @@ namespace Barotrauma List aiElements = new List(); List aiCommonness = new List(); - foreach (XElement element in mainElement.Elements()) + foreach (var element in mainElement.Elements()) { if (!element.Name.ToString().Equals("ai", StringComparison.OrdinalIgnoreCase)) { continue; } aiElements.Add(element); @@ -270,12 +270,12 @@ namespace Barotrauma //choose a random ai element MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random); - foreach (XElement subElement in aiElement.Elements()) + foreach (var subElement in aiElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "chooserandom": - LoadSubElement(subElement.Elements().GetRandom(random)); + LoadSubElement(subElement.Elements().ToArray().GetRandom(random)); break; default: LoadSubElement(subElement); @@ -330,12 +330,13 @@ namespace Barotrauma return _aiParams; } } - private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false); + private CharacterParams.TargetParams GetTargetParams(string targetTag) => GetTargetParams(targetTag.ToIdentifier()); + private CharacterParams.TargetParams GetTargetParams(Identifier targetTag) => AIParams.GetTarget(targetTag, false); private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget)); - private string GetTargetingTag(AITarget aiTarget) + private Identifier GetTargetingTag(AITarget aiTarget) { - if (aiTarget?.Entity == null) { return null; } - string targetingTag = null; + if (aiTarget?.Entity == null) { return Identifier.Empty; } + string targetingTag = string.Empty; if (aiTarget.Entity is Character targetCharacter) { if (targetCharacter.IsDead) @@ -407,7 +408,7 @@ namespace Barotrauma { targetingTag = "room"; } - return targetingTag; + return targetingTag.ToIdentifier(); } public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -683,7 +684,7 @@ namespace Barotrauma //if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState //otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it //TODO: a way for pets to differentiate hostile and friendly humans? - if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase)) + if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget)) { // Attack the character that attacked the target we are protecting ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); @@ -999,7 +1000,7 @@ namespace Barotrauma hullWeights.Clear(); float hullMinSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 2); bool checkWaterLevel = !AIParams.PatrolFlooded || !AIParams.PatrolDry; - foreach (var hull in Hull.hullList) + foreach (var hull in Hull.HullList) { if (hull.Submarine == null) { continue; } if (hull.Submarine.TeamID != Character.Submarine.TeamID) { continue; } @@ -2004,7 +2005,7 @@ namespace Barotrauma bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine; - if (State == AIState.Attack && !IsAttackRunning && !IsCoolDownRunning) + if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning)) { // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; @@ -2324,7 +2325,7 @@ namespace Barotrauma if (item.Condition <= 0.0f) { if (!wasBroken) { PetBehavior?.OnEat(item); } - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } } } @@ -2438,7 +2439,7 @@ namespace Barotrauma if (targetCharacter == Character) { continue; } float valueModifier = 1; - string targetingTag = GetTargetingTag(aiTarget); + Identifier targetingTag = GetTargetingTag(aiTarget); if (targetCharacter != null) { // ignore if target is tagged to be explicitly ignored (Feign Death) @@ -2535,7 +2536,7 @@ namespace Barotrauma if (s.Submarine == null) { continue; } if (s.Submarine.Info.IsRuin) { continue; } bool isCharacterInside = Character.CurrentHull != null; - bool isInnerWall = s.prefab.Tags.Contains("inner"); + bool isInnerWall = s.Prefab.Tags.Contains("inner"); if (isInnerWall && !isCharacterInside) { // Ignore inner walls when outside (walltargets still work) @@ -3141,7 +3142,7 @@ namespace Barotrauma if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } if (Character.Submarine == null) { - if (w.prefab.Tags.Contains("inner")) + if (w.Prefab.Tags.Contains("inner")) { if (!Character.AnimController.CanEnterSubmarine) { return false; } } @@ -3321,10 +3322,13 @@ namespace Barotrauma inactiveTriggers.Clear(); } + private bool TryResetOriginalState(string tag) => + TryResetOriginalState(tag.ToIdentifier()); + ///

/// Resets the target's state to the original value defined in the xml. /// - private bool TryResetOriginalState(string tag) + private bool TryResetOriginalState(Identifier tag) { if (!modifiedParams.ContainsKey(tag)) { return false; } if (AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) @@ -3344,8 +3348,8 @@ namespace Barotrauma } } - private readonly Dictionary modifiedParams = new Dictionary(); - private readonly Dictionary tempParams = new Dictionary(); + private readonly Dictionary modifiedParams = new Dictionary(); + private readonly Dictionary tempParams = new Dictionary(); private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null) { @@ -3369,6 +3373,9 @@ namespace Barotrauma } private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false) + => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting); + + private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false) { if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 7438c42bf..79879e835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -833,10 +833,9 @@ namespace Barotrauma suitableContainer = null; if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i => { - if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; } + if (!i.HasAccess(character)) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } - if (!container.HasAccess(character)) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } var rootContainer = container.Item.GetRootContainer(); if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } @@ -888,21 +887,21 @@ namespace Barotrauma { if (!target.IsArrested && AddTargets(Character, target) && newOrder == null) { - var orderPrefab = Order.GetPrefab("reportintruders"); + var orderPrefab = OrderPrefab.Prefabs["reportintruders"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; if (target.IsEscorted) { if (!Character.IsPrisoner && target.IsPrisoner) { - string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true); - Character.Speak(msg, ChatMessageType.Order); + LocalizedString msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", ("[roomname]", targetHull.DisplayName, FormatCapitals.No)); + Character.Speak(msg.Value, ChatMessageType.Order); speak = false; } else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable) { - string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true); - Character.Speak(msg, ChatMessageType.Order); + LocalizedString msg = TextManager.GetWithVariables("orderdialog.mentalcase", ("[roomname]", targetHull.DisplayName, FormatCapitals.No)); + Character.Speak(msg.Value, ChatMessageType.Order); speak = false; } } @@ -913,14 +912,14 @@ namespace Barotrauma { if (AddTargets(Character, hull) && newOrder == null) { - var orderPrefab = Order.GetPrefab("reportfire"); + var orderPrefab = OrderPrefab.Prefabs["reportfire"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } } if (IsBallastFloraNoticeable(Character, hull) && newOrder == null) { - var orderPrefab = Order.GetPrefab("reportballastflora"); + var orderPrefab = OrderPrefab.Prefabs["reportballastflora"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } @@ -932,7 +931,7 @@ namespace Barotrauma { if (AddTargets(Character, gap) && newOrder == null && !gap.IsRoomToRoom) { - var orderPrefab = Order.GetPrefab("reportbreach"); + var orderPrefab = OrderPrefab.Prefabs["reportbreach"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } @@ -947,7 +946,7 @@ namespace Barotrauma { if (AddTargets(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { - var orderPrefab = Order.GetPrefab("requestfirstaid"); + var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"]; newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); targetHull = hull; } @@ -961,7 +960,7 @@ namespace Barotrauma if (!item.Repairables.Any(r => r.IsBelowRepairIconThreshold)) { continue; } if (AddTargets(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective()) { - var orderPrefab = Order.GetPrefab("reportbrokendevices"); + var orderPrefab = OrderPrefab.Prefabs["reportbrokendevices"]; newOrder = new Order(orderPrefab, hull, item.Repairables?.FirstOrDefault(), orderGiver: Character); targetHull = hull; } @@ -978,15 +977,18 @@ namespace Barotrauma { if (Character.TeamID == CharacterTeamType.FriendlyNPC) { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default, - identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, + identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) { - Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); + Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder + .WithManualPriority(CharacterInfo.HighestManualOrderPriority) + .WithTargetEntity(targetHull) + .WithOrderGiver(Character), "", null, Character)); #endif } } @@ -1025,17 +1027,17 @@ namespace Barotrauma if (Character.Oxygen < 20.0f) { - Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f); + Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); } if (Character.Bleeding > 2.0f) { - Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f); + Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); } if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) { - Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f); + Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); } } @@ -1191,21 +1193,21 @@ namespace Barotrauma case AIObjectiveCombat.CombatMode.Retreat: if (Character.IsSecurity) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } else { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("DialogAttackedByFriendly").Value, null, 0.5f, "attackedbyfriendly".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } break; case AIObjectiveCombat.CombatMode.Offensive: case AIObjectiveCombat.CombatMode.Arrest: - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest").Value, null, 0.5f, "attackedbyfriendlysecurityarrest".ToIdentifier(), minDurationBetweenSimilar: 10.0f); break; case AIObjectiveCombat.CombatMode.None: if (Character.IsSecurity && realDamage > 1) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f); + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse").Value, null, 0.5f, "attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f); } break; } @@ -1415,14 +1417,14 @@ namespace Barotrauma } } - public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak = true) + public void SetOrder(Order order, bool speak = true) { - objectiveManager.SetOrder(order, option, priority, orderGiver, speak); + objectiveManager.SetOrder(order, speak); } - public void SetForcedOrder(Order order, string option, Character orderGiver) + public void SetForcedOrder(Order order) { - var objective = ObjectiveManager.CreateObjective(order, option, orderGiver); + var objective = ObjectiveManager.CreateObjective(order); ObjectiveManager.SetForcedOrder(objective); } @@ -1528,18 +1530,17 @@ namespace Barotrauma /// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second. /// To solve this, create a copy of the collection or change the code so that you first handle the first items and only after that query for the next items. /// - public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func predicate = null) + public static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable items, Identifier containedTag = default, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func predicate = null) { matchingItems.Clear(); items = matchingItems; - if (character == null) { return false; } - if (character.Inventory == null) { return false; } + if (character?.Inventory == null) { return false; } matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) && i.ConditionPercentage >= conditionPercentage && (!requireEquipped || character.HasEquippedItem(i)) && (predicate == null || predicate(i)), recursive, matchingItems); items = matchingItems; - return matchingItems.Any(i => i != null && (containedTag == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); } public static void StructureDamaged(Structure structure, float damageAmount, Character character) @@ -1594,7 +1595,7 @@ namespace Barotrauma (otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f); } } - otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning"), null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls", 10.0f); + otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f); someoneSpoke = true; } // React if we are security @@ -1674,7 +1675,7 @@ namespace Barotrauma GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); } item.StolenDuringRound = true; - otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); + otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f); someoneSpoke = true; #if CLIENT HintManager.OnStoleItem(thief, item); @@ -1749,7 +1750,7 @@ namespace Barotrauma public static void RefreshTargets(Character character, Order order, Hull hull) { - switch (order.Identifier) + switch (order.Identifier.Value.ToLowerInvariant()) { case "reportfire": AddTargets(character, hull); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 2e651a325..83bcb0c42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -346,7 +346,7 @@ namespace Barotrauma } Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; - bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1); + bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && steering.Y > 1); if (useLadders && character.SelectedConstruction != ladders.Item) { if (character.CanInteractWith(ladders.Item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs index 3f144bad8..3b74d869d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/MentalStateManager.cs @@ -128,7 +128,7 @@ namespace Barotrauma possibleTarget => HumanAIController.IsActive(possibleTarget) && (possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) && humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) && - possibleTarget != character).GetRandom(); + possibleTarget != character).GetRandomUnsynced(); if (mentalAttackTarget == null) { @@ -154,8 +154,8 @@ namespace Barotrauma // using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that mentalBehaviorTimer = MentalBehaviorInterval; humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f); - string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}"; - character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f); + Identifier textIdentifier = $"dialogmentalstatereaction{combatMode}".ToIdentifier(); + character.Speak(TextManager.Get(textIdentifier).Value, delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f); if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange)) { @@ -169,8 +169,8 @@ namespace Barotrauma public void CreateDialogueBehavior(MentalType mentalType) { if (mentalType == MentalType.Normal) { return; } - string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}"; - character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f); + Identifier textIdentifier = $"dialogmentalstate{mentalType}".ToIdentifier(); + character.Speak(TextManager.Get(textIdentifier).Value, delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 39f35025e..53cec3b04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -3,182 +3,91 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma { + class NPCConversationCollection : Prefab + { + public static readonly Dictionary> Collections = new Dictionary>(); + + public readonly LanguageIdentifier Language; + + public readonly List Conversations; + public readonly Dictionary PersonalityTraits; + + public NPCConversationCollection(NPCConversationsFile file, ContentXElement element) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Language = element.GetAttributeIdentifier("language", "English").ToLanguageIdentifier(); + Conversations = new List(); + PersonalityTraits = new Dictionary(); + foreach (var subElement in element.Elements()) + { + Identifier elemName = new Identifier(subElement.Name.LocalName); + if (elemName == "Conversation") + { + Conversations.Add(new NPCConversation(subElement)); + } + else if (elemName == "PersonalityTrait") + { + var personalityTrait = new NPCPersonalityTrait(subElement); + PersonalityTraits.Add(personalityTrait.Name, personalityTrait); + } + } + } + + public override void Dispose() { } + } + class NPCConversation { const int MaxPreviousConversations = 20; - private class ConversationCollection - { - public readonly string Identifier; - - public readonly Dictionary> Conversations; - - public ConversationCollection(string identifier) - { - Identifier = identifier; - Conversations = new Dictionary>(); - } - - public void Add(string language, string filePath, XElement subElement) - { - if (!Conversations.ContainsKey(language)) - { - Conversations.Add(language, new List()); - } - Conversations[language].Add(new NPCConversation(subElement, filePath)); - } - - public void RemoveByFile(string filePath) - { - List keysToRemove = new List(); - foreach (var kpv in Conversations) - { - kpv.Value.RemoveAll(c => c.FilePath == filePath); - if (kpv.Value.Count == 0) { keysToRemove.Add(kpv.Key); } - } - - foreach (var key in keysToRemove) - { - Conversations.Remove(key); - } - } - } - - private static Dictionary allConversations = new Dictionary(); - - public readonly string FilePath; - public readonly string Line; - public readonly List AllowedJobs; + public readonly ImmutableHashSet AllowedJobs; - public readonly List Flags; + public readonly ImmutableHashSet Flags; //The line can only be selected when eventmanager intensity is between these values //null = no restriction - public float? maxIntensity, minIntensity; + public readonly float? maxIntensity, minIntensity; - public readonly List Responses; + public readonly ImmutableArray Responses; private readonly int speakerIndex; - private readonly List allowedSpeakerTags; + private readonly ImmutableHashSet allowedSpeakerTags; private readonly bool requireNextLine; // used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere) private readonly bool requireSight; - public static void LoadAll(IEnumerable files) + public NPCConversation(XElement element) { - foreach (var file in files) - { - if (Path.GetExtension(file.Path) == ".csv") continue; // .csv files are not supported - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - string language = doc.Root.GetAttributeString("Language", "English"); - string identifier = doc.Root.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"Conversations file '{file.Path}' has no identifier!"); - return; - } - - foreach (XElement subElement in doc.Root.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "conversation": - if (!allConversations.ContainsKey(identifier)) - { - allConversations.Add(identifier, new ConversationCollection(identifier)); - } - allConversations[identifier].Add(language, file.Path, subElement); - break; - case "personalitytrait": - new NPCPersonalityTrait(subElement, file.Path); - break; - } - } - } - - public static void RemoveByFile(string filePath) - { - List keysToRemove = new List(); - foreach (var kpv in allConversations) - { - kpv.Value.RemoveByFile(filePath); - if (!kpv.Value.Conversations.Any()) - { - keysToRemove.Add(kpv.Key); - } - } - - foreach (string key in keysToRemove) - { - allConversations.Remove(key); - } - - NPCPersonalityTrait.List.RemoveAll(npt => npt.FilePath == filePath); - } - - public NPCConversation(XElement element, string filePath) - { - FilePath = filePath; - Line = element.GetAttributeString("line", ""); speakerIndex = element.GetAttributeInt("speaker", 0); - AllowedJobs = new List(); - string allowedJobsStr = element.GetAttributeString("allowedjobs", ""); - foreach (string allowedJobIdentifier in allowedJobsStr.Split(',')) - { - string key = allowedJobIdentifier.ToLowerInvariant(); - if (JobPrefab.Prefabs.ContainsKey(key)) - { - AllowedJobs.Add(JobPrefab.Prefabs[key]); - } - } - - Flags = new List(element.GetAttributeStringArray("flags", new string[0])); - - allowedSpeakerTags = new List(); - string allowedSpeakerTagsStr = element.GetAttributeString("speakertags", ""); - foreach (string tag in allowedSpeakerTagsStr.Split(',')) - { - if (string.IsNullOrEmpty(tag)) continue; - allowedSpeakerTags.Add(tag.Trim().ToLowerInvariant()); - } + AllowedJobs = element.GetAttributeIdentifierArray("allowedjobs", Array.Empty()).ToImmutableHashSet(); + Flags = element.GetAttributeIdentifierArray("flags", Array.Empty()).ToImmutableHashSet(); + allowedSpeakerTags = element.GetAttributeIdentifierArray("speakertags", Array.Empty()).ToImmutableHashSet(); if (element.Attribute("minintensity") != null) minIntensity = element.GetAttributeFloat("minintensity", 0.0f); if (element.Attribute("maxintensity") != null) maxIntensity = element.GetAttributeFloat("maxintensity", 1.0f); - Responses = new List(); - foreach (XElement subElement in element.Elements()) - { - Responses.Add(new NPCConversation(subElement, filePath)); - } + Responses = element.Elements().Select(s => new NPCConversation(s)).ToImmutableArray(); requireNextLine = element.GetAttributeBool("requirenextline", false); requireSight = element.GetAttributeBool("requiresight", false); } - private static List GetCurrentFlags(Character speaker) + private static List GetCurrentFlags(Character speaker) { - var currentFlags = new List(); - if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep"); } + var currentFlags = new List(); + if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); } if (GameMain.GameSession != null && Level.Loaded != null) { if (Level.Loaded.Type == LevelData.LevelType.LocationConnection) { - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial"); } + if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial".ToIdentifier()); } } else if (Level.Loaded.Type == LevelData.LevelType.Outpost) { @@ -187,30 +96,30 @@ namespace Barotrauma (speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { - currentFlags.Add("EnterOutpost"); + currentFlags.Add("EnterOutpost".ToIdentifier()); } } if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f) { - currentFlags.Add("Casual"); + currentFlags.Add("Casual".ToIdentifier()); } if (GameMain.GameSession.IsCurrentLocationRadiated()) { - currentFlags.Add("InRadiation"); + currentFlags.Add("InRadiation".ToIdentifier()); } } if (speaker != null) { - if (speaker.AnimController.InWater) { currentFlags.Add("Underwater"); } - currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside"); + if (speaker.AnimController.InWater) { currentFlags.Add("Underwater".ToIdentifier()); } + currentFlags.Add((speaker.CurrentHull == null ? "Outside" : "Inside").ToIdentifier()); if (Character.Controlled != null) { if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null) { - currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf"); + currentFlags.Add((speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf").ToIdentifier()); } } @@ -218,7 +127,7 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag)) + if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); } @@ -226,27 +135,27 @@ namespace Barotrauma if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) { - currentFlags.Add("OutpostNPC"); + currentFlags.Add("OutpostNPC".ToIdentifier()); } if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None) { - currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType); + currentFlags.Add($"CampaignNPC.{speaker.CampaignInteractionType}".ToIdentifier()); } if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && - (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)) + (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned")) { if (speaker.TeamID == CharacterTeamType.None) { - currentFlags.Add("Bandit"); + currentFlags.Add("Bandit".ToIdentifier()); } else if (speaker.TeamID == CharacterTeamType.FriendlyNPC) { - currentFlags.Add("Hostage"); + currentFlags.Add("Hostage".ToIdentifier()); } } if (speaker.IsEscorted) { - currentFlags.Add("escort"); + currentFlags.Add("escort".ToIdentifier()); } } @@ -261,16 +170,16 @@ namespace Barotrauma List> lines = new List>(); CreateConversation(availableSpeakers, assignedSpeakers, null, lines, - availableConversations: allConversations.Values.SelectMany(cc => cc.Conversations.Where(kpv => kpv.Key == TextManager.Language).SelectMany(kpv => kpv.Value)).ToList()); + availableConversations: NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].SelectMany(cc => cc.Conversations).ToList()); return lines; } - public static List> CreateRandom(List availableSpeakers, IEnumerable requiredFlags) + public static List> CreateRandom(List availableSpeakers, IEnumerable requiredFlags) { Dictionary assignedSpeakers = new Dictionary(); List> lines = new List>(); - var availableConversations = allConversations.Values.SelectMany(cc => cc.Conversations.SelectMany( - kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList(); + var availableConversations = NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language] + .SelectMany(cc => cc.Conversations.Where(c => requiredFlags.All(f => c.Flags.Contains(f)))).ToList(); if (availableConversations.Count > 0) { CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: false); @@ -282,11 +191,11 @@ namespace Barotrauma List availableSpeakers, Dictionary assignedSpeakers, NPCConversation baseConversation, - List> lineList, - List availableConversations, + IList> lineList, + IList availableConversations, bool ignoreFlags = false) { - List conversations = baseConversation == null ? availableConversations : baseConversation.Responses; + IList conversations = baseConversation == null ? availableConversations : baseConversation.Responses; if (conversations.Count == 0) { return; } int conversationIndex = Rand.Int(conversations.Count); @@ -390,7 +299,8 @@ namespace Barotrauma //check if the character has an appropriate job to say the line if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0) { - if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; } + if (!(potentialSpeaker.Info?.Job?.Prefab is { } speakerJobPrefab) + || !selectedConversation.AllowedJobs.Contains(speakerJobPrefab.Identifier)) { return false; } } //check if the character has all required flags to say the line @@ -450,17 +360,13 @@ namespace Barotrauma { System.Text.StringBuilder sb = new System.Text.StringBuilder(); - foreach (string key in allConversations.Keys) + foreach (Identifier identifier in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language].Keys) { - foreach (string lang in allConversations[key].Conversations.Keys) + foreach (var current in NPCConversationCollection.Collections[GameSettings.CurrentConfig.Language][identifier].Conversations) { - if (lang != TextManager.Language) { continue; } - foreach (var current in allConversations[key].Conversations[lang]) - { - WriteConversation(sb, current, 0); - WriteSubConversations(sb, current.Responses, 1); - WriteEmptyRow(sb); - } + WriteConversation(sb, current, 0); + WriteSubConversations(sb, current.Responses, 1); + WriteEmptyRow(sb); } } @@ -480,15 +386,7 @@ namespace Barotrauma sb.Append(string.Join(",", conv.Flags)); // Flags sb.Append('*'); - for (int i = 0; i < conv.AllowedJobs.Count; i++) // Jobs - { - sb.Append(conv.AllowedJobs[i].Identifier); - - if (i < conv.AllowedJobs.Count - 1) - { - sb.Append(","); - } - } + sb.Append(string.Join(',', conv.AllowedJobs)); sb.Append('*'); sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits @@ -501,13 +399,13 @@ namespace Barotrauma sb.AppendLine(); } - private static void WriteSubConversations(System.Text.StringBuilder sb, List responses, int depthIndex) + private static void WriteSubConversations(System.Text.StringBuilder sb, IList responses, int depthIndex) { for (int i = 0; i < responses.Count; i++) { WriteConversation(sb, responses[i], depthIndex); - if (responses[i].Responses != null && responses[i].Responses.Count > 0) + if (responses[i].Responses != null && responses[i].Responses.Length > 0) { WriteSubConversations(sb, responses[i].Responses, depthIndex + 1); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 2a76c67aa..5e618f9e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -10,8 +10,8 @@ namespace Barotrauma { public virtual float Devotion => AIObjectiveManager.baseDevotion; - public abstract string Identifier { get; set; } - public virtual string DebugTag => Identifier; + public abstract Identifier Identifier { get; set; } + public virtual string DebugTag => Identifier.Value; public virtual bool ForceRun => false; public virtual bool IgnoreUnsafeHulls => false; public virtual bool AbandonWhenCannotCompleteSubjectives => true; @@ -83,7 +83,7 @@ namespace Barotrauma public readonly Character character; public readonly AIObjectiveManager objectiveManager; - public string Option { get; private set; } + public readonly Identifier Option; private bool _abandon; public bool Abandon @@ -157,11 +157,11 @@ namespace Barotrauma return subObjective == null ? this : subObjective.GetActiveObjective(); } - public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null) + public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default) { this.objectiveManager = objectiveManager; this.character = character; - Option = option ?? string.Empty; + Option = option; PriorityModifier = priorityModifier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index ae6eb6ead..b76ebfaa3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -9,11 +9,11 @@ namespace Barotrauma { class AIObjectiveChargeBatteries : AIObjectiveLoop { - public override string Identifier { get; set; } = "charge batteries"; + public override Identifier Identifier { get; set; } = "charge batteries".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; private IEnumerable batteryList; - public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier) + public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier) : base(character, objectiveManager, priorityModifier, option) { } protected override bool Filter(PowerContainer battery) @@ -55,7 +55,7 @@ namespace Barotrauma { if (character == null || character.Submarine == null) { - return new PowerContainer[0]; + return Array.Empty(); } batteryList = character.Submarine.GetItems(true).Select(i => i.GetComponent()).Where(b => b != null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 007aee4a5..dfc2efe6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveCleanupItem : AIObjective { - public override string Identifier { get; set; } = "cleanup item"; + public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; @@ -61,21 +61,6 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (item.IgnoreByAI(character)) - { - Abandon = true; - return; - } - if (item.ParentInventory != null) - { - if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) - { - // Target was picked up or moved by someone. - Abandon = true; - return; - } - } - // Only continue when the get item sub objectives have been completed. if (subObjectives.Any()) { return; } if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) { @@ -133,7 +118,24 @@ namespace Barotrauma } } - protected override bool CheckObjectiveSpecific() => IsCompleted; + protected override bool CheckObjectiveSpecific() + { + if (item.IgnoreByAI(character)) + { + Abandon = true; + return false; + } + if (item.ParentInventory != null) + { + if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder())) + { + // Target was picked up or moved by someone. + Abandon = true; + return false; + } + } + return IsCompleted; + } public override void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index b84bf63ba..79732e006 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveCleanupItems : AIObjectiveLoop { - public override string Identifier { get; set; } = "cleanup items"; + public override Identifier Identifier { get; set; } = "cleanup items".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; protected override bool ForceOrderPriority => false; @@ -79,18 +79,16 @@ namespace Barotrauma public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => allowUnloading && - !container.IgnoreByAI(character) && - container.IsInteractable(character) && + container.HasAccess(character) && container.HasTag("allowcleanup") && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && - container.GetComponent() is ItemContainer itemContainer && itemContainer.HasAccess(character) && + container.GetComponent() != null && IsItemInsideValidSubmarine(container, character); public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { if (item == null) { return false; } - if (item.IgnoreByAI(character)) { return false; } - if (!item.IsInteractable(character)) { return false; } + if (!item.HasAccess(character)) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.ParentInventory != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 3de6f2a28..4f89fe0ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveCombat : AIObjective { - public override string Identifier { get; set; } = "combat"; + public override Identifier Identifier { get; set; } = "combat".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; @@ -250,17 +250,17 @@ namespace Barotrauma case CombatMode.Offensive: if (TargetEliminated && objectiveManager.IsCurrentOrder()) { - character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f); + character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f); } break; case CombatMode.Arrest: - if (HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true)) + if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true)) { IsCompleted = true; } else if (Enemy.IsKnockedDown && !objectiveManager.IsCurrentObjective() && - !HumanAIController.HasItem(character, "handlocker", out _, requireEquipped: false)) + !HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false)) { IsCompleted = true; } @@ -399,7 +399,7 @@ namespace Barotrauma RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref followTargetObjective); TryAddSubObjective(ref seekWeaponObjective, - constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false) + constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false) { AllowStealing = HumanAIController.IsMentallyUnstable, EvaluateCombatPriority = false, // Use a custom formula instead @@ -636,7 +636,7 @@ namespace Barotrauma // If there's an item container that takes a battery, // assume that it's required for the stun effect // as we can't check the status effect conditions here. - var mobileBatteryTag = "mobilebattery"; + var mobileBatteryTag = "mobilebattery".ToIdentifier(); var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); @@ -848,7 +848,7 @@ namespace Barotrauma if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown) { - if (HumanAIController.HasItem(character, "handlocker", out _)) + if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _)) { if (!arrestingRegistered) { @@ -861,10 +861,10 @@ namespace Barotrauma { if (character.TeamID == CharacterTeamType.FriendlyNPC) { - ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs"); + ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier()); if (prefab != null) { - Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true); + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true); } } RemoveFollowTarget(); @@ -914,7 +914,7 @@ namespace Barotrauma } } } - if (HumanAIController.HasItem(character, "handlocker", out IEnumerable matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy)) + if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy)) { var handCuffs = matchingItems.First(); if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true)) @@ -928,7 +928,7 @@ namespace Barotrauma return; } } - character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f); + character.Speak(TextManager.Get("DialogTargetArrested").Value, null, 3.0f, "targetarrested".ToIdentifier(), 30.0f); } if (!objectiveManager.IsCurrentObjective()) { @@ -939,7 +939,7 @@ namespace Barotrauma /// /// Seeks for more ammunition. Creates a new subobjective. /// - private void SeekAmmunition(string[] ammunitionIdentifiers) + private void SeekAmmunition(Identifier[] ammunitionIdentifiers) { retreatTarget = null; RemoveSubObjective(ref retreatObjective); @@ -974,7 +974,7 @@ namespace Barotrauma HumanAIController.UnequipEmptyItems(Weapon); RelatedItem item = null; Item ammunition = null; - string[] ammunitionIdentifiers = null; + Identifier[] ammunitionIdentifiers = null; if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) @@ -1212,17 +1212,17 @@ namespace Barotrauma retreatTarget = null; } - private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30); - private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20); + private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30); + private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); - private void Speak(string textIdentifier, float delay, float minDuration) + private void Speak(Identifier textIdentifier, float delay, float minDuration) { if (character.IsOnPlayerTeam && !character.IsInFriendlySub) { - string msg = TextManager.Get(textIdentifier, true); - if (msg != null) + LocalizedString msg = TextManager.Get(textIdentifier); + if (!msg.IsNullOrEmpty()) { - character.Speak(msg, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration); + character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 26d2e8757..dea08ca34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -7,18 +7,18 @@ namespace Barotrauma { class AIObjectiveContainItem: AIObjective { - public override string Identifier { get; set; } = "contain item"; + public override Identifier Identifier { get; set; } = "contain item".ToIdentifier(); public Func GetItemPriority; - public string[] ignoredContainerIdentifiers; + public Identifier[] ignoredContainerIdentifiers; public bool checkInventory = true; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs and in some cases also enemy NPCs, like pirates) private readonly bool spawnItemIfNotFound; //can either be a tag or an identifier - public readonly string[] itemIdentifiers; + public readonly Identifier[] itemIdentifiers; public readonly ItemContainer container; private readonly Item item; public Item ItemToContain { get; private set; } @@ -60,25 +60,21 @@ namespace Barotrauma this.item = item; } - public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) - : this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { } + public AIObjectiveContainItem(Character character, Identifier itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) + : this(character, new Identifier[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveContainItem(Character character, Identifier[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; this.spawnItemIfNotFound = spawnItemIfNotFound; - for (int i = 0; i < itemIdentifiers.Length; i++) - { - itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); - } this.container = container; } protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character))) + if (container?.Item == null || !container.Item.HasAccess(character)) { Abandon = true; return false; @@ -89,23 +85,28 @@ namespace Barotrauma } else { - int containedItemCount = 0; - foreach (Item it in container.Inventory.AllItems) - { - if (CheckItem(it)) - { - containedItemCount++; - } - } - return containedItemCount >= ItemCount; + return CountItems(); } } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character); + private bool CountItems() + { + int containedItemCount = 0; + foreach (Item it in container.Inventory.AllItems) + { + if (CheckItem(it)) + { + containedItemCount++; + } + } + return containedItemCount >= ItemCount; + } + + private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && i.HasAccess(character); protected override void Act(float deltaTime) { - if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character)) + if (container?.Item == null) { Abandon = true; return; @@ -141,8 +142,8 @@ namespace Barotrauma container.Inventory.TryPutItem(item, null); } } - IsCompleted = true; } + IsCompleted = item != null || CountItems(); } else { @@ -159,7 +160,7 @@ namespace Barotrauma { TargetName = container.Item.Name, AbortCondition = obj => - container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) || (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index dbcfad9d2..047c1b636 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveDecontainItem : AIObjective { - public override string Identifier { get; set; } = "decontain item"; + public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier(); public Func GetItemPriority; @@ -127,7 +127,7 @@ namespace Barotrauma RemoveExistingPredicate = RemoveExistingPredicate, RemoveMax = RemoveExistingMax, GetItemPriority = GetItemPriority, - ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null + ignoredContainerIdentifiers = sourceContainer != null ? new Identifier[] { sourceContainer.Item.Prefab.Identifier } : null }, onCompleted: () => IsCompleted = true, onAbandon: () => Abandon = true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs index 7c689df98..5a57adc31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveEscapeHandcuffs.cs @@ -6,7 +6,7 @@ namespace Barotrauma class AIObjectiveEscapeHandcuffs : AIObjective { // Used for prisoner escorts to allow them to escape their binds - public override string Identifier { get; set; } = "escape handcuffs"; + public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; public override bool AllowOutsideSubmarine => true; public override bool AllowInAnySub => true; @@ -88,7 +88,7 @@ namespace Barotrauma escapeProgress += Rand.Range(2, 5); if (escapeProgress > 15) { - Item handcuffs = character.Inventory.FindItemByTag("handlocker"); + Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier()); if (handcuffs != null) { handcuffs.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 251c0362b..4f818f175 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveExtinguishFire : AIObjective { - public override string Identifier { get; set; } = "extinguish fire"; + public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier(); public override bool ForceRun => true; public override bool ConcurrentObjectives => true; public override bool KeepDivingGearOn => true; @@ -77,16 +77,16 @@ namespace Barotrauma private float sinTime; protected override void Act(float deltaTime) { - var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher"); + var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier()); if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem)) { TryAddSubObjective(ref getExtinguisherObjective, () => { - if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false)) + if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false)) { - character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); + character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f); } - var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true) + var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true) { AllowStealing = true, // If the item is inside an unsafe hull, decrease the priority @@ -94,7 +94,7 @@ namespace Barotrauma }; if (objectiveManager.HasOrder()) { - getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f); + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher").Value, null, 0.0f, "dialogcannotfindfireextinguisher".ToIdentifier(), 10.0f); }; return getItemObjective; }); @@ -139,7 +139,7 @@ namespace Barotrauma extinguisher.Use(deltaTime, character); if (!targetHull.FireSources.Contains(fs)) { - character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f); } } if (move) @@ -147,7 +147,7 @@ namespace Barotrauma //go to the first firesource if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) { - DialogueIdentifier = "dialogcannotreachfire", + DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(), TargetName = fs.Hull.DisplayName }, onAbandon: () => Abandon = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 97d9450c2..a11672bcd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveExtinguishFires : AIObjectiveLoop { - public override string Identifier { get; set; } = "extinguish fires"; + public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier(); public override bool ForceRun => true; public override bool AllowInAnySub => true; @@ -27,7 +27,7 @@ namespace Barotrauma /// public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 500, hull.FireSources.Sum(fs => fs.Size.X))); - protected override IEnumerable GetList() => Hull.hullList; + protected override IEnumerable GetList() => Hull.HullList; protected override AIObjective ObjectiveConstructor(Hull target) => new AIObjectiveExtinguishFire(character, target, objectiveManager, PriorityModifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index ce20e9de1..0704b91c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveFightIntruders : AIObjectiveLoop { - public override string Identifier { get; set; } = "fight intruders"; + public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier(); protected override float IgnoreListClearInterval => 30; public override bool IgnoreUnsafeHulls => true; @@ -45,9 +45,9 @@ namespace Barotrauma { //hold fire while the enemy is in the airlock (except if they've attacked us) if (character.GetDamageDoneByAttacker(target) > 0.0f) { return false; } - return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase)); + return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t == "airlock"); }; - character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f); + character.Speak(TextManager.Get("dialogenteroutpostwarning").Value, null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning".ToIdentifier(), 30.0f); } } return combatObjective; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index cd3abcc19..c2c9f8ee5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -7,13 +7,13 @@ namespace Barotrauma { class AIObjectiveFindDivingGear : AIObjective { - public override string Identifier { get; set; } = "find diving gear"; + public override Identifier Identifier { get; set; } = "find diving gear".ToIdentifier(); public override string DebugTag => $"{Identifier} ({gearTag})"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AbandonWhenCannotCompleteSubjectives => false; - private readonly string gearTag; + private readonly Identifier gearTag; private AIObjectiveGetItem getDivingGear; private AIObjectiveContainItem getOxygen; @@ -21,13 +21,13 @@ namespace Barotrauma public const float MIN_OXYGEN = 10; - public const string HEAVY_DIVING_GEAR = "deepdiving"; - public const string LIGHT_DIVING_GEAR = "lightdiving"; + public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier(); + public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier(); /// /// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear) /// - public const string DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors"; - public const string OXYGEN_SOURCE = "oxygensource"; + public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier(); + public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier(); protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head); @@ -54,7 +54,7 @@ namespace Barotrauma { if (targetItem == null && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); + character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f); } return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { @@ -92,15 +92,15 @@ namespace Barotrauma { if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min)) { - character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f); + character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f); if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1) { - character.Speak(TextManager.Get("dialoglastoxygentank"), null, 0.0f, "dialoglastoxygentank", 30.0f); + character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f); } } else { - character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f); } } return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) @@ -130,7 +130,7 @@ namespace Barotrauma Abandon = true; if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f)) { - character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f); + character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f); } }, onCompleted: () => RemoveSubObjective(ref getOxygen)); @@ -147,11 +147,11 @@ namespace Barotrauma int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1); if (remainingOxygenTanks == 0) { - character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f); + character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f); } else if (remainingOxygenTanks < 10) { - character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f); + character.Speak(TextManager.Get("DialogLowOnOxygenTanks").Value, null, 0.0f, "lowonoxygentanks".ToIdentifier(), 30.0f); } return remainingOxygenTanks; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index dc2a12da2..f3380c63a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveFindSafety : AIObjective { - public override string Identifier { get; set; } = "find safety"; + public override Identifier Identifier { get; set; } = "find safety".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; @@ -317,7 +317,7 @@ namespace Barotrauma Hull bestHull = null; float bestValue = 0; bool bestIsAirlock = false; - foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h))) + foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h))) { 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. @@ -342,7 +342,7 @@ namespace Barotrauma //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) if (hullSafety < bestValue) { continue; } //avoid airlock modules if not allowed to change the sub - if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t.Equals("airlock", StringComparison.OrdinalIgnoreCase))) + if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock")) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 256913c4e..9e9a1be4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveFixLeak : AIObjective { - public override string Identifier { get; set; } = "fix leak"; + public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AllowInAnySub => true; @@ -64,15 +64,15 @@ namespace Barotrauma protected override void Act(float deltaTime) { - var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true); + var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true); if (weldingTool == null) { - TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), + TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onAbandon: () => { if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder()) { - character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f); + character.Speak(TextManager.Get("dialogcannotfindweldingequipment").Value, null, 0.0f, "dialogcannotfindweldingequipment".ToIdentifier(), 10.0f); } Abandon = true; }, @@ -91,7 +91,7 @@ namespace Barotrauma } if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { RemoveExisting = true }, @@ -112,11 +112,11 @@ namespace Barotrauma int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1); if (remainingOxygenTanks == 0) { - character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f); + character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f); } else if (remainingOxygenTanks < 4) { - character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f); + character.Speak(TextManager.Get("DialogLowOnWeldingFuel").Value, null, 0.0f, "lowonweldingfuel".ToIdentifier(), 30.0f); } } return; @@ -142,7 +142,7 @@ namespace Barotrauma bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak), + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak), onAbandon: () => Abandon = true, onCompleted: () => { @@ -160,7 +160,7 @@ namespace Barotrauma { UseDistanceRelativeToAimSourcePos = true, CloseEnough = reach, - DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null, + DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty, TargetName = Leak.FlowTargetHull?.DisplayName, CheckVisibility = false, requiredCondition = () => Leak.Submarine == character.Submarine, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 5f7d91295..9094affbe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveFixLeaks : AIObjectiveLoop { - public override string Identifier { get; set; } = "fix leaks"; + public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; public override bool AllowInAnySub => true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index a0d14f7dc..4b5aba5dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveGetItem : AIObjective { - public override string Identifier { get; set; } = "get item"; + public override Identifier Identifier { get; set; } = "get item".ToIdentifier(); public override bool AbandonWhenCannotCompleteSubjectives => false; public override bool AllowMultipleInstances => true; @@ -21,7 +21,7 @@ namespace Barotrauma public float TargetCondition { get; set; } = 1; public bool AllowDangerousPressure { get; set; } - public readonly ImmutableArray IdentifiersOrTags; + public readonly ImmutableArray IdentifiersOrTags; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) private bool spawnItemIfNotFound = false; @@ -32,8 +32,8 @@ namespace Barotrauma private bool isDoneSeeking; public Item TargetItem => targetItem; private int currSearchIndex; - public string[] ignoredContainerIdentifiers; - public string[] ignoredIdentifiersOrTags; + public Identifier[] ignoredContainerIdentifiers; + public Identifier[] ignoredIdentifiersOrTags; private AIObjectiveGoTo goToObjective; private float currItemPriority; private readonly bool checkInventory; @@ -83,10 +83,10 @@ namespace Barotrauma moveToTarget = targetItem?.GetRootInventoryOwner(); } - public AIObjectiveGetItem(Character character, string identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) - : this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } + public AIObjectiveGetItem(Character character, Identifier identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + : this(character, new Identifier[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { } - public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) + public AIObjectiveGetItem(Character character, IEnumerable identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; @@ -97,27 +97,27 @@ namespace Barotrauma ignoredIdentifiersOrTags = ParseIgnoredTags(identifiersOrTags).ToArray(); } - public static IEnumerable ParseGearTags(IEnumerable identifiersOrTags) + public static IEnumerable ParseGearTags(IEnumerable identifiersOrTags) { - var tags = new List(); - foreach (string tag in identifiersOrTags) + var tags = new List(); + foreach (Identifier tag in identifiersOrTags) { - if (!tag.Contains('!')) + if (!tag.Contains("!")) { - tags.Add(tag.ToLowerInvariant()); + tags.Add(tag); } } return tags; } - public static IEnumerable ParseIgnoredTags(IEnumerable identifiersOrTags) + public static IEnumerable ParseIgnoredTags(IEnumerable identifiersOrTags) { - var ignoredTags = new List(); - foreach (string tag in identifiersOrTags) + var ignoredTags = new List(); + foreach (Identifier tag in identifiersOrTags) { - if (tag.Contains('!')) + if (tag.Contains("!")) { - ignoredTags.Add(tag.Remove("!").ToLowerInvariant()); + ignoredTags.Add(tag.Remove("!")); } } return ignoredTags; @@ -177,7 +177,7 @@ namespace Barotrauma if (dangerousPressure) { #if DEBUG - string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault(); + string itemName = targetItem != null ? targetItem.Name : IdentifiersOrTags.FirstOrDefault().Value; DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow); #endif Abandon = true; @@ -480,7 +480,7 @@ namespace Barotrauma } else { - Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => + Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => { targetItem = spawnedItem; if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) @@ -528,14 +528,13 @@ namespace Barotrauma private bool CheckItem(Item item) { - if (!item.IsInteractable(character)) { return false; } - if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } + if (!item.HasAccess(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; - if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.prefab.Identifier == id || item.HasTag(id))) { return false; } + if (ignoredIdentifiersOrTags != null && ignoredIdentifiersOrTags.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; } - return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id)); + return IdentifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id)); } public override void Reset() @@ -575,9 +574,9 @@ namespace Barotrauma if (!character.IsOnPlayerTeam) { return; } if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; } if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; } - string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); - if (msg == null) { return; } - character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); + LocalizedString msg = TextManager.Get(CannotFindDialogueIdentifierOverride, "dialogcannotfinditem"); + if (msg.IsNullOrEmpty() || !msg.Loaded) { return; } + character.Speak(msg.Value, identifier: "dialogcannotfinditem".ToIdentifier(), minDurationBetweenSimilar: 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs index 9cb439547..0587b597f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItems.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveGetItems : AIObjective { - public override string Identifier { get; set; } = "get items"; + public override Identifier Identifier { get; set; } = "get items".ToIdentifier(); public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool AllowMultipleInstances => true; @@ -24,13 +24,13 @@ namespace Barotrauma public bool RequireLoaded { get; set; } public bool RequireAllItems { get; set; } - private readonly ImmutableArray gearTags; - private readonly string[] ignoredTags; + private readonly ImmutableArray gearTags; + private readonly Identifier[] ignoredTags; private bool subObjectivesCreated; public readonly HashSet achievedItems = new HashSet(); - public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + public AIObjectiveGetItems(Character character, AIObjectiveManager objectiveManager, IEnumerable identifiersOrTags, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { gearTags = AIObjectiveGetItem.ParseGearTags(identifiersOrTags).ToImmutableArray(); ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToArray(); @@ -47,7 +47,7 @@ namespace Barotrauma } if (!subObjectivesCreated) { - foreach (string tag in gearTags) + foreach (Identifier tag in gearTags) { if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; } int count = gearTags.Count(t => t == tag); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index dd250f619..2a1062948 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Barotrauma.Extensions; @@ -8,7 +9,7 @@ namespace Barotrauma { class AIObjectiveGoTo : AIObjective { - public override string Identifier { get; set; } = "go to"; + public override Identifier Identifier { get; set; } = "go to".ToIdentifier(); private AIObjectiveFindDivingGear findDivingGear; private readonly bool repeat; @@ -96,8 +97,8 @@ namespace Barotrauma public override bool AllowOutsideSubmarine => AllowGoingOutside; public override bool AllowInAnySub => true; - public string DialogueIdentifier { get; set; } = "dialogcannotreachtarget"; - public string TargetName { get; set; } + public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier(); + public LocalizedString TargetName { get; set; } public ISpatialEntity Target { get; private set; } @@ -180,9 +181,11 @@ namespace Barotrauma if (DialogueIdentifier == null) { return; } if (!SpeakIfFails) { return; } if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; } - string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); - if (msg == null) { return; } - character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); + LocalizedString msg = TargetName == null ? + TextManager.Get(DialogueIdentifier) : + TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes); + if (msg.IsNullOrEmpty() || !msg.Loaded) { return; } + character.Speak(msg.Value, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); } public void ForceAct(float deltaTime) => Act(deltaTime); @@ -382,13 +385,23 @@ namespace Barotrauma { useScooter = false; checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f); - string scooterTag = "scooter"; - string batteryTag = "mobilebattery"; + Identifier scooterTag = "scooter".ToIdentifier(); + Identifier batteryTag = "mobilebattery".ToIdentifier(); Item scooter = null; - float closeEnough = 250; - float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); - bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!Mimic || - (targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); + bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false); + if (!shouldUseScooter) + { + float threshold = 500; + if (isInside) + { + Vector2 diff = Target.WorldPosition - character.WorldPosition; + shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150; + } + else + { + shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold; + } + } if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) { // Currently equipped scooter @@ -424,8 +437,7 @@ namespace Barotrauma } } } - bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter); - if (scooter != null && isScooterEquipped) + if (scooter != null && character.HasEquippedItem(scooter)) { if (shouldUseScooter) { @@ -534,6 +546,7 @@ namespace Barotrauma void UseScooter(Vector2 targetWorldPos) { + if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; } SteeringManager.Reset(); character.CursorPosition = targetWorldPos; if (character.Submarine != null) @@ -542,19 +555,26 @@ namespace Barotrauma } Vector2 diff = character.CursorPosition - character.Position; Vector2 dir = Vector2.Normalize(diff); - float sqrDist = diff.LengthSquared(); - if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f)) + if (character.CurrentHull == null && IsFollowOrderObjective) { - SteeringManager.SteeringManual(1.0f, dir); - } - else - { - float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2)); - bool isFacing = dot > 0.9f; - if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough)) + float sqrDist = diff.LengthSquared(); + if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f)) { SteeringManager.SteeringManual(1.0f, dir); } + else + { + float dot = Vector2.Dot(dir, VectorExtensions.Forward(character.AnimController.Collider.Rotation + MathHelper.PiOver2)); + bool isFacing = dot > 0.9f; + if (!isFacing && sqrDist > MathUtils.Pow2(CloseEnough)) + { + SteeringManager.SteeringManual(1.0f, dir); + } + } + } + else + { + SteeringManager.SteeringManual(1.0f, dir); } character.SetInput(InputType.Aim, false, true); character.SetInput(InputType.Shoot, false, true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 4be1b47fd..762e8cd08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveIdle : AIObjective { - public override string Identifier { get; set; } = "idle"; + public override Identifier Identifier { get; set; } = "idle".ToIdentifier(); public override bool AllowAutomaticItemUnequipping => true; public override bool AllowInAnySub => true; @@ -93,7 +93,7 @@ namespace Barotrauma public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); } - public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); + public readonly HashSet PreferredOutpostModuleTypes = new HashSet(); public void CalculatePriority(float max = 0) { @@ -391,7 +391,7 @@ namespace Barotrauma { targetHulls.Clear(); hullWeights.Clear(); - foreach (var hull in Hull.hullList) + foreach (var hull in Hull.HullList) { if (character.Submarine == null) { break; } if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 9ec761e4c..cfd3e7ca9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -10,7 +10,7 @@ namespace Barotrauma { class AIObjectiveLoadItem : AIObjective { - public override string Identifier { get; set; } = "load item"; + public override Identifier Identifier { get; set; } = "load item".ToIdentifier(); public override bool IsLoop { get => true; @@ -20,9 +20,9 @@ namespace Barotrauma private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } private Item Container { get; } private ItemContainer ItemContainer { get; } - private ImmutableArray TargetContainerTags { get; } - private ImmutableHashSet ValidContainableItemIdentifiers { get; } - private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); + private ImmutableArray TargetContainerTags { get; } + private ImmutableHashSet ValidContainableItemIdentifiers { get; } + private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); private int itemIndex = 0; private AIObjectiveDecontainItem decontainObjective; @@ -30,7 +30,7 @@ namespace Barotrauma private Item targetItem; private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable"; - public AIObjectiveLoadItem(Item container, ImmutableArray targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, string option, Character character, AIObjectiveManager objectiveManager, float priorityModifier) + public AIObjectiveLoadItem(Item container, ImmutableArray targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, Identifier option, Character character, AIObjectiveManager objectiveManager, float priorityModifier) : base(character, objectiveManager, priorityModifier) { Container = container; @@ -42,7 +42,7 @@ namespace Barotrauma } TargetContainerTags = targetTags; TargetItemCondition = targetCondition; - if (!string.IsNullOrEmpty(option)) + if (!option.IsEmpty) { string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}"; if (TextManager.ContainsTag(optionSpecificDialogueIdentifier)) @@ -63,7 +63,7 @@ namespace Barotrauma private enum CheckStatus { Unfinished, Finished } - private ImmutableHashSet GetValidContainableItemIdentifiers() + private ImmutableHashSet GetValidContainableItemIdentifiers() { if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers)) { @@ -75,7 +75,7 @@ namespace Barotrauma var potentialContainablePrefabs = MapEntityPrefab.List .Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i))) .Cast(); - var validContainableItemIdentifiers = new HashSet(); + var validContainableItemIdentifiers = new HashSet(); foreach (var component in Container.Components) { if (CheckComponent() == CheckStatus.Finished) @@ -125,7 +125,7 @@ namespace Barotrauma useDefaultContainableItemIdentifiers = false; if (statusEffect.TargetIdentifiers != null) { - foreach (string target in statusEffect.TargetIdentifiers) + foreach (Identifier target in statusEffect.TargetIdentifiers) { foreach (var prefab in potentialContainablePrefabs) { @@ -308,11 +308,9 @@ namespace Barotrauma if (rootInventoryOwner is Item parentItem) { if (parentItem.HasTag("donttakeitems")) { return false; } - if (!(parentItem.GetComponent()?.HasAccess(character) ?? true)) { return false; } } - if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } + if (!item.HasAccess(character)) { return false; } if (!character.HasItem(item) && !CanEquip(item)) { return false; } - if (!ItemContainer.HasAccess(character)) { return false; } if (!ItemContainer.CanBeContained(item)) { return false; } if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 5d6eb0753..67a417900 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -9,11 +9,11 @@ namespace Barotrauma { class AIObjectiveLoadItems : AIObjectiveLoop { - public override string Identifier { get; set; } = "load items"; + public override Identifier Identifier { get; set; } = "load items".ToIdentifier(); protected override float IgnoreListClearInterval => 20.0f; protected override bool ResetWhenClearingIgnoreList => false; - private ImmutableArray TargetContainerTags { get; } + private ImmutableArray TargetContainerTags { get; } private List TargetContainers { get; } = new List(); private ItemCondition TargetCondition { get; } @@ -23,7 +23,7 @@ namespace Barotrauma Full } - public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, string option, ImmutableArray containerTags, Item targetContainer = null, float priorityModifier = 1) + public AIObjectiveLoadItems(Character character, AIObjectiveManager objectiveManager, Identifier option, ImmutableArray containerTags, Item targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { if ((containerTags == null || containerTags.None()) && targetContainer == null) @@ -50,19 +50,17 @@ namespace Barotrauma return true; } - public static bool IsValidTarget(Item item, Character character, ImmutableArray? targetContainerTags = null, ItemCondition? targetCondition = null) + 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 (targetContainerTags.HasValue && !Order.TargetItemsMatchItem(targetContainerTags.Value, item)) { 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; } if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } - if (!item.IsInteractable(character)) { return false; } - if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } - if (!container.HasAccess(character)) { return false; } + if (!item.HasAccess(character)) { return false; } // Ignore items that require power but don't have it if (item.GetComponent() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 55b32ce12..38aa4b5d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -36,7 +36,7 @@ namespace Barotrauma return false; } - public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null) + public AIObjectiveLoop(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default) : base(character, objectiveManager, priorityModifier, option) { } protected override void Act(float deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 44cc68d5a..e77eceb92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -38,7 +38,7 @@ namespace Barotrauma } } - public List CurrentOrders { get; } = new List(); + public List CurrentOrders { get; } = new List(); /// /// The AIObjective in with the highest /// @@ -123,23 +123,23 @@ namespace Barotrauma int objectiveCount = Objectives.Count; foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) { - var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); - if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } + var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier]; + if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); } Item item = null; if (orderPrefab.MustSetTarget) { - item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character, orderOption: autonomousObjective.option)?.GetRandom(); + item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced(); } - var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); + 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.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID)) { continue; } } - var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier); + var objective = CreateObjective(order, autonomousObjective.PriorityModifier); if (objective != null && objective.CanBeCompleted) { AddObjective(objective, delay: Rand.Value() / 2); @@ -324,7 +324,7 @@ namespace Barotrauma SortObjectives(); } - public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak) + public void SetOrder(Order order, bool speak) { if (character.IsDead) { @@ -336,13 +336,13 @@ namespace Barotrauma } ClearIgnored(); - if (order == null || order.Identifier == "dismissed") + if (order == null || order.IsDismissal) { - if (!string.IsNullOrEmpty(option)) + if (order.Option != Identifier.Empty) { - if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option))) + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(order.Option))) { - var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option)); + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(order.Option)); CurrentOrders.Remove(dismissedOrderInfo); } } @@ -357,18 +357,18 @@ namespace Barotrauma { if (CurrentOrders.Count <= i) { break; } var currentOrder = CurrentOrders[i]; - if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option)) + if (currentOrder.Objective == null || currentOrder.MatchesOrder(order)) { CurrentOrders.RemoveAt(i); continue; } - var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption); - if (currentOrderInfo.HasValue) + var currentOrderInfo = character.GetCurrentOrder(currentOrder); + if (currentOrderInfo is Order) { - int currentPriority = currentOrderInfo.Value.ManualPriority; + int currentPriority = currentOrderInfo.ManualPriority; if (currentOrder.ManualPriority != currentPriority) { - CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority); + CurrentOrders[i] = currentOrder.WithManualPriority(currentPriority); } } else @@ -377,46 +377,46 @@ namespace Barotrauma } } - var newCurrentOrder = CreateObjective(order, option, orderGiver); - if (newCurrentOrder != null) + var newCurrentObjective = CreateObjective(order); + if (newCurrentObjective != null) { - newCurrentOrder.Abandoned += () => DismissSelf(order, option); - CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder)); + newCurrentObjective.Abandoned += () => DismissSelf(order); + CurrentOrders.Add(order.WithObjective(newCurrentObjective)); } if (!HasOrders()) { // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) CreateAutonomousObjectives(); } - else if (newCurrentOrder != null) + else if (newCurrentObjective != null) { if (speak && character.IsOnPlayerTeam) { - string msg = newCurrentOrder.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative"); - character.Speak(msg, delay: 1.0f); + LocalizedString msg = newCurrentObjective.IsAllowed ? TextManager.Get("DialogAffirmative") : TextManager.Get("DialogNegative"); + character.Speak(msg.Value, delay: 1.0f); } } } - public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1) + public AIObjective CreateObjective(Order order, float priorityModifier = 1) { - if (order == null || order.Identifier == "dismissed") { return null; } + if (order == null || order.IsDismissal) { return null; } AIObjective newObjective; - switch (order.Identifier.ToLowerInvariant()) + switch (order.Identifier.Value.ToLowerInvariant()) { case "follow": - if (orderGiver == null) { return null; } - newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier) + if (order.OrderGiver == null) { return null; } + newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { CloseEnough = Rand.Range(80f, 100f), - CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), + CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), ExtraDistanceOutsideSub = 100, ExtraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, IsFollowOrderObjective = true, Mimic = character.IsOnPlayerTeam, - DialogueIdentifier = "dialogcannotreachplace" + DialogueIdentifier = "dialogcannotreachplace".ToIdentifier() }; break; case "wait": @@ -426,14 +426,14 @@ namespace Barotrauma }; break; case "return": - newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier); - newObjective.Completed += () => DismissSelf(order, option); + newObjective = new AIObjectiveReturn(character, order.OrderGiver, this, priorityModifier: priorityModifier); + newObjective.Completed += () => DismissSelf(order); break; case "fixleaks": newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull); break; case "chargebatteries": - newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier); + newObjective = new AIObjectiveChargeBatteries(character, this, order.Option, priorityModifier); break; case "rescue": newObjective = new AIObjectiveRescueAll(character, this, priorityModifier); @@ -450,16 +450,16 @@ namespace Barotrauma if (order.TargetItemComponent is Pump targetPump) { if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } - newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) + newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier) { IsLoop = false, - Override = orderGiver != null && orderGiver.IsCommanding + Override = order.OrderGiver is { IsCommanding: true } }; - newObjective.Completed += () => DismissSelf(order, option); + newObjective.Completed += () => DismissSelf(order); } else { - newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); + newObjective = new AIObjectivePumpWater(character, this, order.Option, priorityModifier: priorityModifier); } break; case "extinguishfires": @@ -479,22 +479,22 @@ namespace Barotrauma if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; } if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && orderGiver.IsCommanding + Override = order.OrderGiver != null && order.OrderGiver.IsCommanding }; break; case "setchargepct": - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier) + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier) { IsLoop = false, Override = !character.IsDismissed, completionCondition = () => { - if (float.TryParse(option, out float pct)) + if (float.TryParse(order.Option.Value, out float pct)) { var targetRatio = Math.Clamp(pct, 0f, 1f); var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio; @@ -532,7 +532,7 @@ namespace Barotrauma newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier); break; case "prepareforexpedition": - newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems) + newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { KeepActiveWhenReady = true, CheckInventory = true, @@ -548,7 +548,7 @@ namespace Barotrauma } else { - prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(option), order.RequireItems) + prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems) { KeepActiveWhenReady = false, CheckInventory = false, @@ -559,20 +559,20 @@ namespace Barotrauma prepareObjective.KeepActiveWhenReady = false; prepareObjective.Equip = true; newObjective = prepareObjective; - newObjective.Completed += () => DismissSelf(order, option); + newObjective.Completed += () => DismissSelf(order); break; case "loaditems": - newObjective = new AIObjectiveLoadItems(character, this, option, order.GetTargetItems(option), order.TargetEntity as Item, priorityModifier); + newObjective = new AIObjectiveLoadItems(character, this, order.Option, order.GetTargetItems(order.Option), order.TargetEntity as Item, priorityModifier); break; default: if (order.TargetItemComponent == null) { return null; } if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player - Override = orderGiver != null && orderGiver.IsCommanding + Override = order.OrderGiver != null && order.OrderGiver.IsCommanding }; if (newObjective.Abandon) { return null; } break; @@ -585,27 +585,26 @@ namespace Barotrauma return newObjective; } - private void DismissSelf(Order order, string option) + private void DismissSelf(Order order) { - var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order, option)); - if (currentOrder.Order == null) + var currentOrder = CurrentOrders.FirstOrDefault(oi => oi.MatchesOrder(order.Identifier, order.Option)); + if (currentOrder == null) { #if DEBUG DebugConsole.ThrowError("Tried to self-dismiss an order, but no matching current order was found"); #endif return; } - Order dismissOrder = Order.GetPrefab("dismissed"); - var orderOption = Order.GetDismissOrderOption(currentOrder); - int priority = currentOrder.ManualPriority; + + Order dismissOrder = currentOrder.GetDismissal(); #if CLIENT if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) { - GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder, orderOption, priority, character); + GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder); } #else - GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, orderOption, priority, currentOrder.Order.TargetSpatialEntity, character, character)); - SetOrder(dismissOrder, orderOption, priority, character, speak: false); + GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character)); + SetOrder(dismissOrder, speak: false); #endif } @@ -695,7 +694,7 @@ namespace Barotrauma return 0; } - public OrderInfo? GetCurrentOrderInfo() + public Order GetCurrentOrderInfo() { if (currentOrder == null) { return null; } return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 5c7055788..80e9f3371 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectiveOperateItem : AIObjective { - public override string Identifier { get; set; } = "operate item"; + public override Identifier Identifier { get; set; } = "operate item".ToIdentifier(); public override string DebugTag => $"{Identifier} {component.Name}"; public override bool AllowAutomaticItemUnequipping => true; @@ -79,7 +79,7 @@ namespace Barotrauma return Priority; } } - switch (Option) + switch (Option.Value.ToLowerInvariant()) { case "shutdown": if (!reactor.PowerOn) @@ -146,7 +146,7 @@ namespace Barotrauma return Priority; } - public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, + public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, Identifier option, bool requireEquip, Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { @@ -181,7 +181,7 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); + character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name).Value, delay: 2.0f, identifier: "cantfindcontroller".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } Abandon = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs index 9bea9ed11..d83f75768 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePrepare.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class AIObjectivePrepare : AIObjective { - public override string Identifier { get; set; } = "prepare"; + public override Identifier Identifier { get; set; } = "prepare".ToIdentifier(); public override string DebugTag => $"{Identifier}"; public override bool KeepDivingGearOn => true; public override bool KeepDivingGearOnAlsoWhenInactive => true; @@ -19,8 +19,8 @@ namespace Barotrauma private AIObjectiveGetItems getMultipleItemsObjective; private bool subObjectivesCreated; private readonly Item targetItem; - private readonly ImmutableArray requiredItems; - private readonly ImmutableArray optionalItems; + private readonly ImmutableArray requiredItems; + private readonly ImmutableArray optionalItems; private readonly HashSet items = new HashSet(); public bool KeepActiveWhenReady { get; set; } public bool CheckInventory { get; set; } @@ -43,7 +43,7 @@ namespace Barotrauma this.targetItem = targetItem; } - public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable optionalItems, IEnumerable requiredItems = null, float priorityModifier = 1) + public AIObjectivePrepare(Character character, AIObjectiveManager objectiveManager, IEnumerable optionalItems, IEnumerable requiredItems = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.optionalItems = optionalItems.ToImmutableArray(); @@ -98,7 +98,7 @@ namespace Barotrauma { getAllItemsObjective = CreateObjectives(requiredItems, requireAll: true); } - AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) + AIObjectiveGetItems CreateObjectives(IEnumerable itemTags, bool requireAll) { AIObjectiveGetItems objectiveReference = null; if (!TryAddSubObjective(ref objectiveReference, () => new AIObjectiveGetItems(character, objectiveManager, itemTags) @@ -148,7 +148,7 @@ namespace Barotrauma } else { - IEnumerable allItems = optionalItems; + IEnumerable allItems = optionalItems; if (requiredItems != null && requiredItems.Any()) { allItems = requiredItems; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index d0c4045e8..f4537800a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -9,13 +9,13 @@ namespace Barotrauma { class AIObjectivePumpWater : AIObjectiveLoop { - public override string Identifier { get; set; } = "pump water"; + public override Identifier Identifier { get; set; } = "pump water".ToIdentifier(); public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => true; private IEnumerable pumpList; - public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1) + public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { } protected override void FindTargets() @@ -48,7 +48,7 @@ namespace Barotrauma { if (pumpList == null) { - if (character == null || character.Submarine == null) { return new Pump[0]; } + if (character == null || character.Submarine == null) { return Array.Empty(); } pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent()).Where(p => p != null); } return pumpList; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index b2115b8ba..adf82ad04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveRepairItem : AIObjective { - public override string Identifier { get; set; } = "repair item"; + public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); public override bool AllowInAnySub => true; @@ -70,7 +70,7 @@ namespace Barotrauma float reduction = isPriority ? 1 : isSelected ? 2 : 3; float max = AIObjectiveManager.LowestOrderPriority - reduction; float highestWeight = -1; - foreach (string tag in Item.Prefab.Tags) + foreach (Identifier tag in Item.Prefab.Tags) { if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight) { @@ -92,7 +92,7 @@ namespace Barotrauma IsCompleted = Item.IsFullCondition; if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) { - character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "itemrepaired".ToIdentifier(), 10.0f); } return IsCompleted; } @@ -118,7 +118,7 @@ namespace Barotrauma { if (character.IsOnPlayerTeam) { - getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f); + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair").Value, null, 0.0f, "dialogcannotfindrequireditemtorepair".ToIdentifier(), 10.0f); } } subObjectives.Add(getItemObjective); @@ -206,7 +206,7 @@ namespace Barotrauma { if (character.IsOnPlayerTeam && IsRepairing()) { - character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f); } repairable.StopRepairing(character); } @@ -243,7 +243,7 @@ namespace Barotrauma Abandon = true; if (character.IsOnPlayerTeam && IsRepairing()) { - character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, FormatCapitals.Yes).Value, null, 0.0f, "cannotrepair".ToIdentifier(), 10.0f); } }); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 92f1e67e6..a8b001bb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -9,12 +9,12 @@ namespace Barotrauma { class AIObjectiveRepairItems : AIObjectiveLoop { - public override string Identifier { get; set; } = "repair items"; + public override Identifier Identifier { get; set; } = "repair items".ToIdentifier(); /// /// If set, only fix items where required skill matches this. /// - public string RelevantSkill; + public Identifier RelevantSkill; public Item PrioritizedItem { get; private set; } @@ -72,9 +72,9 @@ namespace Barotrauma if (NearlyFullCondition(item)) { return false; } } } - if (!string.IsNullOrWhiteSpace(RelevantSkill)) + if (!RelevantSkill.IsEmpty) { - if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } + if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier == RelevantSkill))) { return false; } } return !HumanAIController.IsItemRepairedByAnother(item, out _); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 3b0b1499f..7b460210f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class AIObjectiveRescue : AIObjective { - public override string Identifier { get; set; } = "rescue"; + public override Identifier Identifier { get; set; } = "rescue".ToIdentifier(); public override bool ForceRun => true; public override bool KeepDivingGearOn => true; @@ -146,9 +146,10 @@ namespace Barotrauma { if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null) { - character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", + ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f); } // Go to the target and select it if (!character.CanInteractWith(targetCharacter)) @@ -158,7 +159,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, - DialogueIdentifier = "dialogcannotreachpatient", + DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), TargetName = targetCharacter.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), @@ -216,7 +217,7 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat, - DialogueIdentifier = "dialogcannotreachpatient", + DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), TargetName = targetCharacter.DisplayName }, onCompleted: () => RemoveSubObjective(ref goToObjective), @@ -233,18 +234,19 @@ namespace Barotrauma { if (targetCharacter.CurrentHull?.DisplayName != null) { - character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", + ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value, + null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f); } } GiveTreatment(deltaTime); } } - private readonly List suitableItemIdentifiers = new List(); - private readonly List itemNameList = new List(); - private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); + private readonly List suitableItemIdentifiers = new List(); + private readonly List itemNameList = new List(); + private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { if (targetCharacter == null) @@ -281,7 +283,7 @@ namespace Barotrauma if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } float bestSuitability = 0.0f; Item bestItem = null; - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability) @@ -311,12 +313,12 @@ namespace Barotrauma { itemNameList.Clear(); suitableItemIdentifiers.Clear(); - foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { if (treatmentSuitability.Value <= cprSuitability) { continue; } if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab) { - if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; } + if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; } suitableItemIdentifiers.Add(treatmentSuitability.Key); //only list the first 4 items if (itemNameList.Count < 4) @@ -327,7 +329,7 @@ namespace Barotrauma } if (itemNameList.Any()) { - string itemListStr = ""; + LocalizedString itemListStr = ""; if (itemNameList.Count == 1) { itemListStr = itemNameList[0]; @@ -337,33 +339,34 @@ namespace Barotrauma //[treatment1] or [treatment2] itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsLast", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemNameList[0], itemNameList[1] }); + ("[treatment1]", itemNameList[0]), + ("[treatment2]", itemNameList[1])); } else { //[treatment1], [treatment2], [treatment3] ... or [treatmentx] itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsFirst", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemNameList[0], itemNameList[1] }); + ("[treatment1]", itemNameList[0]), + ("[treatment2]", itemNameList[1])); for (int i = 2; i < itemNameList.Count - 1; i++) { itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsFirst", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemListStr, itemNameList[i] }); + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList[i])); } itemListStr = TextManager.GetWithVariables( "DialogRequiredTreatmentOptionsLast", - new string[] { "[treatment1]", "[treatment2]" }, - new string[] { itemListStr, itemNameList.Last() }); + ("[treatment1]", itemListStr), + ("[treatment2]", itemNameList.Last())); } if (targetCharacter != character && character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" }, - new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }), - null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", + ("[targetname]", targetCharacter.Name, FormatCapitals.No), + ("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value, + null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f); } RemoveSubObjective(ref getItemObjective); TryAddSubObjective(ref getItemObjective, @@ -374,13 +377,13 @@ namespace Barotrauma Abandon = true; if (character != targetCharacter && character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); } }); } else if (cprSuitability <= 0) { - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; } } @@ -388,7 +391,7 @@ namespace Barotrauma else if (!targetCharacter.IsUnconscious) { //no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR) - character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f); Abandon = true; return; } @@ -425,7 +428,7 @@ namespace Barotrauma } if (remove) { - Entity.Spawner?.AddToRemoveQueue(item); + Entity.Spawner?.AddItemToRemoveQueue(item); } } @@ -434,8 +437,8 @@ namespace Barotrauma bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), - null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); + character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name).Value, + null, 1.0f, $"targethealed{targetCharacter.Name}".ToIdentifier(), 60.0f); } return isCompleted; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index ed1684d2c..e9cd9e4bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -7,7 +7,7 @@ namespace Barotrauma { class AIObjectiveRescueAll : AIObjectiveLoop { - public override string Identifier { get; set; } = "rescue all"; + public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier(); public override bool ForceRun => true; public override bool InverseTargetEvaluation => true; public override bool AllowOutsideSubmarine => true; @@ -112,12 +112,15 @@ namespace Barotrauma { if (GetVitalityFactor(target) >= vitalityThreshold) { return false; } } - if (target.Submarine != character.Submarine) { return false; } if (character.Submarine != null) { // Don't allow going into another sub, unless it's connected and of the same team and type. if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; } } + else + { + return target.Submarine == null; + } if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { // Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index 31f2b4d75..c96c97b8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class AIObjectiveReturn : AIObjective { - public override string Identifier { get; set; } = "return"; + public override Identifier Identifier { get; set; } = "return".ToIdentifier(); public Submarine ReturnTarget { get; } private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective; @@ -93,7 +93,7 @@ namespace Barotrauma // Target the closest airlock float closestDist = 0; Hull airlock = null; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != targetHull.Submarine) { continue; } if (!hull.IsTaggedAirlock()) { continue; } @@ -210,10 +210,10 @@ namespace Barotrauma SteeringManager?.Reset(); if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) { - string msg = TextManager.Get("dialogcannotreturn", returnNull: true); - if (msg != null) + string msg = TextManager.Get("dialogcannotreturn").Value; + if (!msg.IsNullOrEmpty()) { - character.Speak(msg, identifier: "dialogcannotreturn", minDurationBetweenSimilar: 5.0f); + character.Speak(msg, identifier: "dialogcannotreturn".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 4bc797d54..9c1b018f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -18,102 +18,51 @@ namespace Barotrauma Operate } - struct OrderInfo + class OrderCategoryIcon : Prefab { - public readonly Order Order; - public readonly string OrderOption; - public readonly int ManualPriority; - public readonly OrderType Type; - public readonly AIObjective Objective; - public bool IsCurrentOrder => Type == OrderType.Current; + public readonly static PrefabCollection OrderCategoryIcons = new PrefabCollection(); - public enum OrderType + public OrderCategoryIcon(ContentXElement element, OrdersFile file) : base(file, element.GetAttributeIdentifier("category", "")) { - Current, - Previous + Category = Enum.Parse(Identifier.Value, true); + var spriteElement = element.GetChildElement("sprite"); + Sprite = new Sprite(spriteElement, lazyLoad: true); + Color = element.GetAttributeColor("color", Color.White); } - private OrderInfo(Order order, string orderOption, int manualPriority, OrderType orderType, AIObjective objective) - { - Order = order; - OrderOption = orderOption; - ManualPriority = Math.Min(manualPriority, CharacterInfo.HighestManualOrderPriority); - Type = orderType; - Objective = objective; - } + public readonly OrderCategory Category; + public readonly Sprite Sprite; + public readonly Color Color; - public OrderInfo(Order order, string orderOption) : this(order, orderOption, CharacterInfo.HighestManualOrderPriority, null) { } - - public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { } - - public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { } - - public OrderInfo(OrderInfo orderInfo, int manualPriority) : this(orderInfo.Order, orderInfo.OrderOption, manualPriority, orderInfo.Type, orderInfo.Objective) { } - - public OrderInfo(OrderInfo orderInfo, OrderType type) : this(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, type, orderInfo.Objective) { } - - public bool MatchesOrder(string orderIdentifier, string orderOption) => - (orderIdentifier == Order?.Identifier || (string.IsNullOrEmpty(orderIdentifier) && string.IsNullOrEmpty(Order?.Identifier))) && - (orderOption == OrderOption || (string.IsNullOrEmpty(orderOption) && string.IsNullOrEmpty(OrderOption))); - - public bool MatchesOrder(Order order, string option) => - MatchesOrder(order?.Identifier, option); - - public bool MatchesOrder(OrderInfo orderInfo) => - MatchesOrder(orderInfo.Order?.Identifier, orderInfo.OrderOption); - - public bool MatchesDismissedOrder(string dismissOrderOption) - { - string[] dismissedOrder = dismissOrderOption?.Split('.'); - if (dismissedOrder != null && dismissedOrder.Length > 0) - { - string dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : null; - if (dismissedOrderIdentifier == null || dismissedOrderIdentifier != Order?.Identifier) { return false; } - string dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : null; - if (dismissedOrderOption == null && string.IsNullOrEmpty(OrderOption)) { return true; } - return dismissedOrderOption == OrderOption; - } - else - { - return false; - } - } + public override void Dispose() { Sprite?.Remove(); } } - class Order + class OrderPrefab : PrefabWithUintIdentifier { - public static Dictionary Prefabs { get; private set; } - public static Dictionary> OrderCategoryIcons { get; private set; } - public static List PrefabList { get; private set; } - public static Order GetPrefab(string identifier) - { - if (!Prefabs.TryGetValue(identifier, out Order order)) - { - DebugConsole.ThrowError($"Cannot find an order with the identifier '{identifier}'!"); - } - return order; - } + public readonly static PrefabCollection Prefabs = new PrefabCollection(); - public Order Prefab { get; private set; } + public readonly static Identifier DismissalIdentifier = "dismissed".ToIdentifier(); + public static OrderPrefab Dismissal => Prefabs[DismissalIdentifier]; - public readonly string Name; + public readonly OrderCategory? Category; + public readonly Identifier CategoryIdentifier; + + public readonly LocalizedString Name; /// /// Name that can be used with the contextual version of the order /// - public readonly string ContextualName; + public readonly LocalizedString ContextualName; public readonly Sprite SymbolSprite; public readonly Type ItemComponentType; public readonly bool CanTypeBeSubclass; - public readonly ImmutableArray TargetItems; - public readonly ImmutableArray RequireItems; - private readonly Dictionary> OptionTargetItems; + public readonly ImmutableArray TargetItems; + public readonly ImmutableArray RequireItems; + private readonly ImmutableDictionary> OptionTargetItems; public bool HasOptionSpecificTargetItems => OptionTargetItems != null && OptionTargetItems.Any(); - public readonly string Identifier; - - private Color? color; + private readonly Color? color; public Color Color { get @@ -122,49 +71,39 @@ namespace Barotrauma { return color.Value; } - else if (Category.HasValue && OrderCategoryIcons.TryGetValue((OrderCategory)Category, out Tuple sprite)) + else if (OrderCategoryIcon.OrderCategoryIcons.ContainsKey(CategoryIdentifier)) { - return sprite.Item2; + return OrderCategoryIcon.OrderCategoryIcons[Category.ToIdentifier()].Color; } else { return Color.White; } } - private set - { - color = value; - } } - //if true, the order is issued to all available characters - public bool TargetAllCharacters { get; } + public readonly bool TargetAllCharacters; public bool IsReport => TargetAllCharacters && !MustSetTarget; + public bool IsDismissal => Identifier == DismissalIdentifier; + public readonly float FadeOutTime; - public Entity TargetEntity; - public ItemComponent TargetItemComponent; public readonly bool UseController; - public readonly string[] ControllerTags; - public Controller ConnectedController; - public Character OrderGiver; + public readonly ImmutableArray ControllerTags; - public OrderCategory? Category { get; private set; } - - //legacy support /// /// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs. /// - public readonly string[] AppropriateJobs; - public readonly string[] Options; - public readonly string[] HiddenOptions; - public readonly string[] AllOptions; - private readonly Dictionary OptionNames; + public readonly ImmutableArray AppropriateJobs; + public readonly ImmutableArray Options; + public readonly ImmutableArray HiddenOptions; + public readonly ImmutableArray AllOptions; + public readonly ListDictionary OptionNames; - public readonly Dictionary OptionSprites; + public readonly ImmutableDictionary OptionSprites; public readonly bool MustSetTarget; /// @@ -172,40 +111,18 @@ namespace Barotrauma /// Note: if MustSetTarget is true, CanBeGeneralized will always be false. /// public readonly bool CanBeGeneralized; - public readonly string AppropriateSkill; + public readonly Identifier AppropriateSkill; public readonly bool Hidden; public readonly bool IgnoreAtOutpost; - public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; - public bool IsPrefab { get; private set; } + public bool HasOptions => Options.Length > 1; public readonly bool MustManuallyAssign; public readonly bool AutoDismiss; + /// /// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs. /// - public string[] PreferredJobs { get; } - - public readonly OrderTarget TargetPosition; - - private ISpatialEntity targetSpatialEntity; - public ISpatialEntity TargetSpatialEntity - { - get - { - if (targetSpatialEntity == null) - { - if (TargetType == OrderTargetType.WallSection && WallSectionIndex.HasValue) - { - targetSpatialEntity = (TargetEntity as Structure)?.Sections[WallSectionIndex.Value]; - } - else - { - targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; - } - } - return targetSpatialEntity; - } - } + public readonly ImmutableArray PreferredJobs; public enum OrderTargetType { @@ -215,7 +132,7 @@ namespace Barotrauma } public OrderTargetType TargetType { get; } public int? WallSectionIndex { get; } - public bool IsIgnoreOrder { get; } + public bool IsIgnoreOrder => Identifier == "ignorethis" || Identifier == "unignorethis"; /// /// Should the order icon be drawn when the order target is inside a container @@ -231,88 +148,10 @@ namespace Barotrauma public bool ColoredWhenControllingGiver { get; } public bool DisplayGiverInTooltip { get; } - public static void Init() + public OrderPrefab(ContentXElement orderElement, OrdersFile file) : base(file, orderElement.GetAttributeIdentifier("identifier", "")) { - Prefabs = new Dictionary(); - OrderCategoryIcons = new Dictionary>(); - - foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.Orders)) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - bool allowOverriding = false; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - string name = element.Name.ToString(); - if (name.Equals("order", StringComparison.OrdinalIgnoreCase)) - { - string identifier = element.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"Error in file {file.Path}: The order element '{name}' does not have an identifier! All orders must have a unique identifier."); - continue; - } - if (Prefabs.TryGetValue(identifier, out Order duplicate)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding an existing order '{identifier}' with another one defined in '{file.Path}'", Color.Yellow); - Prefabs.Remove(identifier); - } - else - { - DebugConsole.ThrowError($"Error in file {file.Path}: Duplicate element with the idenfitier '{identifier}' found in '{file.Path}'! All orders must have a unique identifier. Use tags to override an order with the same identifier."); - continue; - } - } - var newOrder = new Order(element); - newOrder.Prefab = newOrder; - Prefabs.Add(identifier, newOrder); - } - else if (name.Equals("ordercategory", StringComparison.OrdinalIgnoreCase)) - { - var category = (OrderCategory)Enum.Parse(typeof(OrderCategory), element.GetAttributeString("category", "undefined"), true); - if (OrderCategoryIcons.ContainsKey(category)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding an existing icon for the '{category}' order category with another one defined in '{file}'", Color.Yellow); - OrderCategoryIcons.Remove(category); - } - else - { - DebugConsole.ThrowError($"Error in file {file}: Duplicate element for the '{category}' order category found in '{file}'! All order categories must be unique. Use tags to override an order category."); - continue; - } - } - var spriteElement = element.GetChildElement("sprite"); - if (spriteElement != null) - { - var sprite = new Sprite(spriteElement, lazyLoad: true); - var color = element.GetAttributeColor("color", Color.White); - OrderCategoryIcons.Add(category, new Tuple(sprite, color)); - } - } - } - } - PrefabList = new List(Prefabs.Values); - } - - /// - /// Constructor for order prefabs - /// - private Order(XElement orderElement) - { - Identifier = orderElement.GetAttributeString("identifier", ""); - Name = TextManager.Get("OrderName." + Identifier, returnNull: true) ?? "Name not found"; - ContextualName = TextManager.Get("OrderNameContextual." + Identifier, returnNull: true) ?? Name; + Name = TextManager.Get($"OrderName.{Identifier}"); + ContextualName = TextManager.Get($"OrderNameContextual.{Identifier}"); string targetItemType = orderElement.GetAttributeString("targetitemtype", ""); if (!string.IsNullOrWhiteSpace(targetItemType)) @@ -331,15 +170,15 @@ namespace Barotrauma color = orderElement.GetAttributeColor("color"); FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f); UseController = orderElement.GetAttributeBool("usecontroller", false); - ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]); + ControllerTags = orderElement.GetAttributeIdentifierArray("controllertags", Array.Empty()).ToImmutableArray(); TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); - AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); - PreferredJobs = orderElement.GetAttributeStringArray("preferredjobs", new string[0]); - Options = orderElement.GetAttributeStringArray("options", new string[0]); - HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]); - AllOptions = Options.Concat(HiddenOptions).ToArray(); - - OptionTargetItems = new Dictionary>(); + AppropriateJobs = orderElement.GetAttributeIdentifierArray("appropriatejobs", Array.Empty()).ToImmutableArray(); + PreferredJobs = orderElement.GetAttributeIdentifierArray("preferredjobs", Array.Empty()).ToImmutableArray(); + Options = orderElement.GetAttributeIdentifierArray("options", Array.Empty()).ToImmutableArray(); + HiddenOptions = orderElement.GetAttributeIdentifierArray("hiddenoptions", Array.Empty()).ToImmutableArray(); + AllOptions = Options.Concat(HiddenOptions).ToImmutableArray(); + + var optionTargetItems = new Dictionary>(); if (orderElement.GetAttributeString("targetitems", "") is string targetItems && targetItems.Contains(';')) { string[] splitTargetItems = targetItems.Split(';'); @@ -349,46 +188,38 @@ namespace Barotrauma DebugConsole.ThrowError($"Order \"{Identifier}\" has option-specific target items, but the option count doesn't match the target item count"); } #endif - var allTargetItems = new List(); + var allTargetItems = new List(); for (int i = 0; i < AllOptions.Length; i++) { - string[] optionTargetItems = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',') : new string[0]; - for (int j = 0; j < optionTargetItems.Length; j++) + Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', ',').ToIdentifiers() : Array.Empty(); + for (int j = 0; j < optionTargetItemsSplit.Length; j++) { - optionTargetItems[j] = optionTargetItems[j].ToLowerInvariant().Trim(); - allTargetItems.Add(optionTargetItems[j]); + optionTargetItemsSplit[j] = optionTargetItemsSplit[j].Value.Trim().ToIdentifier(); + allTargetItems.Add(optionTargetItemsSplit[j]); } - OptionTargetItems.Add(AllOptions[i], optionTargetItems.ToImmutableArray()); + optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray()); } TargetItems = allTargetItems.ToImmutableArray(); } else { - TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray(); + TargetItems = orderElement.GetAttributeIdentifierArray("targetitems", Array.Empty(), trim: true).ToImmutableArray(); } - RequireItems = orderElement.GetAttributeStringArray("requireitems", new string[0], trim: true, convertToLowerInvariant: true).ToImmutableArray(); - + RequireItems = orderElement.GetAttributeIdentifierArray("requireitems", Array.Empty(), trim: true).ToImmutableArray(); + OptionTargetItems = optionTargetItems.ToImmutableDictionary(); + var category = orderElement.GetAttributeString("category", null); - if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } + this.Category = !string.IsNullOrWhiteSpace(category) ? Enum.Parse(category, true) : (OrderCategory?)null; + this.CategoryIdentifier = (this.Category?.ToString() ?? string.Empty).ToIdentifier(); MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true); - AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); + AppropriateSkill = orderElement.GetAttributeIdentifier("appropriateskill", Identifier.Empty); Hidden = orderElement.GetAttributeBool("hidden", false); IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false); - var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ?? - orderElement.GetAttributeStringArray("optionnames", new string[0]); - OptionNames = new Dictionary(); - for (int i = 0; i < Options.Length && i < optionNames.Length; i++) - { - OptionNames.Add(Options[i], optionNames[i].Trim()); - } - if (OptionNames.Count != Options.Length) - { - DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options."); - OptionNames.Clear(); - Options.ForEach(o => OptionNames.Add(o, o)); - } + OptionNames = + new ListDictionary( + TextManager.Get("OrderOptions." + Identifier).Split(',', ','), Options.Length, i => Options[i]); var spriteElement = orderElement.GetChildElement("sprite"); if (spriteElement != null) @@ -396,7 +227,7 @@ namespace Barotrauma SymbolSprite = new Sprite(spriteElement, lazyLoad: true); } - OptionSprites = new Dictionary(); + var optionSprites = new Dictionary(); if (Options != null && Options.Length > 0) { var optionSpriteElements = orderElement.GetChildElement("optionsprites")?.GetChildElements("sprite"); @@ -406,14 +237,13 @@ namespace Barotrauma { if (i >= optionSpriteElements.Count()) { break; }; var sprite = new Sprite(optionSpriteElements.ElementAt(i), lazyLoad: true); - OptionSprites.Add(Options[i], sprite); + optionSprites.Add(Options[i], sprite); } } } + OptionSprites = optionSprites.ToImmutableDictionary(); - IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); - IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement); AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100); @@ -421,92 +251,14 @@ namespace Barotrauma DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false); } - /// - /// Constructor for order instances - /// - public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null) + private bool HasSpecifiedJob(Character character, IReadOnlyList jobs) { - Prefab = prefab.Prefab ?? prefab; - - Name = prefab.Name; - ContextualName = prefab.ContextualName; - Identifier = prefab.Identifier; - ItemComponentType = prefab.ItemComponentType; - CanTypeBeSubclass = prefab.CanTypeBeSubclass; - TargetItems = prefab.TargetItems; - OptionTargetItems = prefab.OptionTargetItems; - RequireItems = prefab.RequireItems; - Options = prefab.Options; - SymbolSprite = prefab.SymbolSprite; - Color = prefab.Color; - UseController = prefab.UseController; - ControllerTags = prefab.ControllerTags; - TargetAllCharacters = prefab.TargetAllCharacters; - AppropriateJobs = prefab.AppropriateJobs; - PreferredJobs = prefab.PreferredJobs; - FadeOutTime = prefab.FadeOutTime; - MustSetTarget = prefab.MustSetTarget; - CanBeGeneralized = prefab.CanBeGeneralized; - AppropriateSkill = prefab.AppropriateSkill; - Category = prefab.Category; - MustManuallyAssign = prefab.MustManuallyAssign; - IsIgnoreOrder = prefab.IsIgnoreOrder; - DrawIconWhenContained = prefab.DrawIconWhenContained; - Hidden = prefab.Hidden; - IgnoreAtOutpost = prefab.IgnoreAtOutpost; - AssignmentPriority = prefab.AssignmentPriority; - AutoDismiss = prefab.AutoDismiss; - DisplayGiverInTooltip = prefab.DisplayGiverInTooltip; - ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver; - - OrderGiver = orderGiver; - TargetEntity = targetEntity; - if (targetItem != null) + if (jobs == null || jobs.Count == 0) { return false; } + Identifier jobIdentifier = character?.Info?.Job?.Prefab?.Identifier ?? Identifier.Empty; + if (jobIdentifier.IsEmpty) { return false; } + for (int i = 0; i < jobs.Count; i++) { - if (UseController) - { - ConnectedController = targetItem.Item?.FindController(tags: ControllerTags); - if (ConnectedController == null) - { - DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); - UseController = false; - } - } - TargetEntity = targetItem.Item; - TargetItemComponent = targetItem; - } - - TargetType = OrderTargetType.Entity; - - IsPrefab = false; - } - - /// - /// Constructor for order instances - /// - public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver) - { - TargetPosition = target; - TargetType = OrderTargetType.Position; - } - - /// - /// Constructor for order instances - /// - public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver) - { - WallSectionIndex = sectionIndex; - TargetType = OrderTargetType.WallSection; - } - - private bool HasSpecifiedJob(Character character, string[] jobs) - { - if (jobs == null || jobs.Length == 0) { return false; } - string jobIdentifier = character?.Info?.Job?.Prefab?.Identifier; - if (string.IsNullOrEmpty(jobIdentifier)) { return false; } - for (int i = 0; i < jobs.Length; i++) - { - if (jobIdentifier.Equals(jobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } + if (jobIdentifier == jobs[i]) { return true; } } return false; } @@ -515,14 +267,14 @@ namespace Barotrauma public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs); - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true) + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) { if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { // Use special dialogue when we're rearranging character orders if (!givingOrderToSelf) { - return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; + return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty).Value; } else { @@ -531,7 +283,7 @@ namespace Barotrauma } } string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}"; - if (!string.IsNullOrEmpty(orderOption)) + if (!orderOption.IsEmpty) { if (Identifier != "dismissed") { @@ -539,19 +291,16 @@ namespace Barotrauma } else { - string[] splitOption = orderOption.Split('.'); + string[] splitOption = orderOption.Value.Split('.'); if (splitOption.Length > 0) { messageTag += $".{splitOption[0]}"; } } } - string msg = TextManager.GetWithVariables(messageTag, - new string[2] { "[name]", "[roomname]" }, - new string[2] { targetCharacterName ?? string.Empty, targetRoomName ?? string.Empty }, - formatCapitals: new bool[2] { false, true }, - returnNull: true); - return msg ?? string.Empty; + return TextManager.GetWithVariables(messageTag, + ("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No), + ("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value; } /// @@ -578,7 +327,7 @@ namespace Barotrauma } /// Only returns items which are interactable for this character - public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, string orderOption = null) + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, Identifier orderOption = default) { List matchingItems = new List(); if (submarine == null) { return matchingItems; } @@ -604,7 +353,7 @@ namespace Barotrauma } /// Only returns items which are interactable for this character - public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, string orderOption = null) + public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, Identifier orderOption = default) { Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ? Submarine.MainSubs[1] : @@ -612,51 +361,41 @@ namespace Barotrauma return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor, orderOption: orderOption); } - public string GetOptionName(string id) + public LocalizedString GetOptionName(string id) { - if (Prefab == null) - { - if (OptionNames.ContainsKey(id)) { return OptionNames[id]; } - } - else - { - if (Prefab.OptionNames.ContainsKey(id)) { return Prefab.OptionNames[id]; } - } + return GetOptionName(id.ToIdentifier()); + } + + public LocalizedString GetOptionName(Identifier id) + { + if (OptionNames.ContainsKey(id)) { return OptionNames[id]; } return string.Empty; } - public string GetOptionName(int index) + public LocalizedString GetOptionName(int index) { if (index < 0 || index >= Options.Length) { return null; } - return GetOptionName(Options[index]); + return OptionNames[Options[index]]; } /// /// Used to create the order option for the Dismiss order to know which order it targets /// - /// The order to target with the dismiss order - public static string GetDismissOrderOption(OrderInfo orderInfo) + /// The order to target with the dismiss order + public static Identifier GetDismissOrderOption(Order order) { - if (orderInfo.Order != null) + Identifier option = order.Identifier; + if (order.Option != Identifier.Empty) { - string option = orderInfo.Order.Identifier; - if (!string.IsNullOrEmpty(orderInfo.OrderOption)) - { - option += $".{orderInfo.OrderOption}"; - } - return option; + option = $"{option}.{order.Option}".ToIdentifier(); } - return ""; + return option; } - public override string ToString() + + public ImmutableArray GetTargetItems(Identifier option = default) { - return $"Order ({Name})"; - } - - public ImmutableArray GetTargetItems(string option = null) - { - if (string.IsNullOrEmpty(option) || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) + if (option.IsEmpty || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) { return TargetItems; } @@ -666,16 +405,380 @@ namespace Barotrauma } } - public bool TargetItemsMatchItem(Item item, string option = null) + public bool TargetItemsMatchItem(Item item, Identifier option = default) { if (item == null) { return false; } - ImmutableArray targetItems = GetTargetItems(option); + ImmutableArray targetItems = GetTargetItems(option); return TargetItemsMatchItem(targetItems, item); } - public static bool TargetItemsMatchItem(ImmutableArray targetItems, Item item) + public static bool TargetItemsMatchItem(ImmutableArray targetItems, Item item) { return item != null && targetItems != null && targetItems.Length > 0 && (targetItems.Contains(item.Prefab.Identifier) || item.HasTag(targetItems)); } + + public override void Dispose() { } + } + + class Order + { + public readonly OrderPrefab Prefab; + public readonly Identifier Option; + public readonly int ManualPriority; + public readonly OrderType Type; + public readonly AIObjective Objective; + public bool IsCurrentOrder => Type == OrderType.Current; + public bool IsDismissal => Prefab.IsDismissal; + + public enum OrderType + { + Current, + Previous + } + + public readonly Entity TargetEntity; + public readonly ItemComponent TargetItemComponent; + public readonly Controller ConnectedController; + + public readonly Character OrderGiver; + + public readonly OrderTarget TargetPosition; + + private ISpatialEntity targetSpatialEntity; + public ISpatialEntity TargetSpatialEntity + { + get + { + if (targetSpatialEntity == null) + { + if (TargetType == OrderTargetType.WallSection && WallSectionIndex.HasValue) + { + targetSpatialEntity = (TargetEntity as Structure)?.Sections[WallSectionIndex.Value]; + } + else + { + targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; + } + } + return targetSpatialEntity; + } + } + + public Hull TargetHull => TargetEntity as Hull; + + public enum OrderTargetType + { + Entity, + Position, + WallSection + } + public readonly OrderTargetType TargetType; + public readonly int? WallSectionIndex; + + public LocalizedString Name => Prefab.Name; + public LocalizedString ContextualName => Prefab.ContextualName; + public Identifier Identifier => Prefab.Identifier; + public Type ItemComponentType => Prefab.ItemComponentType; + public bool CanTypeBeSubclass => Prefab.CanTypeBeSubclass; + public ref readonly ImmutableArray ControllerTags => ref Prefab.ControllerTags; + public ref readonly ImmutableArray TargetItems => ref Prefab.TargetItems; + public ref readonly ImmutableArray RequireItems => ref Prefab.RequireItems; + public ref readonly ImmutableArray Options => ref Prefab.Options; + public ref readonly ImmutableArray HiddenOptions => ref Prefab.HiddenOptions; + public ref readonly ImmutableArray AllOptions => ref Prefab.AllOptions; + public Sprite SymbolSprite => Prefab.SymbolSprite; + public Color Color => Prefab.Color; + public bool TargetAllCharacters => Prefab.TargetAllCharacters; + public ref readonly ImmutableArray AppropriateJobs => ref Prefab.AppropriateJobs; + public float FadeOutTime => Prefab.FadeOutTime; + public bool MustSetTarget => Prefab.MustSetTarget; + public Identifier AppropriateSkill => Prefab.AppropriateSkill; + public OrderCategory? Category => Prefab.Category; + public bool MustManuallyAssign => Prefab.MustManuallyAssign; + public bool IsIgnoreOrder => Prefab.IsIgnoreOrder; + public bool DrawIconWhenContained => Prefab.DrawIconWhenContained; + public bool Hidden => Prefab.Hidden; + public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost; + public bool IsReport => Prefab.IsReport; + public bool AutoDismiss => Prefab.AutoDismiss; + public int AssignmentPriority => Prefab.AssignmentPriority; + + public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver; + public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip; + + + public readonly bool UseController; + + /// + /// Constructor for order instances + /// + public Order(OrderPrefab prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + : this(prefab, Identifier.Empty, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } + + + public Order(OrderPrefab prefab, Identifier option, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + : this(prefab, option, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } + + public Order(OrderPrefab prefab, OrderTarget target, Character orderGiver = null) + : this(prefab, prefab.Options.FirstOrDefault(), 0, OrderType.Current, null, target, orderGiver) { } + + public Order(OrderPrefab prefab, Identifier option, OrderTarget target, Character orderGiver = null) + : this(prefab, option, 0, OrderType.Current, null, target, orderGiver) { } + + public Order(OrderPrefab prefab, Structure wall, int? sectionIndex, Character orderGiver = null) + : this(prefab, Identifier.Empty, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } + + public Order(OrderPrefab prefab, Identifier option, Structure wall, int? sectionIndex, Character orderGiver = null) + : this(prefab, option, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } + + public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + { + Prefab = prefab; + Option = option; + ManualPriority = manualPriority; + Type = orderType; + Objective = aiObjective; + + UseController = Prefab.UseController; + + OrderGiver = orderGiver; + TargetEntity = targetEntity; + if (targetItem != null) + { + if (UseController) + { + ConnectedController = targetItem.Item?.FindController(tags: ControllerTags); + if (ConnectedController == null) + { + DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); + UseController = false; + } + } + TargetEntity = targetItem.Item; + TargetItemComponent = targetItem; + } + + TargetType = OrderTargetType.Entity; + } + + public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null) + : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: null, targetItem: null, orderGiver) + { + TargetPosition = target; + TargetType = OrderTargetType.Position; + } + + public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null) + : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: wall, null, orderGiver: orderGiver) + { + WallSectionIndex = sectionIndex; + TargetType = OrderTargetType.WallSection; + } + + private Order( + Order other, + OrderPrefab prefab = null, + Identifier option = default, + int? manualPriority = null, + OrderType? type = null, + AIObjective objective = null, + Entity targetEntity = null, + ItemComponent targetItemComponent = null, + Controller connectedController = null, + Character orderGiver = null, + OrderTarget targetPosition = null, + OrderTargetType? targetType = null, + int? wallSectionIndex = null, + bool? useController = null) + { + Prefab = prefab ?? other.Prefab; + Option = option.IfEmpty(other.Option); + ManualPriority = manualPriority ?? other.ManualPriority; + Type = type ?? other.Type; + Objective = objective ?? other.Objective; + + TargetEntity = targetEntity ?? other.TargetEntity; + TargetItemComponent = targetItemComponent ?? other.TargetItemComponent; + ConnectedController = connectedController ?? other.ConnectedController; + + OrderGiver = orderGiver ?? other.OrderGiver; + + TargetPosition = targetPosition ?? other.TargetPosition; + + TargetType = targetType ?? other.TargetType; + WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex; + + UseController = useController ?? other.UseController; + } + + public Order WithOption(Identifier option) + { + return new Order(this, option: option); + } + + public Order WithManualPriority(int newPriority) + { + return new Order(this, manualPriority: newPriority); + } + + public Order WithOrderGiver(Character orderGiver) + { + return new Order(this, orderGiver: orderGiver); + } + + public Order WithObjective(AIObjective objective) + { + return new Order(this, objective: objective); + } + + public Order WithTargetEntity(Entity entity) + { + return new Order(this, targetEntity: entity); + } + + public Order WithTargetSpatialEntity(ISpatialEntity spatialEntity) + { + if (spatialEntity is WallSection wallSection) + { + Structure wall = wallSection.Wall; + int sectionIndex = wall.Sections.IndexOf(wallSection); + return WithWallSection(wall, sectionIndex); + } + else if (spatialEntity is Entity entity) + { + return WithTargetEntity(entity); + } + else if (spatialEntity is OrderTarget orderTarget) + { + return WithTargetPosition(orderTarget); + } + + throw new InvalidOperationException($"Unexpected input type: {spatialEntity.GetType().Name}"); + } + + public Order WithItemComponent(Item item, ItemComponent component = null) + { + return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item)); + } + + public Order WithWallSection(Structure wall, int? sectionIndex) + { + return new Order(this, targetEntity: wall, wallSectionIndex: sectionIndex, targetType: OrderTargetType.WallSection); + } + + public Order WithType(OrderType type) + { + return new Order(this, type: type); + } + + public Order WithTargetPosition(OrderTarget targetPosition) + { + return new Order(this, targetPosition: targetPosition); + } + + public Order Clone() + { + return new Order(this); + } + + public Order GetDismissal() + { + if (IsDismissal) { throw new InvalidOperationException("Attempted to dismiss a dismissal order"); } + return new Order(this, prefab: OrderPrefab.Prefabs["dismissed"], option: GetDismissOrderOption(this)); + } + + public bool HasAppropriateJob(Character character) + => Prefab.HasAppropriateJob(character); + + public bool HasPreferredJob(Character character) + => Prefab.HasPreferredJob(character); + + public string GetChatMessage( + string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true) + => Prefab.GetChatMessage(targetCharacterName, targetRoomName, givingOrderToSelf, orderOption, isNewOrder); + + /// + /// Get the target item component based on the target item type + /// + public ItemComponent GetTargetItemComponent(Item item) + { + return Prefab.GetTargetItemComponent(item); + } + + public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent) + { + return Prefab.TryGetTargetItemComponent(item, out firstMatchingComponent); + } + + /// Only returns items which are interactable for this character + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null) + { + return Prefab.GetMatchingItems(submarine, mustBelongToPlayerSub, requiredTeam, interactableFor); + } + + + /// Only returns items which are interactable for this character + public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null) + { + return Prefab.GetMatchingItems(mustBelongToPlayerSub, interactableFor); + } + + public LocalizedString GetOptionName(string id) + { + return Prefab.GetOptionName(id); + } + + public LocalizedString GetOptionName(Identifier id) + { + return Prefab.GetOptionName(id); + } + + public LocalizedString GetOptionName(int index) + { + return Prefab.GetOptionName(index); + } + + /// + /// Used to create the order option for the Dismiss order to know which order it targets + /// + /// The order to target with the dismiss order + public static Identifier GetDismissOrderOption(Order order) + { + return OrderPrefab.GetDismissOrderOption(order); + } + + public bool MatchesOrder(Identifier orderIdentifier, Identifier orderOption) => + orderIdentifier == Identifier && orderOption == Option; + + /*public bool MatchesOrder(Order order, Identifier option) => + order != null && MatchesOrder(order.Identifier, option);*/ + + public bool MatchesOrder(Order order) => + order != null && MatchesOrder(order.Identifier, order.Option); + + public bool MatchesDismissedOrder(Identifier dismissOrderOption) + { + Identifier[] dismissedOrder = dismissOrderOption.Value.Split('.').Select(s => s.ToIdentifier()).ToArray(); + if (dismissedOrder != null && dismissedOrder.Length > 0) + { + Identifier dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : Identifier.Empty; + if (dismissedOrderIdentifier == Identifier.Empty || dismissedOrderIdentifier != Identifier) { return false; } + Identifier dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : Identifier.Empty; + if (dismissedOrderOption == Identifier.Empty && Option == Identifier.Empty) { return true; } + return dismissedOrderOption == Option; + } + else + { + return false; + } + } + + public ImmutableArray GetTargetItems(Identifier option = default) + => Prefab.GetTargetItems(option); + + public override string ToString() + { + return $"Order ({Name})"; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 079b59da6..1105eab19 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -93,15 +93,15 @@ namespace Barotrauma } Rate = element.GetAttributeFloat("rate", 0.016f); totalCommonness = 0.0f; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "item": - string identifier = subElement.GetAttributeString("identifier", ""); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); Item newItemToProduce = new Item { - Prefab = string.IsNullOrEmpty(identifier) ? null : ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")), + Prefab = identifier.IsEmpty ? null : ItemPrefab.Find("", subElement.GetAttributeIdentifier("identifier", Identifier.Empty)), Commonness = subElement.GetAttributeFloat("commonness", 0.0f) }; totalCommonness += newItemToProduce.Commonness; @@ -134,8 +134,8 @@ namespace Barotrauma aggregate += Items[i].Commonness; if (aggregate >= r && Items[i].Prefab != null) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); - Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier); + Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); break; } } @@ -174,7 +174,7 @@ namespace Barotrauma PlayForce = element.GetAttributeFloat("playforce", 15.0f); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { @@ -202,7 +202,7 @@ namespace Barotrauma } } - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName); } public StatusIndicatorType GetCurrentStatusIndicatorType() @@ -218,7 +218,7 @@ namespace Barotrauma bool success = OnEat(item.GetTags()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier); } return success; } @@ -226,28 +226,28 @@ namespace Barotrauma public bool OnEat(Character character) { if (character == null || !character.IsDead) { return false; } - bool success = OnEat("dead"); + bool success = OnEat("dead".ToIdentifier()); if (success) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName); } return success; } - private bool OnEat(IEnumerable tags) + private bool OnEat(IEnumerable tags) { - foreach (string tag in tags) + foreach (Identifier tag in tags) { if (OnEat(tag)) { return true; } } return false; } - private bool OnEat(string tag) + public bool OnEat(Identifier tag) { for (int i = 0; i < foods.Count; i++) { - if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase)) + if (tag == foods[i].Tag) { Hunger += foods[i].Hunger; Happiness += foods[i].Happiness; @@ -352,7 +352,7 @@ namespace Barotrauma } else if (Hunger < MaxHunger * 0.1f) { - character.CharacterHealth.ReduceAffliction(null, null, 8.0f * deltaTime); + character.CharacterHealth.ReduceAllAfflictionsOnAllLimbs(8.0f * deltaTime); } if (character.SelectedBy != null) @@ -404,7 +404,7 @@ namespace Barotrauma public static void LoadPets(XElement petsElement) { - foreach (XElement subElement in petsElement.Elements()) + foreach (var subElement in petsElement.Elements()) { string speciesName = subElement.GetAttributeString("speciesname", ""); string seed = subElement.GetAttributeString("seed", "123"); @@ -418,9 +418,9 @@ namespace Barotrauma else { //try to find a spawnpoint in the main sub - var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandom(); + var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced(); //if not found, try any player sub (shuttle/drone etc) - spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom(); + spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } var pet = Character.Create(speciesName, spawnPos, seed); @@ -439,7 +439,7 @@ namespace Barotrauma var inventoryElement = subElement.Element("inventory"); if (inventoryElement != null) { - pet.SpawnInventoryItems(pet.Inventory, inventoryElement); + pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 8aca8933a..95ff37b9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public const float MaxImportance = 100f; public const float MinImportance = 0f; - public Order SuggestedOrderPrefab { get; } + public Order SuggestedOrder { get; } private float importance; public float Importance @@ -25,11 +25,11 @@ namespace Barotrauma public float CurrentRedundancy { get; set; } public readonly ShipCommandManager shipCommandManager; - public string Option { get; set; } + public Identifier Option => SuggestedOrder.Option; public Character OrderedCharacter { get; set; } public Order CurrentOrder { get; private set; } - public ItemComponent TargetItemComponent { get; protected set; } - public Item TargetItem { get; protected set; } + public ItemComponent TargetItemComponent => SuggestedOrder.TargetItemComponent; + public Item TargetItem => SuggestedOrder.TargetEntity as Item; public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected protected virtual Character CommandingCharacter => shipCommandManager.character; @@ -38,25 +38,28 @@ namespace Barotrauma public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies public virtual bool AllowEasySwitching => false; - public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null) + public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrder) { this.shipCommandManager = shipCommandManager; - SuggestedOrderPrefab = suggestedOrderPrefab; - Option = option; + SuggestedOrder = suggestedOrder; } public void SetOrder(Character orderedCharacter) { OrderedCharacter = orderedCharacter; - if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrderPrefab, Option))) + if (OrderedCharacter.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrders.None(o => o.MatchesOrder(SuggestedOrder.Identifier, Option))) { if (orderedCharacter != CommandingCharacter) { - CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); + CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); } - CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); - OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter); - OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5); + CurrentOrder = SuggestedOrder + .WithOption(Option) + .WithItemComponent(TargetItem, TargetItemComponent) + .WithOrderGiver(CommandingCharacter) + .WithManualPriority(CharacterInfo.HighestManualOrderPriority); + OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter); + OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5); } TimeSinceLastAttempt = 0f; } @@ -113,7 +116,7 @@ namespace Barotrauma } // accept only the highest priority order - if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder) + if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder) { #if DEBUG ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs index f588de938..501ac74ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs @@ -4,11 +4,7 @@ namespace Barotrauma { abstract class ShipIssueWorkerItem : ShipIssueWorker { - public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option) - { - TargetItemComponent = targetItemComponent; - TargetItem = targetItem; - } + public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } protected override bool IsIssueViable() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs index c227317ca..d4d1ad1ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs @@ -12,7 +12,7 @@ namespace Barotrauma public override bool AllowEasySwitching => true; - public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { } + public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } float GetTargetingImportance(Entity entity) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs index 9f6cbe6ec..9dc7dc398 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerPowerUpReactor.cs @@ -4,9 +4,7 @@ namespace Barotrauma { class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem { - public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) - { - } + public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } public override void CalculateImportanceSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs index e69eba53f..aa6328e96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerSteer.cs @@ -7,7 +7,7 @@ namespace Barotrauma // The AI could be set to steer automatically through a specialized job or autonomous objectives // but the logic involved doesn't really allow that without some annoyingly specific changes // hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong - public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { } + public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order) : base(shipCommandManager, order) { } public override void CalculateImportanceSpecific() { if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 2cc21e90e..d171db72a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -95,7 +95,7 @@ namespace Barotrauma public static void ShipCommandLog(string text) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage(text); } @@ -251,14 +251,14 @@ namespace Barotrauma if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold) { - IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true); + IEnumerable bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true); foreach (Character orderedCharacter in bestCharacters) { float issueApplicability = mostImportantIssue.Importance; // prefer not to switch if not qualified - issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f; + issueApplicability *= mostImportantIssue.SuggestedOrder.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f; ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter); @@ -276,7 +276,7 @@ namespace Barotrauma } // give slight preference if not qualified for current job - issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f; + issueApplicability += occupiedIssue.SuggestedOrder.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f; // prefer not to switch orders unless considerably more important issueApplicability -= IssueDevotionBuffer; @@ -312,9 +312,9 @@ namespace Barotrauma #if DEBUG ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter); #endif - Order orderPrefab = Order.GetPrefab("dismissed"); + var order = new Order(OrderPrefab.Dismissal, null).WithManualPriority(3).WithOrderGiver(character); //character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); - shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character); + shipIssueWorker.OrderedCharacter.SetOrder(order); shipIssueWorker.RemoveOrder(); break; } @@ -346,18 +346,21 @@ namespace Barotrauma if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { - ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup")); + var order = new Order(OrderPrefab.Prefabs["operatereactor"], "powerup".ToIdentifier(), reactor.Item, reactor); + ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, order)); } if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent() is Steering steeringComponent) { steering = steeringComponent; - ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical")); + var order = new Order(OrderPrefab.Prefabs["steer"], "navigatetactical".ToIdentifier(), nav, steeringComponent); + ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order)); } foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret"))) { - ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent())); + var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent()); + ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); } int crewSizeModifier = 2; @@ -365,14 +368,16 @@ namespace Barotrauma ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this); for (int i = 0; i < crewSizeModifier; i++) { - ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks)); + var order = new Order(OrderPrefab.Prefabs["fixleaks"], null); + ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, order, shipGlobalIssueFixLeaks)); } shipGlobalIssues.Add(shipGlobalIssueFixLeaks); ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this); for (int i = 0; i < crewSizeModifier; i++) { - ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems)); + var order = new Order(OrderPrefab.Prefabs["repairsystems"], null); + ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, order, shipGlobalIssueRepairSystems)); } shipGlobalIssues.Add(shipGlobalIssueRepairSystems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 3fe39c57b..9de13ceed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -31,11 +31,11 @@ namespace Barotrauma private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity); - private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); - private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag)); + private static IEnumerable GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag)); - private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); + private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); public static WreckAI Create(Submarine wreck) { @@ -54,14 +54,14 @@ namespace Barotrauma return; } var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); - var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.Server); + var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.ServerAndClient); if (brainPrefab == null) { DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); return; } allItems = Wreck.GetItems(false); - thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab)); + thalamusItems = allItems.FindAll(i => IsThalamus(((MapEntity)i).Prefab)); hulls.AddRange(Wreck.GetHulls(false)); var potentialBrainHulls = new List<(Hull hull, float weight)>(); brain = new Item(brainPrefab, Vector2.Zero, Wreck); @@ -103,12 +103,12 @@ namespace Barotrauma potentialBrainHulls.Add((hull, weight)); } } - Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.Server); - var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p)); + Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Select(pbh => pbh.hull).ToList(), potentialBrainHulls.Select(pbh => pbh.weight).ToList(), Rand.RandSync.ServerAndClient); + var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(IsThalamus); if (brainHull == null) { DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room."); - brainHull = hulls.GetRandom(Rand.RandSync.Server); + brainHull = hulls.GetRandom(Rand.RandSync.ServerAndClient); } if (brainHull == null) { @@ -118,12 +118,12 @@ namespace Barotrauma brainHull.WaterVolume = brainHull.Volume; brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); brain.CurrentHull = brainHull; - var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.Server); + var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.ServerAndClient); if (backgroundPrefab != null) { new Structure(brainHull.Rect, backgroundPrefab, Wreck); } - var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.Server); + var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.ServerAndClient); if (horizontalWallPrefab != null) { int height = (int)horizontalWallPrefab.Size.Y; @@ -132,7 +132,7 @@ namespace Barotrauma new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); } - var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.Server); + var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.ServerAndClient); if (verticalWallPrefab != null) { int width = (int)verticalWallPrefab.Size.X; @@ -162,7 +162,7 @@ namespace Barotrauma { if (container.Inventory.GetItemAt(i) != null) { continue; } if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab ip && container.CanBeContained(ip, i) && - Config.ForbiddenAmmunition.None(id => id.Equals(ip.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) + Config.ForbiddenAmmunition.None(id => id == ip.Identifier), Rand.RandSync.ServerAndClient) is ItemPrefab ammoPrefab) { Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) @@ -272,7 +272,7 @@ namespace Barotrauma cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside); for (int i = 0; i < cellsOutside; i++) { - ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null); + ISpatialEntity targetEntity = wayPoints.GetRandomUnsynced(wp => wp.CurrentHull == null); if (targetEntity == null) { break; } if (!TrySpawnCell(out _, targetEntity)) { break; } } @@ -310,7 +310,7 @@ namespace Barotrauma // but as long as spawning is handled via status effects, I don't know if there is any better way. // In practice there shouldn't be terminal cells from different thalamus organisms at the same time. // And if there was, the distance check should prevent killing the agents of a different organism. - if (character.SpeciesName.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase)) + if (character.SpeciesName == Config.OffensiveAgent) { // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. float maxDistance = Sonar.DefaultSonarRange; @@ -341,7 +341,7 @@ namespace Barotrauma public static void RemoveThalamusItems(Submarine wreck) { List thalamusItems = new List(); - foreach (var wreckAiConfig in WreckAIConfig.List) + foreach (var wreckAiConfig in WreckAIConfig.Prefabs) { thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity)); } @@ -391,7 +391,7 @@ namespace Barotrauma cellSpawnTimer -= deltaTime; if (cellSpawnTimer < 0) { - TrySpawnCell(out _, spawnOrgans.GetRandom()); + TrySpawnCell(out _, spawnOrgans.GetRandomUnsynced()); cellSpawnTimer = GetSpawnTime(); } } @@ -403,8 +403,8 @@ namespace Barotrauma if (targetEntity == null) { targetEntity = - wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? - hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; + wayPoints.GetRandomUnsynced(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? + hulls.GetRandomUnsynced(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; } if (targetEntity == null) { return false; } if (targetEntity is Hull h) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index e2a9c9b0a..92634f40a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using System; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -6,131 +7,97 @@ using System.Xml.Linq; namespace Barotrauma { - class WreckAIConfig : ISerializableEntity + class WreckAIConfig : PrefabWithUintIdentifier, ISerializableEntity { + public readonly static PrefabCollection Prefabs = new PrefabCollection(); + public string Name => "Wreck AI Config"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - [Serialize("", false)] - public string Entity { get; private set; } + public Identifier Entity => Identifier; - [Serialize("", false)] - public string DefensiveAgent { get; private set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier DefensiveAgent { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string OffensiveAgent { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string Brain { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string Spawner { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string BrainRoomBackground { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string BrainRoomVerticalWall { get; private set; } - [Serialize("", false)] + [Serialize("", IsPropertySaveable.No)] public string BrainRoomHorizontalWall { get; private set; } - [Serialize(60f, false)] + [Serialize(60f, IsPropertySaveable.No)] public float AgentSpawnDelay { get; private set; } - [Serialize(0.5f, false)] + [Serialize(0.5f, IsPropertySaveable.No)] public float AgentSpawnDelayRandomFactor { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AgentSpawnDelayDifficultyMultiplier { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AgentSpawnCountDifficultyMultiplier { get; private set; } - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int MinAgentsPerBrainRoom { get; private set; } - [Serialize(3, false)] + [Serialize(3, IsPropertySaveable.No)] public int MaxAgentsPerRoom { get; private set; } - [Serialize(2, false)] + [Serialize(2, IsPropertySaveable.No)] public int MinAgentsOutside { get; private set; } - [Serialize(5, false)] + [Serialize(5, IsPropertySaveable.No)] public int MaxAgentsOutside { get; private set; } - [Serialize(3, false)] + [Serialize(3, IsPropertySaveable.No)] public int MinAgentsInside { get; private set; } - [Serialize(10, false)] + [Serialize(10, IsPropertySaveable.No)] public int MaxAgentsInside { get; private set; } - [Serialize(15, false)] + [Serialize(15, IsPropertySaveable.No)] public int MaxAgentCount { get; private set; } - [Serialize(100f, false)] + [Serialize(100f, IsPropertySaveable.No)] public float MinWaterLevel { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool KillAgentsWhenEntityDies { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float DeadEntityColorMultiplier { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float DeadEntityColorFadeOutTime { get; private set; } - public readonly string[] ForbiddenAmmunition; + public readonly Identifier[] ForbiddenAmmunition; - public static List List + public static WreckAIConfig GetRandom() => Prefabs.GetRandom(Rand.RandSync.ServerAndClient); + + protected override Identifier DetermineIdentifier(XElement element) { - get - { - if (paramsList == null) - { - LoadAll(); - } - return paramsList; - } + return element.GetAttributeIdentifier("Entity", base.DetermineIdentifier(element)); } - private static List paramsList; - - public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server); - - public WreckAIConfig(XElement element) + public WreckAIConfig(ContentXElement element, WreckAIConfigFile file) : base(file, element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true); + ForbiddenAmmunition = XMLExtensions.GetAttributeIdentifierArray(element, "ForbiddenAmmunition", Array.Empty()); } - public static void LoadAll() - { - paramsList = new List(); - var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig); - if (files.None()) - { - DebugConsole.ThrowError("Cannot find any Wreck AI config!"); - return; - } - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (mainElement.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - paramsList.Clear(); - DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow); - } - else if (paramsList.Any()) - { - DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'"); - } - paramsList.Add(new WreckAIConfig(mainElement)); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 6904391ca..7981478e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -11,8 +11,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) + public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) + : base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) { InitProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs index 3880a0dea..967f1de57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AIChatMessage.cs @@ -13,14 +13,14 @@ namespace Barotrauma /// An arbitrary identifier that can be used to determine what kind of a message this is /// and prevent characters from saying the same kind of line too often. /// - public readonly string Identifier; + public readonly Identifier Identifier; public ChatMessageType? MessageType; public float SendDelay; public double SendTime; - public AIChatMessage(string message, ChatMessageType? type, string identifier = "", float delay = 0.0f) + public AIChatMessage(string message, ChatMessageType? type, Identifier identifier = default, float delay = 0.0f) { Message = message; MessageType = type; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 074817047..50af42736 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -22,8 +22,9 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName); - if (character.VariantOf != null) + #warning TODO: this is kinda janky, this should probably be done better + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName)); + if (!character.VariantOf.IsEmpty) { _ragdollParams.ApplyVariantScale(character.Params.VariantFile); } @@ -421,7 +422,7 @@ namespace Barotrauma } //only one limb left, the character is now full eaten - Entity.Spawner?.AddToRemoveQueue(target); + Entity.Spawner?.AddEntityToRemoveQueue(target); if (Character.AIController is EnemyAIController enemyAi) { @@ -432,10 +433,10 @@ namespace Barotrauma } else //sever a random joint { - target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom()); + target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandomUnsynced()); } } - } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index a02fefc3c..c82919b64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -25,7 +25,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName); + _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.SpeciesName); } return _ragdollParams; } @@ -178,6 +178,14 @@ namespace Barotrauma else if (Crouching) { shoulderHeight -= 0.15f; + if (Crouching) + { + bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f); + if (!movingHorizontally) + { + shoulderHeight -= HumanCrouchParams.MoveDownAmountWhenStationary; + } + } } return Collider.SimPosition + new Vector2( @@ -1401,8 +1409,8 @@ namespace Barotrauma else { //stabilize the oxygen level but don't allow it to go positive and revive the character yet - float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill; - stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax); + float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill; + stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax); character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we } @@ -1426,23 +1434,23 @@ namespace Barotrauma targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); cprPump = 0; - if (skill < CPRSettings.DamageSkillThreshold) + if (skill < CPRSettings.Active.DamageSkillThreshold) { target.LastDamageSource = null; target.DamageLimb( targetTorso.WorldPosition, targetTorso, - new[] { CPRSettings.InsufficientSkillAffliction.Instantiate((CPRSettings.DamageSkillThreshold - skill) * CPRSettings.DamageSkillMultiplier, source: character) }, + new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) }, 0.0f, true, 0.0f, attacker: null); } if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code { - float reviveChance = skill * CPRSettings.ReviveChancePerSkill; - reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent); - reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.ReviveChanceMin, CPRSettings.ReviveChanceMax); + float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill; + reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent); + reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax); if (powerfulCPR) { reviveChance *= 2.0f; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) <= reviveChance) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance) { //increase oxygen and clamp it above zero // -> the character should be revived if there are no major afflictions in addition to lack of oxygen @@ -1463,7 +1471,7 @@ namespace Barotrauma target.CharacterHealth.CalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { - character.Info?.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive); + character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive); SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 2dc2c80e7..39fd2305e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -68,7 +68,7 @@ namespace Barotrauma "Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } - return new Limb[0]; + return Array.Empty(); } return limbs; } @@ -423,13 +423,13 @@ namespace Barotrauma #endif var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); - if (characterPrefab?.XDocument != null) + if (characterPrefab?.ConfigElement != null) { - var mainElement = characterPrefab.XDocument.Root.IsOverride() ? characterPrefab.XDocument.Root.FirstElement() : characterPrefab.XDocument.Root; + var mainElement = characterPrefab.ConfigElement; foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } - AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this); + AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this); } } } @@ -1408,7 +1408,7 @@ namespace Barotrauma #else DebugConsole.NewMessage(errorMsg.Replace("[name]", Character.Name), Color.Red); #endif - GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", Character.SpeciesName.Value)); if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 18b72752a..080c14292 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -77,32 +77,32 @@ namespace Barotrauma partial class Attack : ISerializableEntity { - [Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable] + [Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable] public AttackContext Context { get; private set; } - [Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable] + [Serialize(AttackTarget.Any, IsPropertySaveable.Yes, description: "Does the attack target only specific targets?"), Editable] public AttackTarget TargetType { get; private set; } - [Serialize(LimbType.None, true, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "To which limb is the attack aimed at? If not defined or set to none, the closest limb is used (default)."), Editable] public LimbType TargetLimbType { get; private set; } - [Serialize(HitDetection.Distance, true, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable] + [Serialize(HitDetection.Distance, IsPropertySaveable.Yes, description: "Collision detection is more accurate, but it only affects targets that are in contact with the limb."), Editable] public HitDetection HitDetectionType { get; private set; } - [Serialize(AIBehaviorAfterAttack.FallBack, true, description: "The preferred AI behavior after the attack."), Editable] + [Serialize(AIBehaviorAfterAttack.FallBack, IsPropertySaveable.Yes, description: "The preferred AI behavior after the attack."), Editable] public AIBehaviorAfterAttack AfterAttack { get; set; } - [Serialize(0f, true, description: "A delay before reacting after performing an attack."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "A delay before reacting after performing an attack."), Editable] public float AfterAttackDelay { get; set; } - [Serialize(false, true, description: "Should the AI try to turn around when aiming with this attack?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to turn around when aiming with this attack?"), Editable] public bool Reverse { get; private set; } - [Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable] public bool Retreat { get; private set; } private float _range; - [Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float Range { get => _range * RangeMultiplier; @@ -110,48 +110,48 @@ namespace Barotrauma } private float _damageRange; - [Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float DamageRange { get => _damageRange * RangeMultiplier; set => _damageRange = value; } - [Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } - [Serialize(5f, true, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(5f, IsPropertySaveable.Yes, description: "How long the AI waits between the attacks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float CoolDown { get; set; } = 5; - [Serialize(0f, true, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float SecondaryCoolDown { get; set; } = 0; - [Serialize(0f, true, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float CoolDownRandomFactor { get; private set; } = 0; - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool FullSpeedAfterAttack { get; private set; } private float _structureDamage; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float StructureDamage { get => _structureDamage * DamageMultiplier; set => _structureDamage = value; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool EmitStructureDamageParticles { get; private set; } private float _itemDamage; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage { get =>_itemDamage * DamageMultiplier; set => _itemDamage = value; } - [Serialize(0.0f, true, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)] public float Penetration { get; private set; } /// @@ -169,28 +169,28 @@ namespace Barotrauma /// public float ImpactMultiplier { get; set; } = 1; - [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool Ranged { get; set; } - [Serialize(false, true, description:"Only affects ranged attacks.")] + [Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks.")] public bool AvoidFriendlyFire { get; set; } - [Serialize(20f, true)] + [Serialize(20f, IsPropertySaveable.Yes)] public float RequiredAngle { get; set; } /// /// Legacy support. Use Afflictions. /// - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float Stun { get; private set; } - [Serialize(false, true, description: "Can damage only Humans."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable] public bool OnlyHumans { get; private set; } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string ApplyForceOnLimbs { get @@ -211,54 +211,54 @@ namespace Barotrauma } } - [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldStart { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldMiddle { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable] public Vector2 RootForceWorldEnd { get; private set; } - [Serialize(TransitionMode.Linear, true, description:""), Editable] + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable] public TransitionMode RootTransitionEasing { get; private set; } - [Serialize(0.0f, true, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)] public float Torque { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool ApplyForcesOnlyOnce { get; private set; } - [Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float TargetImpulse { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetImpulseWorld { get; private set; } - [Serialize(0.0f, true, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer)."), Editable(-1000.0f, 1000.0f)] public float TargetForce { get; private set; } - [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetForceWorld { get; private set; } - [Serialize(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float SubmarineImpactMultiplier { get; private set; } - [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SeverLimbsProbability { get; set; } // TODO: disabled because not synced - //[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + //[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] //public float StickChance { get; set; } public float StickChance => 0f; - [Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } - [Serialize(false, true, description: ""), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: ""), Editable] public bool Blink { get; private set; } public IEnumerable StatusEffects @@ -268,11 +268,11 @@ namespace Barotrauma public string Name => "Attack"; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; - } = new Dictionary(); + } = new Dictionary(); //the indices of the limbs Force is applied on //(if none, force is applied only to the limb the attack is attached to) @@ -347,11 +347,12 @@ namespace Barotrauma Penetration = Penetration; } - public Attack(XElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) + public Attack(ContentXElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) { SourceItem = sourceItem; } - public Attack(XElement element, string parentDebugName) + + public Attack(ContentXElement element, string parentDebugName) { Deserialize(element); @@ -372,7 +373,7 @@ namespace Barotrauma InitProjSpecific(element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -395,7 +396,7 @@ namespace Barotrauma else { string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); + afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); @@ -415,7 +416,7 @@ namespace Barotrauma } } } - partial void InitProjSpecific(XElement element = null); + partial void InitProjSpecific(ContentXElement element); public void ReloadAfflictions(XElement element) { @@ -424,13 +425,8 @@ namespace Barotrauma { AfflictionPrefab afflictionPrefab; Affliction affliction; - string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); - if (afflictionPrefab == null) - { - DebugConsole.ThrowError($"Couldn't find the affliction with the identifier {afflictionIdentifier} referenced in {element.Document.ParseContentPathFromUri()}"); - continue; - } + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 7850c8f59..3c0307d51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using FarseerPhysics.Dynamics; using Barotrauma.Extensions; +using System.Collections.Immutable; using Barotrauma.Abilities; #if SERVER using System.Text; @@ -26,7 +27,7 @@ namespace Barotrauma partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable { - public static List CharacterList = new List(); + public readonly static List CharacterList = new List(); partial void UpdateLimbLightSource(Limb limb); @@ -103,8 +104,8 @@ namespace Barotrauma public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; public bool IsEscorted { get; set; } - public readonly Dictionary Properties; - public Dictionary SerializableProperties + public readonly Dictionary Properties; + public Dictionary SerializableProperties { get { return Properties; } } @@ -116,7 +117,7 @@ namespace Barotrauma protected Key[] keys; - public HumanPrefab Prefab; + public HumanPrefab HumanPrefab; private CharacterTeamType teamID; public CharacterTeamType TeamID @@ -157,7 +158,9 @@ namespace Barotrauma return; } // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) - SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, orderGiver: this, speak: false); + var order = new Order(OrderPrefab.Dismissal, Identifier.Empty, + manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); + SetOrder(order, speak: false); #if SERVER GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange }); @@ -222,7 +225,9 @@ namespace Barotrauma if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior { - SetOrder(Order.GetPrefab("fightintruders"), orderOption: null, priority: 3, orderGiver: this, speak: false); + var order = new Order(OrderPrefab.Prefabs["fightintruders"], Identifier.Empty, + manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); + SetOrder(order, speak: false); } } } @@ -270,11 +275,11 @@ namespace Barotrauma public float InvisibleTimer; - private readonly CharacterPrefab prefab; + public readonly CharacterPrefab Prefab; public readonly CharacterParams Params; - public string SpeciesName => Params?.SpeciesName ?? "null"; - public string Group => Params.Group; + public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier(); + public Identifier Group => Params.Group; public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; @@ -318,22 +323,13 @@ namespace Barotrauma set; } - public string TraitorCurrentObjective = ""; - public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); - - /// - /// Can be used by status effects to check the character's gender - /// - public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male; - /// - /// Can be used by status effects to check the character's gender - /// - public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female; + public LocalizedString TraitorCurrentObjective = ""; + public bool IsHuman => SpeciesName == CharacterPrefab.HumanSpeciesName; private float attackCoolDown; - public List CurrentOrders => Info?.CurrentOrders; - public bool IsDismissed => !GetCurrentOrderWithTopPriority().HasValue; + public List CurrentOrders => Info?.CurrentOrders; + public bool IsDismissed => GetCurrentOrderWithTopPriority() == null; private readonly List statusEffects = new List(); @@ -380,13 +376,13 @@ namespace Barotrauma } } - public string VariantOf { get; private set; } + public Identifier VariantOf => Prefab.VariantOf; public string Name { get { - return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName; + return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName.Value; } } @@ -401,19 +397,19 @@ namespace Barotrauma } if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } - var displayName = Params.DisplayName; - if (string.IsNullOrWhiteSpace(displayName)) + LocalizedString displayName = Params.DisplayName; + if (displayName.IsNullOrWhiteSpace()) { if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride)) { - displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true); + displayName = TextManager.Get($"Character.{SpeciesName}"); } else { - displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}", returnNull: true); + displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}"); } } - return string.IsNullOrWhiteSpace(displayName) ? Name : displayName; + return displayName.IsNullOrWhiteSpace() ? Name : displayName.Value; } } @@ -423,7 +419,7 @@ namespace Barotrauma get { if (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises) return Name; - return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName; + return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName.Value; } } @@ -441,7 +437,7 @@ namespace Barotrauma } } - public string ConfigPath => Params.File; + public string ConfigPath => Params.File.Path.Value; public float Mass { @@ -456,7 +452,7 @@ namespace Barotrauma public bool ResetInteract; //text displayed when the character is highlighted if custom interact is set - public string customInteractHUDText; + public LocalizedString CustomInteractHUDText { get; private set; } private Action onCustomInteract; public ConversationAction ActiveConversation; @@ -896,7 +892,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce( "Character.SimPosition:AccessRemoved", GameAnalyticsManager.ErrorSeverity.Error, - errorMsg.Replace("[name]", SpeciesName) + "\n" + Environment.StackTrace.CleanupStackTrace()); + errorMsg.Replace("[name]", SpeciesName.Value) + "\n" + Environment.StackTrace.CleanupStackTrace()); accessRemovedCharacterErrorShown = true; } return Vector2.Zero; @@ -958,34 +954,42 @@ namespace Barotrauma { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { - speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); + speciesName = Path.GetFileNameWithoutExtension(speciesName); } + return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll); + } + public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + { var prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab == null) { DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace); return null; } + return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll); + } + public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + { Character newCharacter = null; - if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (prefab.Identifier != CharacterPrefab.HumanSpeciesName) { - var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else { - newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll); + newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll); } float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; @@ -1012,8 +1016,8 @@ namespace Barotrauma void AddDamageReduction(string affliction, float amount, ActionType actionType = ActionType.Always) { newCharacter.statusEffects.Add(StatusEffect.Load( - new XElement("StatusEffect", new XAttribute("type", actionType), new XAttribute("target", "Character"), - new XElement("ReduceAffliction", new XAttribute("identifier", affliction), new XAttribute("amount", amount))), $"automatic damage reduction ({affliction})")); + new XElement("StatusEffect", new XAttribute("type", actionType), new XAttribute("target", "Character"), + new XElement("ReduceAffliction", new XAttribute("identifier", affliction), new XAttribute("amount", amount))).FromPackage(null), $"automatic damage reduction ({affliction})")); } #if SERVER @@ -1025,12 +1029,11 @@ namespace Barotrauma return newCharacter; } - protected Character(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) + protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) : base(null, id) { - VariantOf = prefab.VariantOf; this.Seed = seed; - this.prefab = prefab; + this.Prefab = prefab; MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); IsRemotePlayer = isRemotePlayer; @@ -1042,15 +1045,15 @@ namespace Barotrauma Properties = SerializableProperty.GetProperties(this); - Params = new CharacterParams(prefab.FilePath); + Params = new CharacterParams(prefab.ContentFile as CharacterFile); Info = characterInfo; - speciesName = VariantOf ?? speciesName; + Identifier speciesName = prefab.Identifier; - if (speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (VariantOf == CharacterPrefab.HumanSpeciesName || speciesName == CharacterPrefab.HumanSpeciesName) { - if (VariantOf != null) + if (!VariantOf.IsEmpty) { DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!"); } @@ -1069,19 +1072,14 @@ namespace Barotrauma keys[i] = new Key((InputType)i); } - var rootElement = prefab.XDocument.Root; - if (VariantOf != null) - { - rootElement = CharacterPrefab.FindBySpeciesName(VariantOf)?.XDocument?.Root; - } - var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; + var mainElement = prefab.ConfigElement; InitProjSpecific(mainElement); - List inventoryElements = new List(); + List inventoryElements = new List(); List inventoryCommonness = new List(); - List healthElements = new List(); + List healthElements = new List(); List healthCommonness = new List(); - foreach (XElement subElement in mainElement.Elements()) + foreach (var subElement in mainElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1100,13 +1098,13 @@ namespace Barotrauma } if (Params.VariantFile != null) { - XElement overrideElement = Params.VariantFile.Root; + var overrideElement = Params.VariantFile.Root.FromPackage(Params.MainElement.ContentPackage); // Only override if the override file contains matching elements if (overrideElement.GetChildElement("inventory") != null) { inventoryElements.Clear(); inventoryCommonness.Clear(); - foreach (XElement subElement in overrideElement.GetChildElements("inventory")) + foreach (var subElement in overrideElement.GetChildElements("inventory")) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1121,7 +1119,7 @@ namespace Barotrauma { healthElements.Clear(); healthCommonness.Clear(); - foreach (XElement subElement in overrideElement.GetChildElements("health")) + foreach (var subElement in overrideElement.GetChildElements("health")) { healthElements.Add(subElement); healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f)); @@ -1157,13 +1155,13 @@ namespace Barotrauma var matchingAffliction = AfflictionPrefab.List .Where(p => p is AfflictionPrefabHusk) .Select(p => p as AfflictionPrefabHusk) - .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase))); - string nonHuskedSpeciesName = string.Empty; + .FirstOrDefault(p => p.TargetSpecies.Any(t => t == AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p))); + Identifier nonHuskedSpeciesName = Identifier.Empty; if (matchingAffliction == null) { DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!"); // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. - nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler"; + nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier(); speciesName = nonHuskedSpeciesName; } else @@ -1172,7 +1170,7 @@ namespace Barotrauma } if (ragdollParams == null && prefab.VariantOf == null) { - string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; + Identifier name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName; ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams(name) : RagdollParams.GetDefaultRagdollParams(name) as RagdollParams; } if (Params.HasInfo && info == null) @@ -1214,14 +1212,20 @@ namespace Barotrauma } ApplyStatusEffects(ActionType.OnSpawn, 1.0f); } - partial void InitProjSpecific(XElement mainElement); + partial void InitProjSpecific(ContentXElement mainElement); public void ReloadHead(int? headId = null, int hairIndex = -1, int beardIndex = -1, int moustacheIndex = -1, int faceAttachmentIndex = -1) { if (Info == null) { return; } var head = AnimController.GetLimb(LimbType.Head); if (head == null) { return; } - Info.RecreateHead(headId ?? Info.HeadSpriteId, Info.Race, Info.Gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + HashSet tags = Info.Head.Preset.TagSet.ToHashSet(); + if (headId.HasValue) + { + tags.RemoveWhere(t => t.StartsWith("variant")); + tags.Add($"variant{headId.Value}".ToIdentifier()); + } + Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); #if CLIENT head.RecreateSprites(); #endif @@ -1239,19 +1243,19 @@ namespace Barotrauma head.OtherWearables.Clear(); //if the element has not been set at this point, the character has no hair and the index should be zero (= no hair) - if (info.FaceAttachment == null) { info.FaceAttachmentIndex = 0; } - Info.FaceAttachment?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment))); - if (info.BeardElement == null) { info.BeardIndex = 0; } - Info.BeardElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard))); - if (info.MoustacheElement == null) { info.MoustacheIndex = 0; } - Info.MoustacheElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache))); - if (info.HairElement == null) { info.HairIndex = 0; } - Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); + if (info.Head.FaceAttachment == null) { info.Head.FaceAttachmentIndex = 0; } + Info.Head.FaceAttachment?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment))); + if (info.Head.BeardElement == null) { info.Head.BeardIndex = 0; } + Info.Head.BeardElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard))); + if (info.Head.MoustacheElement == null) { info.Head.MoustacheIndex = 0; } + Info.Head.MoustacheElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache))); + if (info.Head.HairElement == null) { info.Head.HairIndex = 0; } + Info.Head.HairElement?.GetChildElements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair))); #if CLIENT - if (info.Head?.HairWithHatElement != null) + if (info.Head?.HairWithHatElement?.GetChildElement("sprite") != null) { - head.HairWithHatSprite = new WearableSprite(info.Head?.HairWithHatElement.Element("sprite"), WearableType.Hair); + head.HairWithHatSprite = new WearableSprite(info.Head.HairWithHatElement.GetChildElement("sprite"), WearableType.Hair); } head.EnableHuskSprite = Params.Husk; head.LoadHerpesSprite(); @@ -1391,9 +1395,9 @@ namespace Barotrauma public override string ToString() { #if DEBUG - return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName; + return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName.Value; #else - return SpeciesName; + return SpeciesName.Value; #endif } @@ -1416,12 +1420,15 @@ namespace Barotrauma } if (createNetworkEvent && (GameMain.NetworkMember?.IsServer ?? false)) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties["tags"] }); + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties[nameof(item.Tags).ToIdentifier()] }); } } } - public float GetSkillLevel(string skillIdentifier) + public float GetSkillLevel(string skillIdentifier) => + GetSkillLevel(skillIdentifier.ToIdentifier()); + + public float GetSkillLevel(Identifier skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); @@ -2094,6 +2101,9 @@ namespace Barotrauma } public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) + => HasEquippedItem(tagOrIdentifier.ToIdentifier(), allowBroken, slotType); + + public bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null) { if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) @@ -2164,8 +2174,8 @@ namespace Barotrauma /// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex. /// Returns false while running and true when done. /// - public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable identifiers = null, bool ignoreBroken = true, - IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, + public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable identifiers = null, bool ignoreBroken = true, + IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null) { if (itemIndex == 0) @@ -2194,13 +2204,13 @@ namespace Barotrauma if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } if (IsItemTakenBySomeoneElse(item)) { continue; } - float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; - if (itemPriority <= 0) { continue; } Entity rootInventoryOwner = item.GetRootInventoryOwner(); if (rootInventoryOwner is Item ownerItem) { if (!ownerItem.IsInteractable(this)) { continue; } } + float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; + if (itemPriority <= 0) { continue; } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition; float yDist = Math.Abs(refPos.Y - itemPos.Y); @@ -2316,7 +2326,7 @@ namespace Barotrauma } bool insideTrigger = item.IsInsideTrigger(upperBodyPosition) || item.IsInsideTrigger(lowerBodyPosition); - if (item.Prefab.Triggers.Count > 0 && !insideTrigger && item.Prefab.RequireBodyInsideTrigger) { return false; } + if (item.Prefab.Triggers.Length > 0 && !insideTrigger && item.Prefab.RequireBodyInsideTrigger) { return false; } Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height); @@ -2362,7 +2372,10 @@ namespace Barotrauma itemPosition -= Submarine.SimPosition; } var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); - if (body != null && body.UserData as Item != item && Submarine.LastPickedFixture?.UserData as Item != item) { return false; } + if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item) + { + return false; + } } return true; @@ -2373,10 +2386,10 @@ namespace Barotrauma /// /// Action invoked when another character interacts with this one. T1 = this character, T2 = the interacting character /// Displayed on the character when highlighted. - public void SetCustomInteract(Action onCustomInteract, string hudText) + public void SetCustomInteract(Action onCustomInteract, LocalizedString hudText) { this.onCustomInteract = onCustomInteract; - customInteractHUDText = hudText; + CustomInteractHUDText = hudText; } private void TransformCursorPos() @@ -2449,7 +2462,7 @@ namespace Barotrauma { FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null; if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } - float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); + float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes @@ -2627,7 +2640,7 @@ namespace Barotrauma c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) { - Spawner?.AddToRemoveQueue(c); + Spawner?.AddEntityToRemoveQueue(c); } } else if (closestPlayerDist < c.Params.DisableDistance * 0.9f) @@ -2653,7 +2666,7 @@ namespace Barotrauma c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) { - Entity.Spawner?.AddToRemoveQueue(c); + Entity.Spawner?.AddEntityToRemoveQueue(c); } } else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f)) @@ -2892,7 +2905,7 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam); - partial void SetOrderProjSpecific(Order order, string orderOption, int priority); + partial void SetOrderProjSpecific(Order order); public void AddAttacker(Character character, float damage) @@ -3051,7 +3064,7 @@ namespace Barotrauma if (Submarine != null && !ignoreThresholds) { subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); - if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; } + if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; } } if (SelectedBy != null) @@ -3064,14 +3077,14 @@ namespace Barotrauma if (distToClosestPlayer > Params.DisableDistance) { //despawn in 1 minute if very far from all human players - despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); + despawnTimer = Math.Max(despawnTimer, GameSettings.CurrentConfig.CorpseDespawnDelay - 60.0f); } float despawnPriority = 1.0f; - if (subCorpseCount > GameMain.Config.CorpsesPerSubDespawnThreshold) + if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast) - despawnPriority += (subCorpseCount - GameMain.Config.CorpsesPerSubDespawnThreshold) / (float)GameMain.Config.CorpsesPerSubDespawnThreshold; + despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold; } if (AIController is EnemyAIController) { @@ -3080,20 +3093,20 @@ namespace Barotrauma } despawnTimer += deltaTime * despawnPriority; - if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; } + if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; } if (IsHuman) { var containerPrefab = ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ?? - (MapEntityPrefab.Find(null, identifier: "metalcrate") as ItemPrefab); + (MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab); if (containerPrefab == null) { DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red); } else { - Spawner?.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned); + Spawner?.AddItemToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned); } void onItemContainerSpawned(Item item) @@ -3102,7 +3115,7 @@ namespace Barotrauma item.UpdateTransform(); item.AddTag("name:" + Name); - if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); } + if (info?.Job != null) { item.AddTag($"job:{info.Job.Name}"); } var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } @@ -3117,12 +3130,12 @@ namespace Barotrauma } } - Spawner.AddToRemoveQueue(this); + Spawner.AddEntityToRemoveQueue(this); } public void DespawnNow(bool createNetworkEvents = true) { - despawnTimer = GameMain.Config.CorpseDespawnDelay; + despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); Spawner.Update(createNetworkEvents); } @@ -3133,7 +3146,7 @@ namespace Barotrauma List list = new List(CharacterList); foreach (Character character in list) { - if (character.prefab == prefab) + if (character.Prefab == prefab) { character.Remove(); } @@ -3186,17 +3199,14 @@ namespace Barotrauma } /// Force an order to be set for the character, bypassing hearing checks - public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true, bool force = false) + public void SetOrder(Order order, bool speak = true, bool force = false) { + var orderGiver = order?.OrderGiver; //set the character order only if the character is close enough to hear the message if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; } if (order != null) { - if (order.OrderGiver != orderGiver) - { - order.OrderGiver = orderGiver; - } if (order.AutoDismiss) { switch (order.Category) @@ -3211,29 +3221,29 @@ namespace Barotrauma if (!HumanAIController.IsActive(character)) { continue; } foreach (var currentOrder in character.CurrentOrders) { - if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } - if (currentOrder.Order.Identifier != order.Identifier) { continue; } - if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } - if (!currentOrder.Order.AutoDismiss) { continue; } - character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force); + if (currentOrder == null) { continue; } + if (currentOrder.Category != OrderCategory.Operate) { continue; } + if (currentOrder.Identifier != order.Identifier) { continue; } + if (currentOrder.TargetEntity != order.TargetEntity) { continue; } + if (!currentOrder.AutoDismiss) { continue; } + character.SetOrder(currentOrder.GetDismissal(), speak: speak, force: force); break; } } break; case OrderCategory.Movement: // If there character has another movement order, dismiss that order - OrderInfo? orderToReplace = null; + Order orderToReplace = null; foreach (var currentOrder in CurrentOrders) { - if (currentOrder.Order == null) { continue; } - if (currentOrder.Order.Category != OrderCategory.Movement) { continue; } + if (currentOrder == null) { continue; } + if (currentOrder.Category != OrderCategory.Movement) { continue; } orderToReplace = currentOrder; break; } - if (orderToReplace.HasValue && orderToReplace.Value.Order.AutoDismiss) + if (orderToReplace is { AutoDismiss: true }) { - SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(orderToReplace.Value), orderToReplace.Value.ManualPriority, this, speak: speak, force: force); + SetOrder(orderToReplace.GetDismissal(), speak: speak, force: force); } break; } @@ -3241,45 +3251,37 @@ namespace Barotrauma } // Prevent adding duplicate orders - RemoveDuplicateOrders(order, orderOption); + bool wasDuplicate = RemoveDuplicateOrders(order); + AddCurrentOrder(order); - OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority); - AddCurrentOrder(newOrderInfo); - - if (orderGiver != null) + if (orderGiver != null && order.Identifier != "dismissed" && !wasDuplicate) { var abilityOrderedCharacter = new AbilityOrderedCharacter(this); orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); - if (orderGiver.LastOrderedCharacter != this) + if (order.OrderGiver.LastOrderedCharacter != this) { - orderGiver.SecondLastOrderedCharacter = orderGiver.LastOrderedCharacter; - orderGiver.LastOrderedCharacter = this; + order.OrderGiver.SecondLastOrderedCharacter = order.OrderGiver.LastOrderedCharacter; + order.OrderGiver.LastOrderedCharacter = this; } } if (AIController is HumanAIController humanAI) { - humanAI.SetOrder(order, orderOption, priority, orderGiver, speak); + humanAI.SetOrder(order, speak); } - SetOrderProjSpecific(order, orderOption, priority); + SetOrderProjSpecific(order); } - /// Force an order to be set for the character, bypassing hearing checks - public void SetOrder(OrderInfo orderInfo, Character orderGiver, bool speak = true, bool force = false) + private void AddCurrentOrder(Order newOrder) { - SetOrder(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, orderGiver, speak: speak, force: force); - } - - private void AddCurrentOrder(OrderInfo newOrder) - { - if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed") + if (newOrder == null || newOrder.Identifier == "dismissed") { - if (!string.IsNullOrEmpty(newOrder.OrderOption)) + if (newOrder.Option != Identifier.Empty) { - if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.OrderOption))) + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.Option))) { - var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.OrderOption)); + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.Option)); int dismissedOrderPriority = dismissedOrderInfo.ManualPriority; CurrentOrders.Remove(dismissedOrderInfo); for (int i = 0; i < CurrentOrders.Count; i++) @@ -3287,7 +3289,7 @@ namespace Barotrauma var orderInfo = CurrentOrders[i]; if (orderInfo.ManualPriority < dismissedOrderPriority) { - CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority + 1); } } } @@ -3304,7 +3306,7 @@ namespace Barotrauma var orderInfo = CurrentOrders[i]; if (orderInfo.ManualPriority <= newOrder.ManualPriority) { - CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority - 1); + CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority - 1); } } CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); @@ -3314,56 +3316,60 @@ namespace Barotrauma } } - private void RemoveDuplicateOrders(Order order, string option) + private bool RemoveDuplicateOrders(Order order) { + bool removed = false; int? priorityOfRemoved = null; for (int i = CurrentOrders.Count - 1; i >= 0; i--) { var orderInfo = CurrentOrders[i]; - if (order?.Identifier == orderInfo.Order?.Identifier) + if (order.Identifier == orderInfo.Identifier) { priorityOfRemoved = orderInfo.ManualPriority; CurrentOrders.RemoveAt(i); + removed = true; break; } } - if (!priorityOfRemoved.HasValue) { return; } + if (!priorityOfRemoved.HasValue) { return removed; } for (int i = 0; i < CurrentOrders.Count; i++) { var orderInfo = CurrentOrders[i]; if (orderInfo.ManualPriority < priorityOfRemoved.Value) { - CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + CurrentOrders[i] = orderInfo.WithManualPriority(orderInfo.ManualPriority + 1); } } CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); // Sort the current orders so the one with the highest priority comes first CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + + return removed; } - public OrderInfo? GetCurrentOrderWithTopPriority() + public Order GetCurrentOrderWithTopPriority() { return GetCurrentOrder(orderInfo => { - if (orderInfo.Order == null) { return false; } - if (orderInfo.Order.Identifier == "dismissed") { return false; } + if (orderInfo == null) { return false; } + if (orderInfo.Identifier == "dismissed") { return false; } if (orderInfo.ManualPriority < 1) { return false; } return true; }); } - public OrderInfo? GetCurrentOrder(Order order, string option) + public Order GetCurrentOrder(Order order) { return GetCurrentOrder(orderInfo => { - return orderInfo.MatchesOrder(order, option); + return orderInfo.MatchesOrder(order); }); } - private OrderInfo? GetCurrentOrder(Func predicate) + private Order GetCurrentOrder(Func predicate) { if (CurrentOrders != null && CurrentOrders.Any(predicate)) { @@ -3378,17 +3384,22 @@ namespace Barotrauma private readonly List aiChatMessageQueue = new List(); //key = identifier, value = time the message was sent - private readonly Dictionary prevAiChatMessages = new Dictionary(); + private readonly Dictionary prevAiChatMessages = new Dictionary(); - public void DisableLine(string identifier) + public void DisableLine(Identifier identifier) { - if (!string.IsNullOrEmpty(identifier)) + if (identifier != Identifier.Empty) { prevAiChatMessages[identifier] = (float)Timing.TotalTime; } } - public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f) + public void DisableLine(string identifier) + { + DisableLine(identifier.ToIdentifier()); + } + + public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, Identifier identifier = default, float minDurationBetweenSimilar = 0.0f) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (string.IsNullOrEmpty(message)) { return; } @@ -3402,7 +3413,7 @@ namespace Barotrauma } //already sent a similar message a moment ago - if (!string.IsNullOrEmpty(identifier) && minDurationBetweenSimilar > 0.0f && + if (identifier != Identifier.Empty && minDurationBetweenSimilar > 0.0f && (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier))) { return; @@ -3448,7 +3459,7 @@ namespace Barotrauma { sent.SendTime = Timing.TotalTime; aiChatMessageQueue.Remove(sent); - if (!string.IsNullOrEmpty(sent.Identifier)) + if (sent.Identifier != Identifier.Empty) { prevAiChatMessages[sent.Identifier] = (float)sent.SendTime; } @@ -3456,15 +3467,15 @@ namespace Barotrauma if (prevAiChatMessages.Count > 100) { - List toRemove = new List(); - foreach (KeyValuePair prevMessage in prevAiChatMessages) + HashSet toRemove = new HashSet(); + foreach (KeyValuePair prevMessage in prevAiChatMessages) { if (prevMessage.Value < Timing.TotalTime - 60.0f) { toRemove.Add(prevMessage.Key); } } - foreach (string identifier in toRemove) + foreach (Identifier identifier in toRemove) { prevAiChatMessages.Remove(identifier); } @@ -3497,7 +3508,7 @@ namespace Barotrauma { string errorMsg = "Tried to apply an attack to a removed character ([name]).\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); - GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName)); + GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); return new AttackResult(); } @@ -3674,17 +3685,17 @@ namespace Barotrauma CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill); if (!IsOnPlayerTeam) { return; } - if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } - GameMain.Config.KilledCreatures.Add(target.SpeciesName); + if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; } + CreatureMetrics.Instance.Killed.Add(target.SpeciesName); AddEncounter(target); } public void AddEncounter(Character other) { if (!IsOnPlayerTeam) { return; } - if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(other.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } - GameMain.Config.EncounteredCreatures.Add(other.SpeciesName); - GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName); + if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; } + CreatureMetrics.Instance.Encountered.Add(other.SpeciesName); + CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName); } public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false) @@ -3789,14 +3800,14 @@ namespace Barotrauma if (healthChange < 0.0f) { float attackerSkillLevel = attacker.GetSkillLevel("weapons"); - attacker.Info?.IncreaseSkillLevel("weapons", + attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(), -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f)); } } else if (healthChange > 0.0f) { float attackerSkillLevel = attacker.GetSkillLevel("medical"); - attacker.Info?.IncreaseSkillLevel("medical", + attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(), healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f)); } } @@ -3988,7 +3999,7 @@ namespace Barotrauma if (GameAnalyticsManager.SendUserStatistics) { string causeOfDeathStr = causeOfDeathAffliction == null ? - causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Replace(" ", ""); + causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", ""); string characterType = GetCharacterType(this); GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + causeOfDeathStr); @@ -4157,7 +4168,7 @@ namespace Barotrauma { foreach (Item item in Inventory.AllItems) { - Spawner?.AddToRemoveQueue(item); + Spawner?.AddItemToRemoveQueue(item); } } @@ -4219,14 +4230,14 @@ namespace Barotrauma SaveInventory(Inventory, Info?.InventoryData); } - public void SpawnInventoryItems(Inventory inventory, XElement itemData) + public void SpawnInventoryItems(Inventory inventory, ContentXElement itemData) { SpawnInventoryItemsRecursive(inventory, itemData, new List()); } - private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element, List extraDuffelBags) + private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List extraDuffelBags) { - foreach (XElement itemElement in element.Elements()) + foreach (var itemElement in element.Elements()) { var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true, idRemap: IdRemap.DiscardId); if (newItem == null) { continue; } @@ -4253,7 +4264,7 @@ namespace Barotrauma if (slotIndices.Contains(i)) { var existingItem = inventory.GetItemAt(i); - if (existingItem != null && existingItem != newItem && (existingItem.prefab != newItem.prefab || existingItem.Prefab.MaxStackSize == 1)) + if (existingItem != null && existingItem != newItem && (((MapEntity)existingItem).Prefab != ((MapEntity)newItem).Prefab || existingItem.Prefab.MaxStackSize == 1)) { DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\""); existingItem.Drop(null, createNetworkEvent: false); @@ -4306,7 +4317,7 @@ namespace Barotrauma { // In case the inventory capacity is smaller than it was when saving: // 1) Spawn a new duffel bag if none yet spawned or if the existing ones aren't enough - if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.Find(null, "duffelbag") is ItemPrefab duffelBagPrefab) + if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.FindByIdentifier("duffelbag".ToIdentifier()) is ItemPrefab duffelBagPrefab) { var hull = Hull.FindHull(WorldPosition, guess: CurrentHull); var mainSub = Submarine.MainSubs.FirstOrDefault(s => s.TeamID == TeamID); @@ -4344,7 +4355,7 @@ namespace Barotrauma int itemContainerIndex = 0; var itemContainers = newItem.GetComponents().ToList(); - foreach (XElement childInvElement in itemElement.Elements()) + foreach (var childInvElement in itemElement.Elements()) { if (itemContainerIndex >= itemContainers.Count) break; if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -4488,29 +4499,29 @@ namespace Barotrauma public void LoadTalents() { - List toBeRemoved = null; - foreach (string talent in info.UnlockedTalents) + List toBeRemoved = null; + foreach (Identifier talent in info.UnlockedTalents) { if (!GiveTalent(talent, addingFirstTime: false)) { DebugConsole.AddWarning(Name + " had talent that did not exist! Removing talent from CharacterInfo."); - toBeRemoved ??= new List(); + toBeRemoved ??= new List(); toBeRemoved.Add(talent); } } if (toBeRemoved != null) { - foreach (string removeTalent in toBeRemoved) + foreach (Identifier removeTalent in toBeRemoved) { Info.UnlockedTalents.Remove(removeTalent); } } } - public bool GiveTalent(string talentIdentifier, bool addingFirstTime = true) + public bool GiveTalent(Identifier talentIdentifier, bool addingFirstTime = true) { - TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talentIdentifier, StringComparison.OrdinalIgnoreCase)); + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier == talentIdentifier); if (talentPrefab == null) { DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); @@ -4521,7 +4532,7 @@ namespace Barotrauma public bool GiveTalent(UInt32 talentIdentifier, bool addingFirstTime = true) { - TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UIntIdentifier == talentIdentifier); + TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.UintIdentifier == talentIdentifier); if (talentPrefab == null) { DebugConsole.AddWarning($"Tried to add talent by identifier {talentIdentifier} to character {Name}, but no such talent exists."); @@ -4546,20 +4557,20 @@ namespace Barotrauma if (addingFirstTime) { OnTalentGiven(talentPrefab); - GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None") + ":" + talentPrefab.Identifier, + GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None".ToIdentifier()) + ":" + talentPrefab.Identifier, GameMain.GameSession?.Campaign?.TotalPlayTime ?? 0.0); } return true; } - public bool HasTalent(string identifier) + public bool HasTalent(Identifier identifier) { return info.UnlockedTalents.Contains(identifier); } public bool HasUnlockedAllTalents() { - if (TalentTree.JobTalentTrees.TryGetValue(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree)) { foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) { @@ -4605,7 +4616,7 @@ namespace Barotrauma } } - public bool HasRecipeForItem(string recipeIdentifier) + public bool HasRecipeForItem(Identifier recipeIdentifier) { return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier)); } @@ -4707,11 +4718,11 @@ namespace Barotrauma statValues.Add(statType, value); } } - - public static StatTypes GetSkillStatType(string skillIdentifier) + + private static StatTypes GetSkillStatType(Identifier skillIdentifier) { // Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way - switch (skillIdentifier) + switch (skillIdentifier.Value.ToLowerInvariant()) { case "electrical": return StatTypes.ElectricalSkillBonus; @@ -4745,14 +4756,14 @@ namespace Barotrauma return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } - private readonly Dictionary abilityResistances = new Dictionary(); - + private readonly Dictionary abilityResistances = new Dictionary(); + public float GetAbilityResistance(AfflictionPrefab affliction) { return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f; } - public void ChangeAbilityResistance(string resistanceId, float value) + public void ChangeAbilityResistance(Identifier resistanceId, float value) { if (abilityResistances.ContainsKey(resistanceId)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 85d189f96..e008a0432 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -11,55 +11,91 @@ using Barotrauma.Abilities; namespace Barotrauma { - public enum Gender { None, Male, Female }; - public enum Race { None, White, Black, Brown, Asian }; - + class CharacterInfoPrefab + { + public readonly ImmutableArray Heads; + public readonly ImmutableDictionary> VarTags; + public readonly Identifier MenuCategoryVar; + public readonly Identifier Pronouns; + + public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement) + { + Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray(); + VarTags = varsElement.Elements() + .Select(e => + (e.GetAttributeIdentifier("var", ""), + e.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet())) + .ToImmutableDictionary(); + MenuCategoryVar = menuCategoryElement.GetAttributeIdentifier("var", Identifier.Empty); + Pronouns = pronounsElement.GetAttributeIdentifier("vars", Identifier.Empty); + } + public string ReplaceVars(string str, CharacterInfo.HeadPreset headPreset) + { + return ReplaceVars(str, headPreset.TagSet); + } + + public string ReplaceVars(string str, ImmutableHashSet tagSet) + { + foreach (var key in VarTags.Keys) + { + str = str.Replace($"[{key}]", tagSet.FirstOrDefault(t => VarTags[key].Contains(t)).Value, StringComparison.OrdinalIgnoreCase); + } + return str; + } + } + partial class CharacterInfo { public class HeadInfo { - private int _headSpriteId; - public int HeadSpriteId + public readonly CharacterInfo CharacterInfo; + public readonly HeadPreset Preset; + + private int hairIndex; + + public int HairIndex { - get { return _headSpriteId; } + get => hairIndex; set { - _headSpriteId = Math.Max(Math.Clamp(value, (int)headSpriteRange.X, (int)headSpriteRange.Y), 1); - GetSpriteSheetIndex(); + hairIndex = value; + if (CharacterInfo.Hairs is null) + { + HairWithHatIndex = value; + return; + } + HairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", hairIndex) ?? -1; + if (HairWithHatIndex < 0 || HairWithHatIndex >= CharacterInfo.Hairs.Count) + { + HairWithHatIndex = hairIndex; + } } } - public Vector2? SheetIndex { get; private set; } - public Vector2 headSpriteRange; - public Gender gender; - public Race race; + public int HairWithHatIndex { get; private set; } + public int BeardIndex; + public int MoustacheIndex; + public int FaceAttachmentIndex; public Color HairColor; public Color FacialHairColor; public Color SkinColor; - public int HairIndex { get; set; } = -1; - public int BeardIndex { get; set; } = -1; - public int MoustacheIndex { get; set; } = -1; - public int FaceAttachmentIndex { get; set; } = -1; + public Vector2 SheetIndex => Preset.SheetIndex; - public XElement HairElement { get; set; } - public XElement HairWithHatElement { get; set; } - public XElement BeardElement { get; set; } - public XElement MoustacheElement { get; set; } - public XElement FaceAttachment { get; set; } - - public HeadInfo() { } + 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 HeadInfo(int headId, Gender gender, Race race, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) + public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) { - _headSpriteId = Math.Max(headId, 1); - this.gender = gender; - this.race = race; + CharacterInfo = characterInfo; + Preset = headPreset; HairIndex = hairIndex; BeardIndex = beardIndex; MoustacheIndex = moustacheIndex; FaceAttachmentIndex = faceAttachmentIndex; - GetSpriteSheetIndex(); } public void ResetAttachmentIndices() @@ -69,21 +105,6 @@ namespace Barotrauma MoustacheIndex = -1; FaceAttachmentIndex = -1; } - - public void GetSpriteSheetIndex() - { - if (heads != null && heads.Any()) - { - var matchingHead = heads.Keys.FirstOrDefault(h => h.ID == HeadSpriteId && IsMatchingGender(h.Gender, gender) && IsMatchingRace(h.Race, race)); - if (matchingHead != null) - { - if (heads.TryGetValue(matchingHead, out Vector2 index)) - { - SheetIndex = index; - } - } - } - } } private HeadInfo head; @@ -95,50 +116,38 @@ namespace Barotrauma if (head != value && value != null) { head = value; - if (!IsValidRace(head.race)) - { - head.race = GetRandomRace(Rand.RandSync.Unsynced); - } - CalculateHeadSpriteRange(); - Head.HeadSpriteId = value.HeadSpriteId; - RefreshHeadSprites(); + HeadSprite = null; + AttachmentSprites = null; } } } - public Dictionary Heads - { - get - { - if (heads == null) - { - LoadHeadPresets(); - } - return heads; - } - } - - private static Dictionary heads; + public CharacterInfoPrefab Prefab => CharacterPrefab.Prefabs[SpeciesName].CharacterInfoPrefab; public class HeadPreset : ISerializableEntity { - [Serialize(Race.None, false)] - public Race Race { get; private set; } + private readonly CharacterInfoPrefab characterInfoPrefab; + public Identifier MenuCategory => TagSet.First(t => characterInfoPrefab.VarTags[characterInfoPrefab.MenuCategoryVar].Contains(t)); - [Serialize(Gender.None, false)] - public Gender Gender { get; private set; } - [Serialize(0, false)] - public int ID { get; private set; } + public ImmutableHashSet TagSet { get; private set; } - [Serialize("0,0", false)] + [Serialize("", IsPropertySaveable.No)] + public string Tags + { + get { return string.Join(",", TagSet); } + private set { TagSet = value.Split(",").Select(s => s.ToIdentifier()).ToImmutableHashSet(); } + } + + [Serialize("0,0", IsPropertySaveable.No)] public Vector2 SheetIndex { get; private set; } - public string Name => $"Head Preset {Race} {Gender} {ID}"; + public string Name => $"Head Preset {Tags}"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - public HeadPreset(XElement element) + public HeadPreset(CharacterInfoPrefab charInfoPrefab, XElement element) { + characterInfoPrefab = charInfoPrefab; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } } @@ -172,37 +181,15 @@ namespace Barotrauma if (Character.Inventory != null) { + //Disguise as the ID card name if it's equipped var idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card); - if (idCard == null) { return disguiseName; } - - //Disguise as the ID card name if it's equipped - string[] readTags = idCard.Tags.Split(','); - foreach (string tag in readTags) - { - string[] s = tag.Split(':'); - if (s[0] == "name") - { - return s[1]; - } - } + return idCard?.GetComponent()?.OwnerName ?? disguiseName; } return disguiseName; } } - private string _speciesName; - public string SpeciesName - { - get - { - if (_speciesName == null) - { - _speciesName = CharacterConfigElement.GetAttributeString("speciesname", string.Empty).ToLowerInvariant(); - } - return _speciesName; - } - set { _speciesName = value; } - } + public Identifier SpeciesName { get; } /// /// Note: Can be null. @@ -215,14 +202,14 @@ namespace Barotrauma public int ExperiencePoints { get; private set; } - public HashSet UnlockedTalents { get; private set; } = new HashSet(); + public HashSet UnlockedTalents { get; private set; } = new HashSet(); /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection /// - public IEnumerable GetUnlockedTalentsInTree() + public IEnumerable GetUnlockedTalentsInTree() { - if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } + if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } return UnlockedTalents.Where(t => talentTree.TalentIsInTree(t)); } @@ -230,14 +217,21 @@ namespace Barotrauma /// /// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them /// - public IEnumerable GetEndocrineTalents() + public IEnumerable GetEndocrineTalents() { - if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } + if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty(); } return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t)); } - public int AdditionalTalentPoints { get; set; } + public const int MaxAdditionalTalentPoints = 100; + + private int additionalTalentPoints; + public int AdditionalTalentPoints + { + get { return additionalTalentPoints; } + set { additionalTalentPoints = MathHelper.Clamp(value, 0, MaxAdditionalTalentPoints); } + } private Sprite _headSprite; public Sprite HeadSprite @@ -305,7 +299,7 @@ namespace Barotrauma { if (handleBuff) { - Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).Instantiate(100f)); + Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); } idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); @@ -325,7 +319,7 @@ namespace Barotrauma if (handleBuff) { - Character.CharacterHealth.ReduceAffliction(Character.AnimController.GetLimb(LimbType.Head), "disguised", 100f); + Character.CharacterHealth.ReduceAfflictionOnLimb(Character.AnimController.GetLimb(LimbType.Head), "disguised".ToIdentifier(), 100f); } } @@ -350,7 +344,7 @@ namespace Barotrauma } } - public XElement CharacterConfigElement { get; set; } + public ContentXElement CharacterConfigElement { get; set; } public readonly string ragdollFileName = string.Empty; @@ -362,10 +356,11 @@ namespace Barotrauma public CharacterTeamType TeamID; - private NPCPersonalityTrait personalityTrait; + public NPCPersonalityTrait PersonalityTrait { get; private set; } public const int MaxCurrentOrders = 3; public static int HighestManualOrderPriority => MaxCurrentOrders; + public int GetManualOrderPriority(Order order) { if (order != null && order.AssignmentPriority < 100 && CurrentOrders.Any()) @@ -373,7 +368,7 @@ namespace Barotrauma int orderPriority = HighestManualOrderPriority; for (int i = 0; i < CurrentOrders.Count; i++) { - if (CurrentOrders[i].Order is Order currentOrder && order.AssignmentPriority >= currentOrder.AssignmentPriority) + if (order.AssignmentPriority >= CurrentOrders[i].AssignmentPriority) { break; } @@ -390,141 +385,19 @@ namespace Barotrauma } } - public List CurrentOrders { get; } = new List(); + public List CurrentOrders { get; } = new List(); //unique ID given to character infos in MP //used by clients to identify which infos are the same to prevent duplicate characters in round summary public ushort ID; - public List SpriteTags + public List SpriteTags { get; private set; } - public NPCPersonalityTrait PersonalityTrait - { - get { return personalityTrait; } - } - - /// - /// Setting the value with this property also resets the head attachments. Use Head.headSpriteId if you don't want that. - /// - public int HeadSpriteId - { - get { return Head.HeadSpriteId; } - set - { - Head.HeadSpriteId = value; - ResetHeadAttachments(); - RefreshHeadSprites(); - } - } - - public readonly bool HasGenders; - public readonly bool HasRaces; - - public Gender Gender - { - get { return Head.gender; } - set - { - Gender previousValue = Head.gender; - Head.gender = value; - if (!IsValidGender(Head.gender)) - { - Head.gender = GetDefaultGender(); - } - if (Head.gender != previousValue) - { - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - RefreshHeadSprites(); - } - } - } - - public Race Race - { - get { return Head.race; } - set - { - Race previousValue = Head.race; - Head.race = value; - if (!IsValidRace(Head.race)) - { - Head.race = GetDefaultRace(); - } - if (Head.race != previousValue) - { - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - RefreshHeadSprites(); - } - } - } - - private bool IsValidRace(Race race) => HasRaces ? race != Race.None : race == Race.None; - - private bool IsValidGender(Gender gender) => HasGenders ? gender != Gender.None : gender == Gender.None; - - private Gender GetDefaultGender() => HasGenders ? Gender.Male : Gender.None; - - private Race GetDefaultRace() => HasRaces ? Race.White : Race.None; - - public int HairIndex - { - get => Head.HairIndex; - set => Head.HairIndex = value; - } - - public int BeardIndex - { - get => Head.BeardIndex; - set => Head.BeardIndex = value; - } - - public int MoustacheIndex - { - get => Head.MoustacheIndex; - set => Head.MoustacheIndex = value; - } - - public int FaceAttachmentIndex - { - get => Head.FaceAttachmentIndex; - set => Head.FaceAttachmentIndex = value; - } - - public readonly ImmutableArray<(Color Color, float Commonness)> HairColors; - public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors; - public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors; - - public Color HairColor - { - get => Head.HairColor; - set => Head.HairColor = value; - } - - public Color FacialHairColor - { - get => Head.FacialHairColor; - set => Head.FacialHairColor = value; - } - - public Color SkinColor - { - get => Head.SkinColor; - set => Head.SkinColor = value; - } - - public XElement HairElement => Head.HairElement; - - public XElement BeardElement => Head.BeardElement; - - public XElement MoustacheElement => Head.MoustacheElement; - - public XElement FaceAttachment => Head.FaceAttachment; + public readonly bool HasSpecifierTags; private RagdollParams ragdoll; public RagdollParams Ragdoll @@ -534,8 +407,8 @@ namespace Barotrauma if (ragdoll == null) { // TODO: support for variants - string speciesName = SpeciesName; - bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)); + Identifier speciesName = SpeciesName; + bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName); ragdoll = isHumanoid ? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName) : RagdollParams.GetRagdollParams(speciesName, ragdollFileName) as RagdollParams; @@ -545,119 +418,171 @@ namespace Barotrauma set { ragdoll = value; } } - public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; + public bool IsAttachmentsLoaded => Head.HairIndex > -1 && Head.BeardIndex > -1 && Head.MoustacheIndex > -1 && Head.FaceAttachmentIndex > -1; + + public IEnumerable GetValidAttachmentElements(IEnumerable elements, HeadPreset headPreset, WearableType? wearableType = null) + => FilterElements(elements, headPreset.TagSet, wearableType); + + public int CountValidAttachmentsOfType(WearableType wearableType) + => GetValidAttachmentElements(Wearables, Head.Preset, wearableType).Count(); + + public readonly ImmutableArray<(Color Color, float Commonness)> HairColors; + public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors; + public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors; + + private void GetName(ContentPath namesFile, Rand.RandSync randSync, out string name) + { + XDocument doc = XMLExtensions.TryLoadXml(namesFile); + name = doc.Root.GetAttributeString("format", ""); + Dictionary> entries = new Dictionary>(); + foreach (var subElement in doc.Root.Elements()) + { + Identifier elemName = subElement.NameAsIdentifier(); + if (!entries.ContainsKey(elemName)) + { + entries.Add(elemName, new List()); + } + ImmutableHashSet identifiers = subElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); + if (identifiers.IsSubsetOf(Head.Preset.TagSet)) + { + entries[elemName].Add(subElement.GetAttributeString("value", "")); + } + } + + foreach (var k in entries.Keys) + { + name = name.Replace($"[{k}]", entries[k].GetRandom(randSync), StringComparison.OrdinalIgnoreCase); + } + } + + private static void LoadTagsBackwardsCompatibility(XElement element, HashSet tags) + { + //we need this to be able to load save files from + //older versions with the shittier hardcoded character + //info implementation + Identifier gender = element.GetAttributeIdentifier("gender", ""); + int headSpriteId = element.GetAttributeInt("headspriteid", -1); + if (!gender.IsEmpty) { tags.Add(gender); } + if (headSpriteId > 0) { tags.Add($"head{headSpriteId}".ToIdentifier()); } + } // talent-relevant values public int MissionsCompletedSinceDeath = 0; // Used for creating the data - public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "") + public CharacterInfo(Identifier speciesName, string name = "", string originalName = "", Either jobOrJobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, Identifier npcIdentifier = default) { - if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + JobPrefab jobPrefab = null; + Job job = null; + if (jobOrJobPrefab != null) { - speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); + jobOrJobPrefab.TryGet(out job); + jobOrJobPrefab.TryGet(out jobPrefab); } ID = idCounter; idCounter++; - _speciesName = speciesName; - SpriteTags = new List(); - XDocument doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument; - if (doc == null) { return; } - CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + SpeciesName = speciesName; + SpriteTags = new List(); + CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement; + if (CharacterConfigElement == null) { return; } // TODO: support for variants - Head = new HeadInfo(); - HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - HasRaces = CharacterConfigElement.GetAttributeBool("races", false); - SetGenderAndRace(randSync); - Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant); - HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - SetColors(); + HasSpecifierTags = CharacterConfigElement.GetAttributeBool("specifiertags", false); + if (HasSpecifierTags) + { + HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - if (!string.IsNullOrEmpty(name)) - { - Name = name; - } - else if (!string.IsNullOrEmpty(npcIdentifier) && TextManager.Get("npctitle." + npcIdentifier, true) is string npcTitle) - { - Name = npcTitle; - } - else - { - Name = GetRandomName(randSync); + var headPreset = Prefab.Heads.GetRandom(randSync); + Head = new HeadInfo(this, headPreset); + SetAttachments(randSync); + SetColors(randSync); + + Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant)); + + if (!string.IsNullOrEmpty(name)) + { + Name = name; + } + else if (!npcIdentifier.IsEmpty && TextManager.Get("npctitle." + npcIdentifier) is { Loaded: true } npcTitle) + { + Name = npcTitle.Value; + } + else + { + Name = GetRandomName(randSync); + } + + SetPersonalityTrait(); + + Salary = CalculateSalary(); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; - SetPersonalityTrait(); - Salary = CalculateSalary(); if (ragdollFileName != null) { this.ragdollFileName = ragdollFileName; } - LoadHeadAttachments(); } private void SetPersonalityTrait() - { - personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId); - } + => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet)); public string GetRandomName(Rand.RandSync randSync) { string name = ""; - if (CharacterConfigElement.Element("name") != null) + var nameElement = CharacterConfigElement.GetChildElement("names"); + if (nameElement != null) { - string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); - if (firstNamePath != "") - { - firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - name = ToolBox.GetRandomLine(firstNamePath, randSync); - } - - string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); - if (lastNamePath != "") - { - lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - if (name != "") { name += " "; } - name += ToolBox.GetRandomLine(lastNamePath, randSync); - } + GetName(nameElement.GetAttributeContentPath("path") ?? ContentPath.Empty, randSync, out name); } return name; } - public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array) - => ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), Rand.RandSync.Unsynced) + public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync) + => ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), randSync) .Color; - private void SetGenderAndRace(Rand.RandSync randSync) + private void SetAttachments(Rand.RandSync randSync) { - Head.gender = GetRandomGender(randSync); - Head.race = GetRandomRace(randSync); - CalculateHeadSpriteRange(); - HeadSpriteId = GetRandomHeadID(randSync); + LoadHeadAttachments(); + + int pickRandomIndex(IReadOnlyList list) + { + var elems = GetValidAttachmentElements(list, Head.Preset).ToArray(); + var weights = GetWeights(elems).ToArray(); + return list.IndexOf(ToolBox.SelectWeightedRandom(elems, weights, randSync)); + } + + Head.HairIndex = pickRandomIndex(Hairs); + Head.BeardIndex = pickRandomIndex(Beards); + Head.MoustacheIndex = pickRandomIndex(Moustaches); + Head.FaceAttachmentIndex = pickRandomIndex(FaceAttachments); } - private void SetColors() + private void SetColors(Rand.RandSync randSync) { - HairColor = SelectRandomColor(HairColors); - FacialHairColor = SelectRandomColor(FacialHairColors); - SkinColor = SelectRandomColor(SkinColors); + Head.HairColor = SelectRandomColor(HairColors, randSync); + Head.FacialHairColor = SelectRandomColor(FacialHairColors, randSync); + Head.SkinColor = SelectRandomColor(SkinColors, randSync); } + private bool IsColorValid(in Color clr) + => clr.R != 0 || clr.G != 0 || clr.B != 0; + private void CheckColors() { - if (HairColor == Color.Black) + if (IsColorValid(Head.HairColor)) { - HairColor = SelectRandomColor(HairColors); + Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced); } - if (FacialHairColor == Color.Black) + if (IsColorValid(Head.FacialHairColor)) { - FacialHairColor = SelectRandomColor(FacialHairColors); + Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced); } - if (SkinColor == Color.Black) + if (IsColorValid(Head.SkinColor)) { - SkinColor = SelectRandomColor(SkinColors); + Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced); } } @@ -669,97 +594,52 @@ namespace Barotrauma Name = infoElement.GetAttributeString("name", ""); OriginalName = infoElement.GetAttributeString("originalname", null); Salary = infoElement.GetAttributeInt("salary", 1000); + ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); - UnlockedTalents = new HashSet(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); + UnlockedTalents = new HashSet(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty())); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); - Enum.TryParse(infoElement.GetAttributeString("race", "None"), true, out Race race); - Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender); - _speciesName = infoElement.GetAttributeString("speciesname", null); - XDocument doc = null; - if (_speciesName != null) + HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); + LoadTagsBackwardsCompatibility(infoElement, tags); + SpeciesName = infoElement.GetAttributeIdentifier("speciesname", ""); + ContentXElement element; + if (!SpeciesName.IsEmpty) { - doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument; + element = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement; } else { // Backwards support (human only) - string file = infoElement.GetAttributeString("file", ""); - doc = XMLExtensions.TryLoadXml(file); + // Actually you know what this is backwards! + throw new InvalidOperationException("SpeciesName not defined"); } - if (doc == null) { return; } + if (element == null) { return; } // TODO: support for variants - CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - HasRaces = CharacterConfigElement.GetAttributeBool("hasraces", false); - if (!IsValidGender(gender)) + CharacterConfigElement = element; + HasSpecifierTags = CharacterConfigElement.GetAttributeBool("specifiertags", false); + if (HasSpecifierTags) { - gender = GetRandomGender(Rand.RandSync.Unsynced); - } - if (!IsValidRace(race)) - { - race = GetRandomRace(Rand.RandSync.Unsynced); - } - HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); - SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); - - RecreateHead( - infoElement.GetAttributeInt("headspriteid", 1), - race, - gender, - infoElement.GetAttributeInt("hairindex", -1), - infoElement.GetAttributeInt("beardindex", -1), - infoElement.GetAttributeInt("moustacheindex", -1), - infoElement.GetAttributeInt("faceattachmentindex", -1)); + RecreateHead( + tags.ToImmutableHashSet(), + infoElement.GetAttributeInt("hairindex", -1), + infoElement.GetAttributeInt("beardindex", -1), + infoElement.GetAttributeInt("moustacheindex", -1), + infoElement.GetAttributeInt("faceattachmentindex", -1)); - //backwards compatibility - if (infoElement.Attribute("skincolor") == null && infoElement.Attribute("race") != null) - { - string raceStr = infoElement.GetAttributeString("race", string.Empty); - Race obsoleteRace = Race.None; - Enum.TryParse(raceStr, ignoreCase: true, out obsoleteRace); - switch (obsoleteRace) + HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); + + Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); + Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White); + Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); + CheckColors(); + + if (string.IsNullOrEmpty(Name)) { - case Race.White: - case Race.None: - SkinColor = new Color(255, 215, 200, 255); - break; - case Race.Brown: - SkinColor = new Color(158, 95, 72, 255); - break; - case Race.Black: - SkinColor = new Color(153, 75, 42, 255); - break; - case Race.Asian: - SkinColor = new Color(191, 116, 61, 255); - break; - } - } - else - { - SkinColor = infoElement.GetAttributeColor("skincolor", Color.Black); - } - HairColor = infoElement.GetAttributeColor("haircolor", Color.Black); - FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Black); - CheckColors(); - - if (string.IsNullOrEmpty(Name)) - { - if (CharacterConfigElement.Element("name") != null) - { - string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); - if (firstNamePath != "") + var nameElement = CharacterConfigElement.GetChildElement("names"); + if (nameElement != null) { - firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - Name = ToolBox.GetRandomLine(firstNamePath); - } - - string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); - if (lastNamePath != "") - { - lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - if (Name != "") Name += " "; - Name += ToolBox.GetRandomLine(lastNamePath); + GetName(nameElement.GetAttributeContentPath("path") ?? ContentPath.Empty, Rand.RandSync.ServerAndClient, out Name); } } } @@ -770,16 +650,16 @@ namespace Barotrauma } StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false); - string personalityName = infoElement.GetAttributeString("personality", ""); + Identifier personalityName = infoElement.GetAttributeIdentifier("personality", ""); ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty); - if (!string.IsNullOrEmpty(personalityName)) + if (personalityName != Identifier.Empty) { - personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName); + PersonalityTrait = NPCPersonalityTrait.Get(GameSettings.CurrentConfig.Language, personalityName); } MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); - foreach (XElement subElement in infoElement.Elements()) + foreach (var subElement in infoElement.Elements()) { bool jobCreated = false; if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated) @@ -818,43 +698,26 @@ namespace Barotrauma LoadHeadAttachments(); } - public Gender GetRandomGender(Rand.RandSync randSync) - { - if (HasGenders) - { - return (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; - } - return Gender.None; - } + private List hairs; + public IReadOnlyList Hairs => hairs; + private List beards; + public IReadOnlyList Beards => beards; + private List moustaches; + public IReadOnlyList Moustaches => moustaches; + private List faceAttachments; + public IReadOnlyList FaceAttachments => faceAttachments; - public Race GetRandomRace(Rand.RandSync randSync) - { - if (HasRaces) - { - return new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); - } - return Race.None; - } - - - public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0; - - private List hairs; - private List beards; - private List moustaches; - private List faceAttachments; - - private IEnumerable wearables; - public IEnumerable Wearables + private IEnumerable wearables; + public IEnumerable Wearables { get { if (wearables == null) { - var attachments = CharacterConfigElement.Element("HeadAttachments"); + var attachments = CharacterConfigElement.GetChildElement("HeadAttachments"); if (attachments != null) { - wearables = attachments.Elements("Wearable"); + wearables = attachments.GetChildElements("Wearable"); } } return wearables; @@ -873,167 +736,77 @@ namespace Barotrauma private int GetIdentifier(string name) { - int id = ToolBox.StringToInt(name); - id ^= HeadSpriteId; - id ^= (int)Race << 6; - id ^= HairIndex << 12; - id ^= BeardIndex << 18; - id ^= MoustacheIndex << 24; - id ^= FaceAttachmentIndex << 30; + int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet)); + id ^= Head.HairIndex << 12; + id ^= Head.BeardIndex << 18; + id ^= Head.MoustacheIndex << 24; + id ^= Head.FaceAttachmentIndex << 30; if (Job != null) { - id ^= ToolBox.StringToInt(Job.Prefab.Identifier); + id ^= ToolBox.StringToInt(Job.Prefab.Identifier.Value); } return id; } - public IEnumerable FilterByTypeAndHeadID(IEnumerable elements, WearableType targetType, int headSpriteId) + public IEnumerable FilterElements(IEnumerable elements, ImmutableHashSet tags, WearableType? targetType = null) { - if (elements == null) { return elements; } - return elements.Where(e => + if (elements is null) { return null; } + return elements.Where(w => { - if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; } - int headId = e.GetAttributeInt("headid", -1); - // if the head id is less than 1, the id is not valid and the condition is ignored. - return headId < 1 || headId == headSpriteId; + if (!(targetType is null)) + { + if (Enum.TryParse(w.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; } + } + HashSet t = w.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); + LoadTagsBackwardsCompatibility(w, t); + return t.IsSubsetOf(tags); }); } - public IEnumerable FilterElementsByGenderAndRace(IEnumerable elements, Gender gender, Race race) + public void RecreateHead(ImmutableHashSet tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) { - if (elements == null) { return elements; } - return elements.Where(w => - IsMatchingGender(Enum.Parse(w.GetAttributeString("gender", "None"), ignoreCase: true), gender) && - IsMatchingRace(Enum.Parse(w.GetAttributeString("race", "None"), ignoreCase: true), race)); + HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags)); + if (headPreset == null) { headPreset = Prefab.Heads.GetRandomUnsynced(); } + head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + ReloadHeadAttachments(); } - public static bool IsMatchingGender(Gender gender, Gender myGender) => gender == Gender.None || gender == myGender; - public static bool IsMatchingRace(Race race, Race myRace) => race == Race.None || race == myRace; - - private void LoadHeadPresets() + public string ReplaceVars(string str) { - if (CharacterConfigElement == null) { return; } - heads = new Dictionary(); - var headsElement = CharacterConfigElement.GetChildElement("heads"); - if (headsElement != null) - { - foreach (var head in headsElement.GetChildElements("head")) - { - var preset = new HeadPreset(head); - heads.Add(preset, preset.SheetIndex); - } - } + return Prefab.ReplaceVars(str, Head.Preset); } - private void CalculateHeadSpriteRange() +#if CLIENT + public void RecreateHead(MultiplayerPreferences characterSettings) { - if (CharacterConfigElement == null) { return; } - Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero); - // If the range is defined, we use it as it is - if (Head.headSpriteRange != Vector2.Zero) { return; } - if (heads == null) - { - LoadHeadPresets(); - } - // If there are any head presets defined, use them. - if (heads.Any()) - { - var ids = heads.Keys.Where(h => IsMatchingRace(Race, h.Race) && IsMatchingGender(Gender, h.Gender)).Select(w => w.ID); - ids = ids.OrderBy(id => id); - if (ids.Any()) - { - Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); - } - else - { - DebugConsole.ThrowError($"[CharacterInfo] Couldn't find a head definition that matches {Race} and {Gender}!"); - } - } - // Else we calculate the range from the wearables. - if (Head.headSpriteRange == Vector2.Zero) - { - var wearableElements = Wearables; - if (wearableElements == null) { return; } - var wearables = FilterElementsByGenderAndRace(wearableElements, head.gender, head.race).ToList(); - if (wearables == null) - { - Head.headSpriteRange = Vector2.Zero; - return; - } - if (wearables.None()) - { - DebugConsole.ThrowError($"[CharacterInfo] No headidrange defined and no wearables matching the gender {Head.gender} and the race {Head.race} could be found. Total wearables found: {Wearables.Count()}."); - return; - } - else - { - // Ignore head ids that are less than 1, because they are not supported. - var ids = wearables.Select(w => w.GetAttributeInt("headid", -1)).Where(id => id > 0); - if (ids.None()) - { - DebugConsole.ThrowError($"[CharacterInfo] Wearables with matching gender and race were found but none with a valid headid! Total wearables found: {Wearables.Count()}."); - return; - } - ids = ids.OrderBy(id => id); - Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); - } - } - } + RecreateHead( + characterSettings.TagSet.ToImmutableHashSet(), + characterSettings.HairIndex, + characterSettings.BeardIndex, + characterSettings.MoustacheIndex, + characterSettings.FaceAttachmentIndex); + Head.SkinColor = characterSettings.SkinColor; + Head.HairColor = characterSettings.HairColor; + Head.FacialHairColor = characterSettings.FacialHairColor; + } +#endif + public void RecreateHead(HeadInfo headInfo) { RecreateHead( - headInfo.HeadSpriteId, - headInfo.race, - headInfo.gender, + headInfo.Preset.TagSet, headInfo.HairIndex, headInfo.BeardIndex, headInfo.MoustacheIndex, headInfo.FaceAttachmentIndex); - SkinColor = headInfo.SkinColor; - HairColor = headInfo.HairColor; - FacialHairColor = headInfo.FacialHairColor; + Head.SkinColor = headInfo.SkinColor; + Head.HairColor = headInfo.HairColor; + Head.FacialHairColor = headInfo.FacialHairColor; CheckColors(); } - /// - /// Recreates the head info and checks that everything is valid. - /// - public void RecreateHead(int headID, Race race, Gender gender, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) - { - if (!IsValidGender(gender)) - { - gender = GetRandomGender(Rand.RandSync.Unsynced); - } - if (!IsValidRace(race)) - { - race = GetRandomRace(Rand.RandSync.Unsynced); - } - if (heads == null) - { - LoadHeadPresets(); - } - Color skin = Color.Black; - Color hair = Color.Black; - Color facialHair = Color.Black; - if (head != null) - { - skin = head.SkinColor; - hair = head.HairColor; - facialHair = head.FacialHairColor; - } - head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex) - { - SkinColor = skin, - HairColor = hair, - FacialHairColor = facialHair - }; - CalculateHeadSpriteRange(); - ReloadHeadAttachments(); - RefreshHead(); - } - /// /// Reloads the head sprite and the attachment sprites. /// @@ -1043,23 +816,21 @@ namespace Barotrauma RefreshHeadSprites(); } - partial void LoadHeadSpriteProjectSpecific(XElement limbElement); + partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement); private void LoadHeadSprite() { - foreach (XElement limbElement in Ragdoll.MainElement.Elements()) + foreach (var limbElement in Ragdoll.MainElement.Elements()) { if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } - XElement spriteElement = limbElement.Element("sprite"); + ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; if (string.IsNullOrEmpty(spritePath)) { continue; } - spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString()); + spritePath = ReplaceVars(spritePath); string fileName = Path.GetFileNameWithoutExtension(spritePath); @@ -1080,7 +851,7 @@ namespace Barotrauma Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; //extract the tags out of the filename - SpriteTags = file.Split('[', ']').Skip(1).ToList(); + SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList(); if (SpriteTags.Any()) { SpriteTags.RemoveAt(SpriteTags.Count - 1); @@ -1101,95 +872,44 @@ namespace Barotrauma { if (hairs == null) { - float commonness = Gender == Gender.Female ? 0.05f : 0.2f; - hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Hair, head.HeadSpriteId), WearableType.Hair, commonness); + float commonness = 0.1f; + hairs = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Hair), WearableType.Hair, commonness); } if (beards == null) { - beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Beard, head.HeadSpriteId), WearableType.Beard); + beards = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Beard), WearableType.Beard); } if (moustaches == null) { - moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Moustache, head.HeadSpriteId), WearableType.Moustache); + moustaches = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Moustache), WearableType.Moustache); } if (faceAttachments == null) { - faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.FaceAttachment, head.HeadSpriteId), WearableType.FaceAttachment); - } - - if (IsValidIndex(Head.HairIndex, hairs)) - { - Head.HairElement = hairs[Head.HairIndex]; - } - else - { - Head.HairElement = GetRandomElement(hairs); - Head.HairIndex = hairs.IndexOf(Head.HairElement); - } - if (Head.HairElement != null) - { - int thisHairIndex = hairs.IndexOf(head.HairElement); - int hairWithHatIndex = head.HairElement.GetAttributeInt("replacewhenwearinghat", thisHairIndex); - if (thisHairIndex != hairWithHatIndex && hairWithHatIndex > -1 && hairWithHatIndex < hairs.Count) - { - head.HairWithHatElement = hairs[hairWithHatIndex]; - } - else - { - head.HairWithHatElement = null; - } - } - - if (IsValidIndex(Head.BeardIndex, beards)) - { - Head.BeardElement = beards[Head.BeardIndex]; - } - else - { - Head.BeardElement = GetRandomElement(beards); - Head.BeardIndex = beards.IndexOf(Head.BeardElement); - } - if (IsValidIndex(Head.MoustacheIndex, moustaches)) - { - Head.MoustacheElement = moustaches[Head.MoustacheIndex]; - } - else - { - Head.MoustacheElement = GetRandomElement(moustaches); - Head.MoustacheIndex = moustaches.IndexOf(Head.MoustacheElement); - } - if (IsValidIndex(Head.FaceAttachmentIndex, faceAttachments)) - { - Head.FaceAttachment = faceAttachments[Head.FaceAttachmentIndex]; - } - else - { - Head.FaceAttachment = GetRandomElement(faceAttachments); - Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment); + faceAttachments = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.FaceAttachment), WearableType.FaceAttachment); } } } - public static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) + public static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) { // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. - var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); - var list = new List() { emptyElement }; + var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)).FromPackage(null); + var list = new List() { emptyElement }; list.AddRange(elements); return list; } - public XElement GetRandomElement(IEnumerable elements) + public ContentXElement GetRandomElement(IEnumerable elements) { var filtered = elements.Where(IsWearableAllowed); if (filtered.Count() == 0) { return null; } var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced); - return element == null || element.Name == "Empty" ? null : element; + return element == null || element.NameAsIdentifier() == "Empty" ? null : element; } - private bool IsWearableAllowed(XElement element) + private bool IsWearableAllowed(ContentXElement element) { - string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty); + string spriteName = element.GetChildElement("sprite").GetAttributeString("name", string.Empty); return IsAllowed(Head.HairElement, spriteName) && IsAllowed(Head.BeardElement, spriteName) && IsAllowed(Head.MoustacheElement, spriteName) && IsAllowed(Head.FaceAttachment, spriteName); } @@ -1197,7 +917,7 @@ namespace Barotrauma { if (element != null) { - var disallowed = element.GetAttributeStringArray("disallow", new string[0]); + var disallowed = element.GetAttributeStringArray("disallow", Array.Empty()); if (disallowed.Any(s => spriteName.Contains(s))) { return false; @@ -1206,9 +926,9 @@ namespace Barotrauma return true; } - public static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; + public static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; - private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); + private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); partial void LoadAttachmentSprites(bool omitJob); @@ -1225,9 +945,9 @@ namespace Barotrauma return (int)(salary * Job.Prefab.PriceMultiplier); } - public void IncreaseSkillLevel(string skillIdentifier, float increase, bool gainedFromAbility = false) + public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false) { - if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } + if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } if (Job.Prefab.Identifier == "assistant") { @@ -1256,7 +976,7 @@ namespace Barotrauma OnSkillChanged(skillIdentifier, prevLevel, newLevel); } - public void SetSkillLevel(string skillIdentifier, float level) + public void SetSkillLevel(Identifier skillIdentifier, float level) { if (Job == null) { return; } @@ -1274,7 +994,7 @@ namespace Barotrauma } } - partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel); + partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel); public void GiveExperience(int amount, bool isMissionExperience = false) { @@ -1393,24 +1113,22 @@ namespace Barotrauma new XAttribute("name", Name), new XAttribute("originalname", OriginalName), new XAttribute("speciesname", SpeciesName), - new XAttribute("gender", Head.gender.ToString()), - new XAttribute("race", Head.race.ToString()), + new XAttribute("tags", string.Join(",", Head.Preset.TagSet)), new XAttribute("salary", Salary), new XAttribute("experiencepoints", ExperiencePoints), new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)), new XAttribute("additionaltalentpoints", AdditionalTalentPoints), - new XAttribute("headspriteid", HeadSpriteId), - new XAttribute("hairindex", HairIndex), - new XAttribute("beardindex", BeardIndex), - new XAttribute("moustacheindex", MoustacheIndex), - new XAttribute("faceattachmentindex", FaceAttachmentIndex), - new XAttribute("skincolor", XMLExtensions.ColorToString(SkinColor)), - new XAttribute("haircolor", XMLExtensions.ColorToString(HairColor)), - new XAttribute("facialhaircolor", XMLExtensions.ColorToString(FacialHairColor)), + new XAttribute("hairindex", Head.HairIndex), + new XAttribute("beardindex", Head.BeardIndex), + new XAttribute("moustacheindex", Head.MoustacheIndex), + new XAttribute("faceattachmentindex", Head.FaceAttachmentIndex), + new XAttribute("skincolor", XMLExtensions.ColorToString(Head.SkinColor)), + new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)), + new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), - new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); - // TODO: animations? + new XAttribute("personality", PersonalityTrait?.Name.Value ?? "")); + // TODO: animations? charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); @@ -1448,7 +1166,7 @@ namespace Barotrauma return charElement; } - public static void SaveOrders(XElement parentElement, params OrderInfo[] orders) + public static void SaveOrders(XElement parentElement, params Order[] orders) { if (parentElement == null || orders == null || orders.None()) { return; } // If an order is invalid, we discard the order and increase the priority of the following orders so @@ -1458,8 +1176,8 @@ namespace Barotrauma var linkedSubs = GetLinkedSubmarines(); foreach (var orderInfo in orders) { - var order = orderInfo.Order; - if (order == null || string.IsNullOrEmpty(order.Identifier)) + var order = orderInfo; + if (order == null || order.Identifier == Identifier.Empty) { DebugConsole.ThrowError("Error saving an order - the order or its identifier is null"); priorityIncrease++; @@ -1488,7 +1206,7 @@ namespace Barotrauma targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub); if (!targetAvailableInNextLevel) { - if (!order.CanBeGeneralized) + if (!order.Prefab.CanBeGeneralized) { DebugConsole.Log($"Trying to save an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order requires a target so it won't be saved."); priorityIncrease++; @@ -1510,9 +1228,9 @@ namespace Barotrauma new XAttribute("id", order.Identifier), new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease), new XAttribute("targettype", (int)order.TargetType)); - if (!string.IsNullOrEmpty(orderInfo.OrderOption)) + if (orderInfo.Option != Identifier.Empty) { - orderElement.Add(new XAttribute("option", orderInfo.OrderOption)); + orderElement.Add(new XAttribute("option", orderInfo.Option)); } if (order.OrderGiver != null) { @@ -1559,7 +1277,7 @@ namespace Barotrauma /// public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement) { - var currentOrders = new List(characterInfo.CurrentOrders); + var currentOrders = new List(characterInfo.CurrentOrders); // Sort the current orders to make sure the one with the highest priority comes first currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); SaveOrders(parentElement, currentOrders.ToArray()); @@ -1580,7 +1298,7 @@ namespace Barotrauma var orders = LoadOrders(orderData); foreach (var order in orders) { - character.SetOrder(order, order.Order?.OrderGiver, speak: false, force: true); + character.SetOrder(order, speak: false, force: true); } } @@ -1589,9 +1307,9 @@ namespace Barotrauma ApplyOrderData(Character, OrderData); } - public static List LoadOrders(XElement ordersElement) + public static List LoadOrders(XElement ordersElement) { - var orders = new List(); + var orders = new List(); if (ordersElement == null) { return orders; } // If an order is invalid, we discard the order and increase the priority of the following orders so // 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and @@ -1602,7 +1320,7 @@ namespace Barotrauma { Order order = null; string orderIdentifier = orderElement.GetAttributeString("id", ""); - var orderPrefab = Order.GetPrefab(orderIdentifier); + var orderPrefab = OrderPrefab.Prefabs[orderIdentifier]; if (orderPrefab == null) { DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\""); @@ -1648,9 +1366,9 @@ namespace Barotrauma order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver); break; } - string orderOption = orderElement.GetAttributeString("option", ""); + Identifier orderOption = orderElement.GetAttributeIdentifier("option", ""); int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease; - var orderInfo = new OrderInfo(order, orderOption, manualPriority); + var orderInfo = order.WithOption(orderOption).WithManualPriority(manualPriority); orders.Add(orderInfo); bool GetTargetEntity(ushort targetId, out Entity targetEntity) @@ -1722,21 +1440,12 @@ namespace Barotrauma /// /// Reloads the attachment xml elements according to the indices. Doesn't reload the sprites. /// - private void ReloadHeadAttachments() + public void ReloadHeadAttachments() { ResetLoadedAttachments(); LoadHeadAttachments(); } - /// - /// Loads only the elements according to the indices, not the sprites. - /// - private void ResetHeadAttachments() - { - ResetAttachmentIndices(); - ResetLoadedAttachments(); - } - private void ResetAttachmentIndices() { Head.ResetAttachmentIndices(); @@ -1828,11 +1537,11 @@ namespace Barotrauma return 0f; } } - public float GetSavedStatValue(StatTypes statType, string statIdentifier) + public float GetSavedStatValue(StatTypes statType, Identifier statIdentifier) { if (SavedStatValues.TryGetValue(statType, out var statValues)) { - return statValues.Where(s => s.StatIdentifier.Equals(statIdentifier, StringComparison.OrdinalIgnoreCase)).Sum(v => v.StatValue); + return statValues.Where(s => s.StatIdentifier == statIdentifier).Sum(v => v.StatValue); } else { @@ -1879,7 +1588,7 @@ namespace Barotrauma class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter { - public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility) + public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility) { Value = skillAmount; SkillIdentifier = skillIdentifier; @@ -1888,7 +1597,7 @@ namespace Barotrauma } public Character Character { get; set; } public float Value { get; set; } - public string SkillIdentifier { get; set; } + public Identifier SkillIdentifier { get; set; } public bool GainedFromAbility { get; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index e5802eae4..6cec0f84c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; +using static Barotrauma.CharacterInfo; namespace Barotrauma { - class CharacterPrefab : IPrefab, IDisposable + class CharacterPrefab : PrefabWithUintIdentifier, IImplementsVariants { public readonly static PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; @@ -19,36 +21,51 @@ namespace Barotrauma Character.RemoveByPrefab(this); } - public string OriginalName { get; private set; } - public string Name { get; private set; } - public string Identifier { get; private set; } - public string FilePath { get; private set; } - public string VariantOf { get; private set; } + public string Name => Identifier.Value; + public Identifier VariantOf { get; } + public void InheritFrom(CharacterPrefab parent) + { + ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ParseConfigElement(); + } - public ContentPackage ContentPackage { get; private set; } + private void ParseConfigElement() + { + var headsElement = ConfigElement.GetChildElement("Heads"); + var varsElement = ConfigElement.GetChildElement("Vars"); + var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory"); + var pronounsElement = ConfigElement.GetChildElement("Pronouns"); - public XDocument XDocument { get; private set; } + if (headsElement != null && varsElement != null && menuCategoryElement != null && pronounsElement != null) + { + CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement); + } + } - public static IEnumerable ConfigFilePaths => Prefabs.Select(p => p.FilePath); - public static IEnumerable ConfigFiles => Prefabs.Select(p => p.XDocument); + private XElement originalElement; + public ContentXElement ConfigElement { get; private set; } - public const string HumanSpeciesName = "human"; - public static string HumanConfigFile => FindBySpeciesName(HumanSpeciesName).FilePath; + public CharacterInfoPrefab CharacterInfoPrefab { get; private set; } + + public static IEnumerable ConfigElements => Prefabs.Select(p => p.ConfigElement); + + public static readonly Identifier HumanSpeciesName = "human".ToIdentifier(); + public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile; + public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName); /// /// Searches for a character config file from all currently selected content packages, /// or from a specific package if the contentPackage parameter is given. /// - public static CharacterPrefab FindBySpeciesName(string speciesName) + public static CharacterPrefab FindBySpeciesName(Identifier speciesName) { - speciesName = speciesName.ToLowerInvariant(); if (!Prefabs.ContainsKey(speciesName)) { return null; } return Prefabs[speciesName]; } public static CharacterPrefab FindByFilePath(string filePath) { - return Prefabs.Find(p => p.FilePath.CleanUpPath() == filePath.CleanUpPath()); + return Prefabs.Find(p => p.ContentFile.Path == filePath); } public static CharacterPrefab Find(Predicate predicate) @@ -56,91 +73,38 @@ namespace Barotrauma return Prefabs.Find(predicate); } - public static void RemoveByFile(string file) + public CharacterPrefab(ContentXElement mainElement, CharacterFile file) : base(file, ParseName(mainElement, file)) { - Prefabs.RemoveByFile(file); + originalElement = mainElement; + ConfigElement = mainElement; + VariantOf = mainElement.VariantOf(); + + ParseConfigElement(); } - public static bool LoadFromFile(ContentFile file, bool forceOverride=false) + public static Identifier ParseName(XElement element, CharacterFile file) { - return LoadFromFile(file.Path, file.ContentPackage, forceOverride); - } - - public static bool LoadFromFile(string filePath, ContentPackage contentPackage, bool forceOverride=false) - { - XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null) + string name = element.GetAttributeString("name", null); + if (!string.IsNullOrEmpty(name)) { - DebugConsole.ThrowError($"Loading character file failed: {filePath}"); - return false; - } - if (Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.FilePath == filePath))) - { - DebugConsole.ThrowError($"Duplicate path: {filePath}"); - return false; - } - XElement mainElement = doc.Root; - if (doc.Root.IsCharacterVariant()) - { - if (!CheckSpeciesName(mainElement, filePath, out string n)) { return false; } - string inherit = mainElement.GetAttributeString("inherit", null); - string id = n.ToLowerInvariant(); - Prefabs.Add(new CharacterPrefab - { - Name = n, - OriginalName = n, - Identifier = id, - FilePath = filePath, - ContentPackage = contentPackage, - XDocument = doc, - VariantOf = inherit - }, isOverride: false); - return true; - } - else if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - } - if (!CheckSpeciesName(mainElement, filePath, out string name)) { return false; } - string identifier = name.ToLowerInvariant(); - Prefabs.Add(new CharacterPrefab - { - Name = name, - OriginalName = name, - Identifier = identifier, - FilePath = filePath, - ContentPackage = contentPackage, - XDocument = doc - }, forceOverride || doc.Root.IsOverride()); - - return true; - } - - public static bool CheckSpeciesName(XElement mainElement, string filePath, out string name) - { - name = mainElement.GetAttributeString("name", null); - if (name != null) - { - DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); + DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); } else { - name = mainElement.GetAttributeString("speciesname", string.Empty); + name = element.GetAttributeString("speciesname", string.Empty); } - if (string.IsNullOrWhiteSpace(name)) + return new Identifier(name); + } + + public static bool CheckSpeciesName(XElement mainElement, CharacterFile file, out Identifier name) + { + name = ParseName(mainElement, file); + if (name == Identifier.Empty) { - DebugConsole.ThrowError($"No species name defined for: {filePath}"); + DebugConsole.ThrowError($"No species name defined for: {file.Path}"); return false; } return true; } - - public static void LoadAll() - { - foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character)) - { - LoadFromFile(file); - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs index 070190f32..3a415130a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CorpsePrefab.cs @@ -7,19 +7,19 @@ using System.Xml.Linq; namespace Barotrauma { - class CorpsePrefab : HumanPrefab, IPrefab, IDisposable + class CorpsePrefab : HumanPrefab { public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; Prefabs.Remove(this); } - public static CorpsePrefab Get(string identifier) + public static CorpsePrefab Get(Identifier identifier) { if (Prefabs == null) { @@ -37,99 +37,11 @@ namespace Barotrauma } } - [Serialize(Level.PositionType.Wreck, false)] + [Serialize(Level.PositionType.Wreck, IsPropertySaveable.No)] public Level.PositionType SpawnPosition { get; private set; } - public ContentPackage ContentPackage { get; private set; } - - public CorpsePrefab(XElement element, string filePath, bool allowOverriding) : base(element, filePath) - { - Prefabs.Add(this, allowOverriding); - } + public CorpsePrefab(ContentXElement element, CorpsesFile file) : base(element, file) { } public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync); - - public static void LoadAll(IEnumerable files) - { - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - DebugConsole.Log("*** " + file.Path + " ***"); - RemoveByFile(file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - var rootElement = doc.Root; - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "corpse": - new CorpsePrefab(rootElement, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - break; - case "corpses": - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var itemElement = element.GetChildElement("item"); - if (itemElement != null) - { - new CorpsePrefab(itemElement, file.Path, true) - { - ContentPackage = file.ContentPackage - }; - } - else - { - DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); - } - } - else - { - new CorpsePrefab(element, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - } - } - break; - case "override": - var corpses = rootElement.GetChildElement("corpses"); - if (corpses != null) - { - foreach (var element in corpses.Elements()) - { - new CorpsePrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage, - }; - } - } - foreach (var element in rootElement.GetChildElements("corpse")) - { - new CorpsePrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage - }; - } - break; - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}"); - break; - } - } - - public static void RemoveByFile(string filePath) - { - Prefabs.RemoveByFile(filePath); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 150116a25..93fe641e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -12,7 +12,7 @@ namespace Barotrauma public string Name => ToString(); - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } public float PendingAdditionStrength { get; set; } public float AdditionStrength { get; set; } @@ -21,7 +21,7 @@ namespace Barotrauma protected float _strength; - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public virtual float Strength { get { return _strength; } @@ -43,10 +43,10 @@ namespace Barotrauma private float _nonClampedStrength = -1; public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength; - [Serialize("", true), Editable] - public string Identifier { get; private set; } + [Serialize("", IsPropertySaveable.Yes), Editable] + public Identifier Identifier { get; private set; } - [Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)] public float Probability { get; set; } = 1.0f; public float DamagePerSecond; @@ -73,7 +73,7 @@ namespace Barotrauma Prefab = prefab; PendingAdditionStrength = Prefab.GrainBurst; _strength = strength; - Identifier = prefab?.Identifier; + Identifier = prefab.Identifier; foreach (var periodicEffect in prefab.PeriodicEffects) { @@ -269,14 +269,15 @@ namespace Barotrauma } } - public float GetResistance(AfflictionPrefab affliction) + public float GetResistance(Identifier afflictionId) { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } + var affliction = AfflictionPrefab.Prefabs[afflictionId]; AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } if (!currentEffect.ResistanceFor.Any(r => - r.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || - r.Equals(affliction.AfflictionType, StringComparison.OrdinalIgnoreCase))) + r == affliction.Identifier || + r == affliction.AfflictionType)) { return 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 4d42e7aa4..9e74e40e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -26,7 +26,7 @@ namespace Barotrauma private readonly List huskInfection = new List(); - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public override float Strength { get { return _strength; } @@ -211,10 +211,10 @@ namespace Barotrauma } character.Enabled = false; - Entity.Spawner.AddToRemoveQueue(character); + Entity.Spawner.AddEntityToRemoveQueue(character); UnsubscribeFromDeathEvent(); - string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); + Identifier huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk); CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); if (prefab == null) @@ -230,8 +230,8 @@ namespace Barotrauma if (huskCharacterInfo != null) { var bodyTint = GetBodyTint(); - huskCharacterInfo.SkinColor = - Color.Lerp(huskCharacterInfo.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f); + huskCharacterInfo.Head.SkinColor = + Color.Lerp(huskCharacterInfo.Head.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f); } var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true); @@ -306,7 +306,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public static List AttachHuskAppendage(Character character, string afflictionIdentifier, XElement appendageDefinition = null, Ragdoll ragdoll = null) + public static List AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null) { var appendage = new List(); if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction)) @@ -314,26 +314,26 @@ namespace Barotrauma DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!"); return appendage; } - string nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); - string huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); + Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction); + Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction); CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName); - if (huskPrefab?.XDocument == null) + if (huskPrefab?.ConfigElement == null) { DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!"); return appendage; } - var mainElement = huskPrefab.XDocument.Root.IsOverride() ? huskPrefab.XDocument.Root.FirstElement() : huskPrefab.XDocument.Root; + var mainElement = huskPrefab.ConfigElement; var element = appendageDefinition; if (element == null) { - element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeString("affliction", string.Empty).Equals(afflictionIdentifier)); + element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier); } if (element == null) { DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!"); return appendage; } - string pathToAppendage = element.GetAttributeString("path", string.Empty); + ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty; XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage); if (doc == null) { return appendage; } if (ragdoll == null) @@ -344,10 +344,12 @@ namespace Barotrauma { ragdoll.Flip(); } - var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); - foreach (var jointElement in doc.Root.Elements("joint")) + + var root = doc.Root.FromPackage(pathToAppendage.ContentPackage); + var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); + foreach (var jointElement in root.GetChildElements("joint")) { - if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement)) + if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams); Limb attachLimb = null; @@ -388,15 +390,15 @@ namespace Barotrauma return appendage; } - public static string GetHuskedSpeciesName(string speciesName, AfflictionPrefabHusk prefab) + public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab) { return prefab.HuskedSpeciesName.Replace(AfflictionPrefabHusk.Tag, speciesName); } - public static string GetNonHuskedSpeciesName(string huskedSpeciesName, AfflictionPrefabHusk prefab) + public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab) { - string nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag); - return huskedSpeciesName.ToLowerInvariant().Remove(nonTag); + Identifier nonTag = prefab.HuskedSpeciesName.Remove(AfflictionPrefabHusk.Tag); + return huskedSpeciesName.Remove(nonTag); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 087627e9e..da10f3719 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -2,28 +2,29 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { - static class CPRSettings + class CPRSettings : Prefab { - public static string FilePath { get; private set; } - public static bool IsLoaded { get; private set; } - public static float ReviveChancePerSkill { get; private set; } - public static float ReviveChanceExponent { get; private set; } - public static float ReviveChanceMin { get; private set; } - public static float ReviveChanceMax { get; private set; } - public static float StabilizationPerSkill { get; private set; } - public static float StabilizationMin { get; private set; } - public static float StabilizationMax { get; private set; } - public static float DamageSkillThreshold { get; private set; } - public static float DamageSkillMultiplier { get; private set; } + public readonly static PrefabSelector Prefabs = new PrefabSelector(); + public static CPRSettings Active => Prefabs.ActivePrefab; - private static string insufficientSkillAfflictionIdentifier { get; set; } - public static AfflictionPrefab InsufficientSkillAffliction + public readonly float ReviveChancePerSkill; + public readonly float ReviveChanceExponent; + public readonly float ReviveChanceMin; + public readonly float ReviveChanceMax; + public readonly float StabilizationPerSkill; + public readonly float StabilizationMin; + public readonly float StabilizationMax; + public readonly float DamageSkillThreshold; + public readonly float DamageSkillMultiplier; + + private readonly string insufficientSkillAfflictionIdentifier; + public AfflictionPrefab InsufficientSkillAffliction { get { @@ -34,7 +35,7 @@ namespace Barotrauma } } - public static void Load(XElement element, string filePath) + public CPRSettings(XElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier()) { ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f); ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f); @@ -49,33 +50,26 @@ namespace Barotrauma DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f); insufficientSkillAfflictionIdentifier = element.GetAttributeString("insufficientskillaffliction", ""); - - IsLoaded = true; - FilePath = filePath; } - public static void Unload() - { - IsLoaded = false; - FilePath = null; - } + public override void Dispose() { } } class AfflictionPrefabHusk : AfflictionPrefab { - public AfflictionPrefabHusk(XElement element, string filePath, Type type = null) : base(element, filePath, type) + public AfflictionPrefabHusk(ContentXElement element, AfflictionsFile file, Type type = null) : base(element, file, type) { - HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant(); - if (HuskedSpeciesName == null) + HuskedSpeciesName = element.GetAttributeIdentifier("huskedspeciesname", Identifier.Empty); + if (HuskedSpeciesName.IsEmpty) { DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); - HuskedSpeciesName = "[speciesname]husk"; + HuskedSpeciesName = "[speciesname]husk".ToIdentifier(); } - TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true); + TargetSpecies = element.GetAttributeIdentifierArray("targets", Array.Empty(), trim: true); if (TargetSpecies.Length == 0) { DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange); - TargetSpecies = new string[] { "human" }; + TargetSpecies = new Identifier[] { CharacterPrefab.HumanSpeciesName }; } var attachElement = element.GetChildElement("attachlimb"); if (attachElement != null) @@ -112,9 +106,9 @@ namespace Barotrauma public float ActiveThreshold, DormantThreshold, TransitionThreshold; public float TransformThresholdOnDeath; - public readonly string HuskedSpeciesName; - public readonly string[] TargetSpecies; - public const string Tag = "[speciesname]"; + public readonly Identifier HuskedSpeciesName; + public readonly Identifier[] TargetSpecies; + public static readonly Identifier Tag = "[speciesname]".ToIdentifier(); public readonly bool TransferBuffs; public readonly bool SendMessages; @@ -123,124 +117,122 @@ namespace Barotrauma public readonly bool ControlHusk; } - class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier + class AfflictionPrefab : PrefabWithUintIdentifier { public class Effect { //this effect is applied when the strength is within this range - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinVitalityDecrease { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxVitalityDecrease { get; private set; } //how much the strength of the affliction changes per second - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float StrengthChange { get; private set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool MultiplyByMaxVitality { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinScreenBlur { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxScreenBlur { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinScreenDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxScreenDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinRadialDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxRadialDistort { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinChromaticAberration { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxChromaticAberration { get; private set; } - [Serialize("255,255,255,255", false)] + [Serialize("255,255,255,255", IsPropertySaveable.No)] public Color GrainColor { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinGrainStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxGrainStrength { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float ScreenEffectFluctuationFrequency { get; private set; } - - [Serialize(1.0f, false)] + + [Serialize(1.0f, IsPropertySaveable.No)] public float MinAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxAfflictionOverlayAlphaMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MinBuffMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxBuffMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MinSpeedMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxSpeedMultiplier { get; private set; } - - [Serialize(1.0f, false)] + + [Serialize(1.0f, IsPropertySaveable.No)] public float MinSkillMultiplier { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float MaxSkillMultiplier { get; private set; } + + private readonly Identifier[] resistanceFor; + public IReadOnlyList ResistanceFor => resistanceFor; - private readonly string[] resistanceFor; - public IEnumerable ResistanceFor - { - get { return resistanceFor; } - } - - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinResistance { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MaxResistance { get; private set; } - [Serialize("", false)] - public string DialogFlag { get; private set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier DialogFlag { get; private set; } - [Serialize("", false)] - public string Tag { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("", IsPropertySaveable.No)] + public Identifier Tag { get; private set; } + + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MinFaceTint { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MaxFaceTint { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MinBodyTint { get; private set; } - [Serialize("0,0,0,0", false)] + [Serialize("0,0,0,0", IsPropertySaveable.No)] public Color MaxBodyTint { get; private set; } /// /// Prevents AfflictionHusks with the specified identifier(s) from transforming the character into an AI-controlled character /// - public string[] BlockTransformation { get; private set; } + public Identifier[] BlockTransformation { get; private set; } public readonly Dictionary AfflictionStatValues = new Dictionary(); public readonly HashSet AfflictionAbilityFlags = new HashSet(); @@ -248,14 +240,14 @@ namespace Barotrauma //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); - public Effect(XElement element, string parentDebugName) + public Effect(ContentXElement element, string parentDebugName) { SerializableProperty.DeserializeProperties(this, element); - resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true); - BlockTransformation = element.GetAttributeStringArray("blocktransformation", new string[0], convertToLowerInvariant: true); + resistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty()); + BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty()); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -288,9 +280,9 @@ namespace Barotrauma public readonly List StatusEffects = new List(); public readonly float MinInterval, MaxInterval; - public PeriodicEffect(XElement element, string parentDebugName) + public PeriodicEffect(ContentXElement element, string parentDebugName) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); } @@ -307,48 +299,28 @@ namespace Barotrauma } } - public static AfflictionPrefab InternalDamage; - public static AfflictionPrefab ImpactDamage; - public static AfflictionPrefab Bleeding; - public static AfflictionPrefab Burn; - public static AfflictionPrefab OxygenLow; - public static AfflictionPrefab Bloodloss; - public static AfflictionPrefab Pressure; - public static AfflictionPrefab Stun; - public static AfflictionPrefab RadiationSickness; + public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"]; + public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"]; + public static AfflictionPrefab Bleeding => Prefabs["bleeding"]; + public static AfflictionPrefab Burn => Prefabs["burn"]; + public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"]; + public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"]; + public static AfflictionPrefab Pressure => Prefabs["pressure"]; + public static AfflictionPrefab Stun => Prefabs["stun"]; + public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"]; public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override void Dispose() { } - public static IEnumerable List - { - get - { - foreach (var prefab in Prefabs) - { - yield return prefab; - } - } - } - - public string FilePath { get; private set; } - - /// - /// Unique identifier that's generated by hashing the prefab's string identifier. - /// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer. - /// - public uint UIntIdentifier { get; set; } + public static IEnumerable List => Prefabs; // Arbitrary string that is used to identify the type of the affliction. - public readonly string AfflictionType; + public readonly Identifier AfflictionType; + private readonly ContentXElement configElement; + //Does the affliction affect a specific limb or the whole character public readonly bool LimbSpecific; @@ -356,18 +328,14 @@ namespace Barotrauma //(e.g. mental health problems on head, lack of oxygen on torso...) public readonly LimbType IndicatorLimb; - public string Identifier { get; private set; } - public string OriginalName { get { return Identifier; } } - public ContentPackage ContentPackage { get; private set; } - - public readonly string Name, Description; - public readonly string TranslationOverride; + public readonly LocalizedString Name, Description; + public readonly Identifier TranslationIdentifier; public readonly bool IsBuff; public readonly bool HealableInMedicalClinic; public readonly float HealCostMultiplier; public readonly int BaseHealCost; - public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; + public readonly LocalizedString CauseOfDeathDescription, SelfCauseOfDeathDescription; //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; @@ -392,7 +360,7 @@ namespace Barotrauma public float DamageOverlayAlpha; //steam achievement given when the affliction is removed from the controlled character - public readonly string AchievementOnRemoved; + public readonly Identifier AchievementOnRemoved; public readonly Sprite Icon; public readonly Color[] IconColors; @@ -407,11 +375,9 @@ namespace Barotrauma public IList PeriodicEffects => periodicEffects; - private readonly string typeName; - private readonly ConstructorInfo constructor; - public IEnumerable> TreatmentSuitability + public IEnumerable> TreatmentSuitability { get { @@ -420,255 +386,32 @@ namespace Barotrauma float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType)); if (suitability > 0.0f) { - yield return new KeyValuePair(itemPrefab.Identifier, suitability); + yield return new KeyValuePair(itemPrefab.Identifier, suitability); } } } } - public static void LoadAll(IEnumerable files) + public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", "")) { - CPRSettings.Unload(); - InternalDamage = null; - ImpactDamage = null; - Bleeding = null; - Burn = null; - OxygenLow = null; - Bloodloss = null; - Pressure = null; - Stun = null; - RadiationSickness = null; -#if CLIENT - CharacterHealth.DamageOverlay?.Remove(); - CharacterHealth.DamageOverlay = null; - CharacterHealth.DamageOverlayFile = string.Empty; -#endif - var prevPrefabs = Prefabs.AllPrefabs.SelectMany(kvp => kvp.Value).ToList(); - foreach (var prefab in prevPrefabs) - { - prefab?.Dispose(); - } - System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll"); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - - if (InternalDamage == null) { DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs."); } - if (Bleeding == null) { DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs."); } - if (Burn == null) { DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs."); } - if (OxygenLow == null) { DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs."); } - if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); } - if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); } - if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); } - if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - if (doc.Root.IsOverride()) - { - DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one."); - } - - List<(AfflictionPrefab prefab, XElement element)> loadedAfflictions = new List<(AfflictionPrefab prefab, XElement element)>(); - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) && - !elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the affliction '{elementName}' in file '{file.Path}'"); - continue; - } - if (Prefabs.ContainsKey(identifier)) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Duplicate affliction: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - } - string type = sourceElement.GetAttributeString("type", ""); - switch (sourceElement.Name.ToString().ToLowerInvariant()) - { - case "cprsettings": - type = "cprsettings"; - break; - case "damageoverlay": - type = "damageoverlay"; - break; - } - - AfflictionPrefab prefab = null; - switch (type) - { - case "damageoverlay": -#if CLIENT - if (CharacterHealth.DamageOverlay != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding damage overlay with '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': damage overlay already loaded. Add tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one."); - break; - } - } - CharacterHealth.DamageOverlay?.Remove(); - CharacterHealth.DamageOverlay = new Sprite(element); - CharacterHealth.DamageOverlayFile = file.Path; -#endif - break; - case "bleeding": - prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(AfflictionBleeding)); - break; - case "huskinfection": - case "alieninfection": - prefab = new AfflictionPrefabHusk(sourceElement, file.Path, typeof(AfflictionHusk)); - break; - case "cprsettings": - if (CPRSettings.IsLoaded) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding the CPR settings with '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': CPR settings already loaded. Add tags as the parent of the custom CPRSettings to allow overriding the vanilla values."); - break; - } - } - CPRSettings.Load(sourceElement, file.Path); - break; - case "damage": - case "burn": - case "oxygenlow": - case "bloodloss": - case "stun": - case "pressure": - case "internaldamage": - prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(Affliction)) - { - ContentPackage = file.ContentPackage - }; - break; - default: - prefab = new AfflictionPrefab(sourceElement, file.Path) - { - ContentPackage = file.ContentPackage - }; - break; - } - switch (identifier) - { - case "internaldamage": - InternalDamage = prefab; - break; - case "blunttrauma": - ImpactDamage = prefab; - break; - case "bleeding": - Bleeding = prefab; - break; - case "burn": - Burn = prefab; - break; - case "oxygenlow": - OxygenLow = prefab; - break; - case "bloodloss": - Bloodloss = prefab; - break; - case "pressure": - Pressure = prefab; - break; - case "stun": - Stun = prefab; - break; - case "radiationsickness": - RadiationSickness = prefab; - break; - } - if (ImpactDamage == null) { ImpactDamage = InternalDamage; } - - if (prefab != null) - { - loadedAfflictions.Add((prefab, sourceElement)); - Prefabs.Add(prefab, isOverride); - prefab.CalculatePrefabUIntIdentifier(Prefabs); - } - } - - //load the effects after all the afflictions in the file have been instantiated - //otherwise afflictions can't inflict other afflictions that are defined at a later point in the file - foreach ((AfflictionPrefab prefab, XElement element) in loadedAfflictions) - { - prefab.LoadEffects(element); - } - } - - public static void RemoveByFile(string filePath) - { - if (CPRSettings.FilePath == filePath) { CPRSettings.Unload(); } -#if CLIENT - if (CharacterHealth.DamageOverlayFile == filePath) - { - CharacterHealth.DamageOverlay?.Remove(); - CharacterHealth.DamageOverlay = null; - } -#endif - - Prefabs.RemoveByFile(filePath); - } - - public AfflictionPrefab(XElement element, string filePath, Type type = null) - { - FilePath = filePath; - - typeName = type == null ? element.Name.ToString() : type.Name; - if (typeName == "InternalDamage" && type == null) - { - type = typeof(Affliction); - } - - Identifier = element.GetAttributeString("identifier", ""); - - AfflictionType = element.GetAttributeString("type", ""); - TranslationOverride = element.GetAttributeString("translationoverride", null); - string translationId = TranslationOverride ?? Identifier; - Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", ""); - Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); + configElement = element; + + AfflictionType = element.GetAttributeIdentifier("type", ""); + TranslationIdentifier = element.GetAttributeIdentifier("translationoverride", Identifier); + Name = TextManager.Get($"AfflictionName.{TranslationIdentifier}").Fallback(element.GetAttributeString("name", "")); + Description = TextManager.Get($"AfflictionDescription.{TranslationIdentifier}").Fallback(element.GetAttributeString("description", "")); IsBuff = element.GetAttributeBool("isbuff", false); HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic", !IsBuff && - !AfflictionType.Equals("geneticmaterialbuff", StringComparison.OrdinalIgnoreCase) && - !AfflictionType.Equals("geneticmaterialdebuff", StringComparison.OrdinalIgnoreCase)); + AfflictionType != "geneticmaterialbuff" && + AfflictionType != "geneticmaterialdebuff"); HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); if (element.Attribute("nameidentifier") != null) { - Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name; + Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name); } LimbSpecific = element.GetAttributeBool("limbspecific", false); @@ -687,7 +430,8 @@ namespace Barotrauma MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f)); + ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", + Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold)); TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); @@ -695,14 +439,14 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); - CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", ""); - SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); + CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", "")); + SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", "")); IconColors = element.GetAttributeColorArray("iconcolors", null); AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false); - AchievementOnRemoved = element.GetAttributeString("achievementonremoved", ""); + AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", ""); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -724,43 +468,42 @@ namespace Barotrauma } } - try - { - if (type == null) - { - type = Type.GetType("Barotrauma." + typeName, true, true); - if (type == null) - { - DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\"."); - return; - } - } - } - catch - { - DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\"."); - type = typeof(Affliction); - } - constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) }); } - private void LoadEffects(XElement element) + public static void LoadAllEffects() { - foreach (XElement subElement in element.Elements()) + Prefabs.ForEach(p => p.LoadEffects()); + } + + public static void ClearAllEffects() + { + Prefabs.ForEach(p => p.ClearEffects()); + } + + public void LoadEffects() + { + ClearEffects(); + foreach (var subElement in configElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "effect": - effects.Add(new Effect(subElement, Name)); + effects.Add(new Effect(subElement, Name.Value)); break; case "periodiceffect": - periodicEffects.Add(new PeriodicEffect(subElement, Name)); + periodicEffects.Add(new PeriodicEffect(subElement, Name.Value)); break; } } } + public void ClearEffects() + { + effects.Clear(); + periodicEffects.Clear(); + } + #if CLIENT public void ReloadSoundsIfNeeded() { @@ -770,7 +513,7 @@ namespace Barotrauma { foreach (var sound in statusEffect.Sounds) { - if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + if (sound.Sound == null) { RoundSound.Reload(sound); } } } } @@ -780,7 +523,7 @@ namespace Barotrauma { foreach (var sound in statusEffect.Sounds) { - if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + if (sound.Sound == null) { RoundSound.Reload(sound); } } } } @@ -789,7 +532,7 @@ namespace Barotrauma public override string ToString() { - return "AfflictionPrefab (" + Name + ")"; + return $"AfflictionPrefab ({Name})"; } public Affliction Instantiate(float strength, Character source = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs index 64472ae4e..408545fa2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionSpaceHerpes.cs @@ -41,7 +41,7 @@ namespace Barotrauma invertControlsToggleTimer = 5.0f; if (Rand.Range(0.0f, 1.0f) < 0.5f) { - characterHealth.ReduceAffliction(null, "invertcontrols", 100); + characterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 6d6dcac8f..ad88711ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -19,23 +19,23 @@ namespace Barotrauma public Rectangle HighlightArea; - public readonly string Name; + public readonly LocalizedString Name; //public readonly List Afflictions = new List(); - public readonly Dictionary VitalityMultipliers = new Dictionary(); - public readonly Dictionary VitalityTypeMultipliers = new Dictionary(); + public readonly Dictionary VitalityMultipliers = new Dictionary(); + public readonly Dictionary VitalityTypeMultipliers = new Dictionary(); public LimbHealth() { } - public LimbHealth(XElement element, CharacterHealth characterHealth) + public LimbHealth(ContentXElement element, CharacterHealth characterHealth) { string limbName = element.GetAttributeString("name", null) ?? "generic"; if (limbName != "generic") { Name = TextManager.Get("HealthLimbName." + limbName); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -53,16 +53,16 @@ namespace Barotrauma continue; } - string afflictionIdentifier = subElement.GetAttributeString("identifier", ""); - string afflictionType = subElement.GetAttributeString("type", ""); + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + Identifier afflictionType = subElement.GetAttributeIdentifier("type", ""); float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - if (!string.IsNullOrEmpty(afflictionIdentifier)) + if (!afflictionIdentifier.IsEmpty) { - VitalityMultipliers.Add(afflictionIdentifier.ToLowerInvariant(), multiplier); + VitalityMultipliers.Add(afflictionIdentifier, multiplier); } else { - VitalityTypeMultipliers.Add(afflictionType.ToLowerInvariant(), multiplier); + VitalityTypeMultipliers.Add(afflictionType, multiplier); } break; } @@ -219,7 +219,7 @@ namespace Barotrauma InitProjSpecific(null, character); } - public CharacterHealth(XElement element, Character character, XElement limbHealthElement = null) + public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null) { this.Character = character; InitIrremovableAfflictions(); @@ -230,7 +230,7 @@ namespace Barotrauma limbHealths.Clear(); limbHealthElement ??= element; - foreach (XElement subElement in limbHealthElement.Elements()) + foreach (var subElement in limbHealthElement.Elements()) { if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; } limbHealths.Add(new LimbHealth(subElement, this)); @@ -255,7 +255,7 @@ namespace Barotrauma } } - partial void InitProjSpecific(XElement element, Character character); + partial void InitProjSpecific(ContentXElement element, Character character); public IReadOnlyCollection GetAllAfflictions() { @@ -282,10 +282,13 @@ namespace Barotrauma private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex]; private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false)); - public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) + public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) => + GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions); + + public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true) => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions); - public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true) + public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true) => GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions); private Affliction GetAffliction(Func predicate, bool allowLimbAfflictions = true) @@ -409,7 +412,7 @@ namespace Barotrauma foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; - resistance += affliction.GetResistance(afflictionPrefab); + resistance += affliction.GetResistance(afflictionPrefab.Identifier); } return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab)); } @@ -436,37 +439,58 @@ namespace Barotrauma } private readonly List matchingAfflictions = new List(); - public void ReduceAffliction(Limb targetLimb, string afflictionIdentifier, float amount, ActionType? treatmentAction = null) + + public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null) { matchingAfflictions.Clear(); + matchingAfflictions.AddRange(afflictions.Keys); - if (targetLimb == null) - { - matchingAfflictions.AddRange(afflictions.Keys); - } - else - { - foreach (KeyValuePair kvp in afflictions) - { - var affliction = kvp.Key; - if (kvp.Value == null) - { - matchingAfflictions.Add(affliction); - } - else if (limbHealths[targetLimb.HealthIndex] == kvp.Value) - { - matchingAfflictions.Add(affliction); - } - } - } + ReduceMatchingAfflictions(amount, treatmentAction); + } + + public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null) + { + if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(afflictions.Keys); + matchingAfflictions.RemoveAll(a => + a.Prefab.Identifier != affliction && + a.Prefab.AfflictionType != affliction); + + ReduceMatchingAfflictions(amount, treatmentAction); + } - if (!string.IsNullOrEmpty(afflictionIdentifier)) - { - matchingAfflictions.RemoveAll(a => - !a.Prefab.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase) && - !a.Prefab.AfflictionType.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase)); - } + private IEnumerable GetAfflictionsForLimb(Limb targetLimb) + => afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]); + + public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null) + { + if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); + + ReduceMatchingAfflictions(amount, treatmentAction); + } + + public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null) + { + if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); } + if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); } + + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb)); + + matchingAfflictions.RemoveAll(a => + a.Prefab.Identifier != affliction && + a.Prefab.AfflictionType != affliction); + + ReduceMatchingAfflictions(amount, treatmentAction); + } + + private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction) + { if (matchingAfflictions.Count == 0) { return; } float reduceAmount = amount / matchingAfflictions.Count; @@ -635,7 +659,7 @@ namespace Barotrauma if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { - if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase))) + if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; } @@ -953,7 +977,7 @@ namespace Barotrauma /// A dictionary where the key is the identifier of the item and the value the suitability /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. /// If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds. - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f) { //key = item identifier //float = suitability @@ -979,7 +1003,7 @@ namespace Barotrauma if (strength <= affliction.Prefab.TreatmentThreshold) { continue; } if (ignoreHiddenAfflictions && strength < affliction.Prefab.ShowIconThreshold) { continue; } - foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { if (!treatmentSuitability.ContainsKey(treatment.Key)) { @@ -996,23 +1020,23 @@ namespace Barotrauma //normalize the suitabilities to a range of 0 to 1 if (normalize) { - foreach (string treatment in treatmentSuitability.Keys.ToList()) + foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) { treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); } } } - public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); + public IEnumerable GetActiveAfflictionTags() => GetActiveAfflictionTags(afflictions.Keys); - private readonly HashSet afflictionTags = new HashSet(); - public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) + private readonly HashSet afflictionTags = new HashSet(); + public IEnumerable GetActiveAfflictionTags(IEnumerable afflictions) { afflictionTags.Clear(); foreach (Affliction affliction in afflictions) { var currentEffect = affliction.GetActiveEffect(); - if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.Tag)) + if (currentEffect != null && !currentEffect.Tag.IsEmpty) { afflictionTags.Add(currentEffect.Tag); } @@ -1036,10 +1060,10 @@ namespace Barotrauma } foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction) { - if (statusEffectAffliction.affliction.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || - statusEffectAffliction.affliction.Equals(affliction.Prefab.AfflictionType, StringComparison.OrdinalIgnoreCase)) + if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier || + statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType) { - strength -= statusEffectAffliction.amount * statusEffectDuration; + strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration; } } } @@ -1064,7 +1088,7 @@ namespace Barotrauma msg.Write((byte)activeAfflictions.Count); foreach (Affliction affliction in activeAfflictions) { - msg.Write(affliction.Prefab.UIntIdentifier); + msg.Write(affliction.Prefab.UintIdentifier); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); @@ -1089,7 +1113,7 @@ namespace Barotrauma foreach (var (limbHealth, affliction) in limbAfflictions) { msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1); - msg.Write(affliction.Prefab.UIntIdentifier); + msg.Write(affliction.Prefab.UintIdentifier); msg.WriteRangedSingle( MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength), 0.0f, affliction.Prefab.MaxStrength, 8); @@ -1144,7 +1168,7 @@ namespace Barotrauma public void Load(XElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index b40c60233..07f16e007 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -2,6 +2,7 @@ using System; using System.Xml.Linq; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma @@ -10,23 +11,23 @@ namespace Barotrauma { public string Name => "Damage Modifier"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - [Serialize(1.0f, false), Editable(DecimalCount = 2)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(DecimalCount = 2)] public float DamageMultiplier { get; private set; } - [Serialize(1.0f, false), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)] public float ProbabilityMultiplier { get; private set; } - [Serialize("0.0,360", false), Editable] + [Serialize("0.0,360", IsPropertySaveable.No), Editable] public Vector2 ArmorSector { get; @@ -35,14 +36,14 @@ namespace Barotrauma public Vector2 ArmorSectorInRadians => new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y)); - [Serialize(false, false), Editable] + [Serialize(false, IsPropertySaveable.No), Editable] public bool DeflectProjectiles { get; private set; } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string AfflictionIdentifiers { get @@ -56,7 +57,7 @@ namespace Barotrauma } } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string AfflictionTypes { get @@ -72,22 +73,11 @@ namespace Barotrauma private string rawAfflictionIdentifierString; private string rawAfflictionTypeString; - private string[] parsedAfflictionIdentifiers; - private string[] parsedAfflictionTypes; - public string[] ParsedAfflictionIdentifiers - { - get - { - return parsedAfflictionIdentifiers; - } - } - public string[] ParsedAfflictionTypes - { - get - { - return parsedAfflictionTypes; - } - } + private ImmutableArray parsedAfflictionIdentifiers; + private ImmutableArray parsedAfflictionTypes; + public ref readonly ImmutableArray ParsedAfflictionIdentifiers => ref parsedAfflictionIdentifiers; + + public ref readonly ImmutableArray ParsedAfflictionTypes => ref parsedAfflictionTypes; public DamageModifier(XElement element, string parentDebugName) { @@ -102,55 +92,58 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(rawAfflictionTypeString)) { - parsedAfflictionTypes = new string[0]; + parsedAfflictionTypes = Enumerable.Empty().ToImmutableArray(); return; } - string[] splitValue = rawAfflictionTypeString.Split(',', ','); - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].ToLowerInvariant().Trim(); - } - parsedAfflictionTypes = splitValue; + + parsedAfflictionTypes = rawAfflictionTypeString.Split(',', ',') + .Select(s => s.Trim()).ToIdentifiers().ToImmutableArray(); } private void ParseAfflictionIdentifiers() { if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString)) { - parsedAfflictionIdentifiers = new string[0]; + parsedAfflictionIdentifiers = Enumerable.Empty().ToImmutableArray(); return; } - string[] splitValue = rawAfflictionIdentifierString.Split(',', ','); - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].ToLowerInvariant().Trim(); - } - parsedAfflictionIdentifiers = splitValue; + + parsedAfflictionIdentifiers = rawAfflictionIdentifierString.Split(',', ',') + .Select(s => s.Trim()).ToIdentifiers().ToImmutableArray(); } - public bool MatchesAfflictionIdentifier(string identifier) + public bool MatchesAfflictionIdentifier(string identifier) => + MatchesAfflictionIdentifier(identifier.ToIdentifier()); + + public bool MatchesAfflictionIdentifier(Identifier identifier) { //if no identifiers have been defined, the damage modifier affects all afflictions if (AfflictionIdentifiers.Length == 0) { return true; } - return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + return parsedAfflictionIdentifiers.Any(id => id == identifier); } - public bool MatchesAfflictionType(string type) + public bool MatchesAfflictionType(string type) => + MatchesAfflictionType(type.ToIdentifier()); + + public bool MatchesAfflictionType(Identifier type) { //if no types have been defined, the damage modifier affects all afflictions if (AfflictionTypes.Length == 0) { return true; } - return parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase)); + return parsedAfflictionTypes.Any(t => t == type); } /// /// Returns true if the type or the identifier matches the defined types/identifiers. /// - public bool MatchesAffliction(string identifier, string type) + public bool MatchesAffliction(string identifier, string type) => + MatchesAffliction(identifier.ToIdentifier(), type.ToIdentifier()); + + public bool MatchesAffliction(Identifier identifier, Identifier type) { //if no identifiers or types have been defined, the damage modifier affects all afflictions if (AfflictionIdentifiers.Length == 0 && AfflictionTypes.Length == 0) { return true; } - return parsedAfflictionIdentifiers.Any(id => id.Equals(identifier, StringComparison.OrdinalIgnoreCase)) - || parsedAfflictionTypes.Any(t => t.Equals(type, StringComparison.OrdinalIgnoreCase)); + return parsedAfflictionIdentifiers.Any(id => id == identifier) + || parsedAfflictionTypes.Any(t => t == type); } public bool MatchesAffliction(Affliction affliction) => MatchesAffliction(affliction.Identifier, affliction.Prefab.AfflictionType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 47a66ff4f..5f9372f84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using System; +using Barotrauma.Extensions; using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -6,32 +7,29 @@ using System.Xml.Linq; namespace Barotrauma { - class HumanPrefab + class HumanPrefab : PrefabWithUintIdentifier { - [Serialize("notfound", false)] - public string Identifier { get; protected set; } - - [Serialize("any", false)] + [Serialize("any", IsPropertySaveable.No)] public string Job { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float Commonness { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float HealthMultiplier { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float HealthMultiplierInMultiplayer { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AimSpeed { get; protected set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float AimAccuracy { get; protected set; } - private readonly HashSet moduleFlags = new HashSet(); + private readonly HashSet moduleFlags = new HashSet(); - [Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")] + [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the NPC prefer to spawn in.")] public string ModuleFlags { get => string.Join(",", moduleFlags); @@ -43,16 +41,16 @@ namespace Barotrauma string[] splitFlags = value.Split(','); foreach (var f in splitFlags) { - moduleFlags.Add(f); + moduleFlags.Add(f.ToIdentifier()); } } } } - private readonly HashSet spawnPointTags = new HashSet(); + private readonly HashSet spawnPointTags = new HashSet(); - [Serialize("", true, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")] + [Serialize("", IsPropertySaveable.Yes, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")] public string SpawnPointTags { get => string.Join(",", spawnPointTags); @@ -64,27 +62,22 @@ namespace Barotrauma string[] splitTags = value.Split(','); foreach (var tag in splitTags) { - spawnPointTags.Add(tag.ToLowerInvariant()); + spawnPointTags.Add(tag.ToIdentifier()); } } } } - [Serialize(CampaignMode.InteractionType.None, false)] + [Serialize(CampaignMode.InteractionType.None, IsPropertySaveable.No)] public CampaignMode.InteractionType CampaignInteractionType { get; protected set; } - [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] public AIObjectiveIdle.BehaviorType Behavior { get; protected set; } - [Serialize(float.PositiveInfinity, false)] + [Serialize(float.PositiveInfinity, IsPropertySaveable.No)] public float ReportRange { get; protected set; } - public List PreferredOutpostModuleTypes { get; protected set; } - - public string OriginalName { get { return Identifier; } } - - - public string FilePath { get; protected set; } + public Identifier[] PreferredOutpostModuleTypes { get; protected set; } public XElement Element { get; protected set; } @@ -92,24 +85,22 @@ namespace Barotrauma public readonly Dictionary ItemSets = new Dictionary(); public readonly Dictionary CustomNPCSets = new Dictionary(); - public HumanPrefab(XElement element, string filePath) + public HumanPrefab(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - FilePath = filePath; SerializableProperty.DeserializeProperties(this, element); - Identifier = Identifier.ToLowerInvariant(); Job = Job.ToLowerInvariant(); Element = element; element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1))); element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1))); - PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList(); + PreferredOutpostModuleTypes = element.GetAttributeIdentifierArray("preferredoutpostmoduletypes", Array.Empty()); } - public IEnumerable GetModuleFlags() + public IEnumerable GetModuleFlags() { return moduleFlags; } - public IEnumerable GetSpawnPointTags() + public IEnumerable GetSpawnPointTags() { return spawnPointTags; } @@ -139,7 +130,7 @@ namespace Barotrauma else { idleObjective.Behavior = Behavior; - foreach (string moduleType in PreferredOutpostModuleTypes) + foreach (Identifier moduleType in PreferredOutpostModuleTypes) { idleObjective.PreferredOutpostModuleTypes.Add(moduleType); } @@ -180,7 +171,7 @@ namespace Barotrauma { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); @@ -217,28 +208,21 @@ namespace Barotrauma { character.Inventory.TryPutItem(item, null, item.AllowedSlots); } - if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") + IdCard idCardComponent = item.GetComponent(); + if (idCardComponent != null) { - item.AddTag("name:" + character.Name); - var job = character.Info?.Job; - if (job != null) - { - item.AddTag("job:" + job.Name); - } - - IdCard idCardComponent = item.GetComponent(); - idCardComponent?.Initialize(character.Info); + idCardComponent.Initialize(null, character); if (submarine != null && (submarine.Info.IsWreck || submarine.Info.IsOutpost)) { idCardComponent.SubmarineSpecificID = submarine.SubmarineSpecificIDTag; } - var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); + var idCardTags = itemElement.GetAttributeStringArray("tags", Array.Empty()); foreach (string tag in idCardTags) { item.AddTag(tag); } - } + } foreach (WifiComponent wifiComponent in item.GetComponents()) { @@ -250,5 +234,7 @@ namespace Barotrauma InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents); } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 7fd0524ed..3f1993a7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -9,87 +9,87 @@ namespace Barotrauma { private readonly JobPrefab prefab; - private readonly Dictionary skills; + private readonly Dictionary skills; - public string Name - { - get { return prefab.Name; } - } + public LocalizedString Name => prefab.Name; - public string Description - { - get { return prefab.Description; } - } + public LocalizedString Description => prefab.Description; - public JobPrefab Prefab - { - get { return prefab; } - } - - public List Skills - { - get { return skills.Values.ToList(); } - } + public JobPrefab Prefab => prefab; + + public List Skills => skills.Values.ToList(); public int Variant; public Skill PrimarySkill { get; } - public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0) + public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { } + + public Job(JobPrefab jobPrefab, Rand.RandSync randSync, int variant, params Skill[] s) { prefab = jobPrefab; Variant = variant; - skills = new Dictionary(); + skills = new Dictionary(); + foreach (var skill in s) { skills.Add(skill.Identifier, skill); } foreach (SkillPrefab skillPrefab in prefab.Skills) { - var skill = new Skill(skillPrefab, randSync); - skills.Add(skillPrefab.Identifier, skill); + Skill skill; + if (skills.ContainsKey(skillPrefab.Identifier)) + { + skill = skills[skillPrefab.Identifier]; + skills[skillPrefab.Identifier] = new Skill(skill.Identifier, skill.Level); + } + else + { + skill = new Skill(skillPrefab, randSync); + skills.Add(skillPrefab.Identifier, skill); + } if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; } } } public Job(XElement element) { - string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant(); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); JobPrefab p; if (!JobPrefab.Prefabs.ContainsKey(identifier)) { DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job."); - p = JobPrefab.Random(); + p = JobPrefab.Random(Rand.RandSync.Unsynced); } else { p = JobPrefab.Prefabs[identifier]; } prefab = p; - skills = new Dictionary(); - foreach (XElement subElement in element.Elements()) + skills = new Dictionary(); + foreach (var subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; } - string skillIdentifier = subElement.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(skillIdentifier)) { continue; } + if (subElement.NameAsIdentifier() != "skill") { continue; } + Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + if (skillIdentifier.IsEmpty) { continue; } var skill = new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)); skills.Add(skillIdentifier, skill); if (skillIdentifier == prefab.PrimarySkill?.Identifier) { PrimarySkill = skill; } } } - public static Job Random(Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static Job Random(Rand.RandSync randSync) { var prefab = JobPrefab.Random(randSync); var variant = Rand.Range(0, prefab.Variants, randSync); return new Job(prefab, randSync, variant); } - public float GetSkillLevel(string skillIdentifier) + public float GetSkillLevel(Identifier skillIdentifier) { - if (string.IsNullOrWhiteSpace(skillIdentifier)) { return 0.0f; } + if (skillIdentifier.IsEmpty) { return 0.0f; } skills.TryGetValue(skillIdentifier, out Skill skill); - return (skill == null) ? 0.0f : skill.Level; + return skill?.Level ?? 0.0f; } - public void IncreaseSkillLevel(string skillIdentifier, float increase, bool increasePastMax) + public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool increasePastMax) { if (skills.TryGetValue(skillIdentifier, out Skill skill)) { @@ -130,7 +130,7 @@ namespace Barotrauma else { string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; if (itemPrefab == null) { DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); @@ -192,27 +192,16 @@ namespace Barotrauma if (item.Prefab.Identifier == "idcard") { - if (spawnPoint != null) - { - foreach (string s in spawnPoint.IdCardTags) - { - item.AddTag(s); - if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) { item.Description = spawnPoint.IdCardDesc; } - } - } - item.AddTag("name:" + character.Name); - item.AddTag("job:" + Name); - IdCard idCardComponent = item.GetComponent(); - idCardComponent?.Initialize(character.Info); + idCardComponent?.Initialize(spawnPoint, character); } foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = character.TeamID; } - - if (parentItem != null) parentItem.Combine(item, user: null); + + if (parentItem != null) { parentItem.Combine(item, user: null); } foreach (XElement childItemElement in itemElement.Elements()) { @@ -227,7 +216,7 @@ namespace Barotrauma jobElement.Add(new XAttribute("name", Name)); jobElement.Add(new XAttribute("identifier", prefab.Identifier)); - foreach (KeyValuePair skill in skills) + foreach (KeyValuePair skill in skills) { jobElement.Add(new XElement("skill", new XAttribute("identifier", skill.Value.Identifier), new XAttribute("level", skill.Value.Level))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 442f29ab9..1d1c112f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -9,54 +10,78 @@ namespace Barotrauma { public class AutonomousObjective { - public string identifier; - public string option; - public readonly float priorityModifier; - public readonly bool ignoreAtOutpost; + public readonly Identifier Identifier; + public readonly Identifier Option; + public readonly float PriorityModifier; + public readonly bool IgnoreAtOutpost; public AutonomousObjective(XElement element) { - identifier = element.GetAttributeString("identifier", null); + Identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); //backwards compatibility - if (string.IsNullOrEmpty(identifier)) + if (Identifier == Identifier.Empty) { - identifier = element.GetAttributeString("aitag", null); + Identifier = element.GetAttributeIdentifier("aitag", Identifier.Empty); } - option = element.GetAttributeString("option", null); - priorityModifier = element.GetAttributeFloat("prioritymodifier", 1); - priorityModifier = MathHelper.Max(priorityModifier, 0); - ignoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false); + Option = element.GetAttributeIdentifier("option", Identifier.Empty); + PriorityModifier = element.GetAttributeFloat("prioritymodifier", 1); + PriorityModifier = MathHelper.Max(PriorityModifier, 0); + IgnoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false); } } - partial class JobPrefab : IPrefab, IDisposable + class ItemRepairPriority : Prefab + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public readonly float Priority; + + public ItemRepairPriority(XElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("tag", Identifier.Empty)) + { + Priority = element.GetAttributeFloat("priority", -1f); + if (Priority < 0) + { + DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}."); + } + } + + public override void Dispose() { } + } + + class JobVariant + { + public JobPrefab Prefab; + public int Variant; + public JobVariant(JobPrefab prefab, int variant) + { + Prefab = prefab; + Variant = variant; + } + } + + partial class JobPrefab : PrefabWithUintIdentifier { public static readonly PrefabCollection Prefabs = new PrefabCollection(); private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; Prefabs.Remove(this); } - private static readonly Dictionary _itemRepairPriorities = new Dictionary(); + private static readonly Dictionary _itemRepairPriorities = new Dictionary(); /// /// Tag -> priority. /// - public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; + public static IReadOnlyDictionary ItemRepairPriorities => _itemRepairPriorities; - public static XElement NoJobElement; + public static ContentXElement NoJobElement; public static JobPrefab Get(string identifier) { - if (Prefabs == null) - { - DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded."); - return null; - } if (Prefabs.ContainsKey(identifier)) { return Prefabs[identifier]; @@ -70,62 +95,41 @@ namespace Barotrauma public class PreviewItem { - public readonly string ItemIdentifier; + public readonly Identifier ItemIdentifier; public readonly bool ShowPreview; - public PreviewItem(string itemIdentifier, bool showPreview) + public PreviewItem(Identifier itemIdentifier, bool showPreview) { ItemIdentifier = itemIdentifier; ShowPreview = showPreview; } } - public readonly Dictionary ItemSets = new Dictionary(); - public readonly Dictionary> PreviewItems = new Dictionary>(); + public readonly Dictionary ItemSets = new Dictionary(); + public readonly ImmutableDictionary> PreviewItems; public readonly List Skills = new List(); public readonly List AutonomousObjectives = new List(); - public readonly List AppropriateOrders = new List(); + public readonly List AppropriateOrders = new List(); - [Serialize("1,1,1,1", false)] + [Serialize("1,1,1,1", IsPropertySaveable.No)] public Color UIColor { get; private set; } - [Serialize("notfound", false)] - public string Identifier - { - get; - private set; - } + public readonly LocalizedString Name; - [Serialize("notfound", false)] - public string Name - { - get; - private set; - } - - [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] + [Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)] public AIObjectiveIdle.BehaviorType IdleBehavior { get; private set; } - public string OriginalName { get { return Identifier; } } + public readonly LocalizedString Description; - public ContentPackage ContentPackage { get; private set; } - - [Serialize("", false)] - public string Description - { - get; - private set; - } - - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool OnlyJobSpecificDialog { get; @@ -133,7 +137,7 @@ namespace Barotrauma } //the number of these characters in the crew the player starts with in the single player campaign - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int InitialCount { get; @@ -141,7 +145,7 @@ namespace Barotrauma } //if set to true, a client that has chosen this as their preferred job will get it no matter what - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool AllowAlways { get; @@ -149,7 +153,7 @@ namespace Barotrauma } //how many crew members can have the job (only one captain etc) - [Serialize(100, false)] + [Serialize(100, IsPropertySaveable.No)] public int MaxNumber { get; @@ -158,21 +162,21 @@ namespace Barotrauma //how many crew members are REQUIRED to have the job //(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference) - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int MinNumber { get; private set; } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float MinKarma { get; private set; } - [Serialize(1.0f, false)] + [Serialize(1.0f, IsPropertySaveable.No)] public float PriceMultiplier { get; @@ -180,7 +184,7 @@ namespace Barotrauma } // TODO: not used - [Serialize(10.0f, false)] + [Serialize(10.0f, IsPropertySaveable.No)] public float Commonness { get; @@ -188,7 +192,7 @@ namespace Barotrauma } //how much the vitality of the character is increased/reduced from the default value - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] public float VitalityModifier { get; @@ -196,7 +200,7 @@ namespace Barotrauma } //whether the job should be available to NPCs - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool HiddenJob { get; @@ -208,35 +212,33 @@ namespace Barotrauma public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill); - public string FilePath { get; private set; } - - public XElement Element { get; private set; } - public XElement ClothingElement { get; private set; } + public ContentXElement Element { get; private set; } + public ContentXElement ClothingElement { get; private set; } public int Variants { get; private set; } - public JobPrefab(XElement element, string filePath) + public JobPrefab(ContentXElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - FilePath = filePath; SerializableProperty.DeserializeProperties(this, element); Name = TextManager.Get("JobName." + Identifier); - Description = TextManager.Get("JobDescription." + Identifier, returnNull: true) ?? string.Empty; - Identifier = Identifier.ToLowerInvariant(); + Description = TextManager.Get("JobDescription." + Identifier); Element = element; + var previewItems = new Dictionary>(); + int variant = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "itemset": ItemSets.Add(variant, subElement); - PreviewItems[variant] = new List(); + previewItems[variant] = new List(); loadItemIdentifiers(subElement, variant); variant++; break; case "skills": - foreach (XElement skillElement in subElement.Elements()) + foreach (var skillElement in subElement.Elements()) { Skills.Add(new SkillPrefab(skillElement)); } @@ -246,7 +248,7 @@ namespace Barotrauma break; case "appropriateobjectives": case "appropriateorders": - subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeString("identifier", "").ToLowerInvariant())); + subElement.Elements().ForEach(order => AppropriateOrders.Add(order.GetAttributeIdentifier("identifier", ""))); break; case "jobicon": Icon = new Sprite(subElement.FirstElement()); @@ -267,19 +269,22 @@ namespace Barotrauma continue; } - string itemIdentifier = itemElement.GetAttributeString("identifier", ""); - if (string.IsNullOrWhiteSpace(itemIdentifier)) + Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (itemIdentifier.IsEmpty) { DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier."); } else { - PreviewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true))); + previewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true))); } loadItemIdentifiers(itemElement, variant); } } + PreviewItems = previewItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())) + .ToImmutableDictionary(); + Variants = variant; Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start)); @@ -287,77 +292,7 @@ namespace Barotrauma // Disabled on purpose, TODO: remove all references? //ClothingElement = element.GetChildElement("PortraitClothing"); } - - public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => !p.HiddenJob, sync); - - public static void LoadAll(IEnumerable files) - { - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - if (doc.Root.IsOverride()) - { - DebugConsole.ThrowError($"Error in '{file.Path}': Cannot override all job prefabs, because many of them are required by the main game! Please try overriding jobs one by one."); - } - foreach (XElement element in mainElement.Elements()) - { - if (element.IsOverride()) - { - var job = new JobPrefab(element.FirstElement(), file.Path) - { - ContentPackage = file.ContentPackage - }; - Prefabs.Add(job, true); - } - else - { - if (!element.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) { continue; } - var job = new JobPrefab(element, file.Path) - { - ContentPackage = file.ContentPackage - }; - Prefabs.Add(job, false); - } - } - NoJobElement ??= mainElement.GetChildElement("nojob"); - var itemRepairPrioritiesElement = mainElement.GetChildElement("ItemRepairPriorities"); - if (itemRepairPrioritiesElement != null) - { - foreach (var subElement in itemRepairPrioritiesElement.Elements()) - { - string tag = subElement.GetAttributeString("tag", null); - if (tag != null) - { - float priority = subElement.GetAttributeFloat("priority", -1f); - if (priority >= 0) - { - _itemRepairPriorities.TryAdd(tag, priority); - } - else - { - DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}."); - } - } - else - { - DebugConsole.AddWarning($"The 'tag' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}."); - } - } - } - } - - public static void RemoveByFile(string filePath) - { - Prefabs.RemoveByFile(filePath); - } + public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index 2baae954b..41a894960 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -4,12 +4,12 @@ namespace Barotrauma { class Skill { - private float level; - - public string Identifier { get; } + public readonly Identifier Identifier; public const float MaximumSkill = 100.0f; + private float level; + public float Level { get { return level; } @@ -21,18 +21,11 @@ namespace Barotrauma level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill); } - private Sprite icon; - public Sprite Icon - { - get - { - if (icon == null) - { - icon = GetIcon(); - } - return icon; - } - } + private Identifier iconJobId; + + public Sprite Icon => !iconJobId.IsEmpty && JobPrefab.Prefabs.TryGet(iconJobId, out var jobPrefab) + ? jobPrefab.Icon + : null; public readonly float PriceMultiplier = 1.0f; @@ -40,39 +33,42 @@ namespace Barotrauma { Identifier = prefab.Identifier; level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); - icon = GetIcon(); + iconJobId = GetIconJobId(); PriceMultiplier = prefab.PriceMultiplier; } - public Skill(string identifier, float level) + public Skill(Identifier identifier, float level) { Identifier = identifier; this.level = level; - icon = GetIcon(); + iconJobId = GetIconJobId(); } - private Sprite GetIcon() + private Identifier GetIconJobId() { - string jobId = null; - switch (Identifier.ToLowerInvariant()) + Identifier jobId = Identifier.Empty; + if (Identifier == "electrical") { - case "electrical": - jobId = "engineer"; - break; - case "helm": - jobId = "captain"; - break; - case "mechanical": - jobId = "mechanic"; - break; - case "medical": - jobId = "medicaldoctor"; - break; - case "weapons": - jobId = "securityofficer"; - break; + jobId = "engineer".ToIdentifier(); } - return jobId != null && JobPrefab.Prefabs.ContainsKey(jobId) ? JobPrefab.Prefabs[jobId].IconSmall : null; + else if (Identifier == "helm") + { + jobId = "captain".ToIdentifier(); + } + else if (Identifier == "mechanical") + { + jobId = "mechanic".ToIdentifier(); + } + else if (Identifier == "medical") + { + jobId = "medicaldoctor".ToIdentifier(); + } + else if (Identifier == "weapons") + { + jobId = "securityofficer".ToIdentifier(); + } + + return jobId; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs index d4bb305f3..3a8ed2a6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/SkillPrefab.cs @@ -5,7 +5,7 @@ namespace Barotrauma { class SkillPrefab { - public readonly string Identifier; + public readonly Identifier Identifier; public Range LevelRange { get; private set; } @@ -16,9 +16,9 @@ namespace Barotrauma public bool IsPrimarySkill { get; } - public SkillPrefab(XElement element) + public SkillPrefab(ContentXElement element) { - Identifier = element.GetAttributeString("identifier", ""); + Identifier = element.GetAttributeIdentifier("identifier", ""); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f); var levelString = element.GetAttributeString("level", ""); if (levelString.Contains(",")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 978410599..97e51110f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -576,7 +576,7 @@ namespace Barotrauma } } - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -622,7 +622,7 @@ namespace Barotrauma body.BodyType = BodyType.Dynamic; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -644,9 +644,10 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } - if (character.VariantOf != null && character.Params.VariantFile != null) + if (!character.VariantOf.IsEmpty) { - var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); + var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab) + ? basePrefab.ConfigElement.GetChildElement("attack") : null; if (attackElement != null) { attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); @@ -668,7 +669,7 @@ namespace Barotrauma InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public void MoveToPos(Vector2 pos, float force, bool pullFromCenter = false) { @@ -1084,7 +1085,7 @@ namespace Barotrauma attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this); } } - /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) + /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)) { // TODO: use the hit pos? var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs index d2465d2f1..e12965df7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/NPCPersonalityTrait.cs @@ -8,15 +8,7 @@ namespace Barotrauma { class NPCPersonalityTrait { - private static List list = new List(); - public static List List - { - get { return list; } - } - - public readonly string FilePath; - - public readonly string Name; + public readonly Identifier Name; public readonly List AllowedDialogTags; @@ -26,20 +18,32 @@ namespace Barotrauma get { return commonness; } } - public NPCPersonalityTrait(XElement element, string filePath) + public static IEnumerable GetAll(LanguageIdentifier language) { - FilePath = filePath; - Name = element.GetAttributeString("name", ""); - AllowedDialogTags = new List(element.GetAttributeStringArray("alloweddialogtags", new string[0])); - commonness = element.GetAttributeFloat("commonness", 1.0f); + return NPCConversationCollection.Collections[language] + .SelectMany(cc => cc.PersonalityTraits.Values); + } - list.Add(this); + public static NPCPersonalityTrait Get(LanguageIdentifier language, Identifier traitName) + { + return NPCConversationCollection.Collections[language] + .FirstOrDefault(cc => cc.PersonalityTraits.ContainsKey(traitName)) + .PersonalityTraits[traitName]; + } + + public NPCPersonalityTrait(XElement element) + { + Name = element.GetAttributeIdentifier("name", ""); + AllowedDialogTags = new List(element.GetAttributeStringArray("alloweddialogtags", Array.Empty())); + commonness = element.GetAttributeFloat("commonness", 1.0f); } public static NPCPersonalityTrait GetRandom(string seed) { + #warning TODO: implement NPCPersonality content type and revise this for determinism var rand = new MTRandom(ToolBox.StringToInt(seed)); - return ToolBox.SelectWeightedRandom(list, list.Select(t => t.commonness).ToList(), rand); + var list = GetAll(GameSettings.CurrentConfig.Language); + return ToolBox.SelectWeightedRandom(list, t => t.commonness, rand); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 7b004f09d..f6a9d1d4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -21,71 +21,72 @@ namespace Barotrauma abstract class GroundedMovementParams : AnimationParams { - [Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)] public Vector2 StepSize { get; set; } - [Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float HeadPosition { get; set; } - [Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)] public float TorsoPosition { get; set; } - [Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)] + [Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)] public float StepLiftHeadMultiplier { get; set; } - [Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)] public float StepLiftAmount { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool MultiplyByDir { get; set; } - [Serialize(0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)] public float StepLiftOffset { get; set; } - [Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)] + [Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)] public float StepLiftFrequency { get; set; } - [Serialize(0.75f, true, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)] + [Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)] public float BackwardsMovementMultiplier { get; set; } } abstract class SwimParams : AnimationParams { - [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float SteerTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float LegTorque { get; set; } } abstract class AnimationParams : EditableParams, IMemorizable { - public string SpeciesName { get; private set; } + public Identifier SpeciesName { get; private set; } public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch; public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast; - protected static Dictionary> allAnimations = new Dictionary>(); + protected static Dictionary> allAnimations = new Dictionary>(); + /// allAnimations[speciesName][fileName] private float _movementSpeed; - [Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] public float MovementSpeed { get => _movementSpeed; set => _movementSpeed = value; } - [Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float CycleSpeed { get; set; } /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float HeadAngle { get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians); @@ -102,7 +103,7 @@ namespace Barotrauma /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float TorsoAngle { get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians); @@ -117,49 +118,44 @@ namespace Barotrauma public float TorsoAngleInRadians { get; private set; } = float.NaN; - [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TorsoTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float FootTorque { get; set; } - [Serialize(AnimationType.NotDefined, true), Editable] + [Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable] public virtual AnimationType AnimationType { get; protected set; } - [Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ArmIKStrength { get; set; } - [Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandIKStrength { get; set; } - public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}"; - public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); + public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}"; + public static string GetDefaultFile(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); - public static string GetFolder(string speciesName) + public static string GetFolder(Identifier speciesName) { CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName); - if (prefab?.XDocument == null) + if (prefab?.ConfigElement == null) { DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'"); return string.Empty; } - return GetFolder(prefab.XDocument, prefab.FilePath); + return GetFolder(prefab.ConfigElement, prefab.FilePath.Value); } - public static string GetFolder(XDocument doc, string filePath) + private static string GetFolder(ContentXElement root, string filePath) { - var root = doc.Root; - if (root?.IsOverride() ?? false) - { - root = root.FirstElement(); - } - var folder = root?.Element("animations")?.GetAttributeString("folder", string.Empty); + var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value; if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { - folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations"); + folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations"); } return folder.CleanUpPathCrossPlatform(true); } @@ -167,9 +163,9 @@ namespace Barotrauma /// /// Selects a random filepath from multiple paths, matching the specified animation type. /// - public static string GetRandomFilePath(IEnumerable filePaths, AnimationType type) + public static string GetRandomFilePath(IReadOnlyList filePaths, AnimationType type) { - return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.Server); + return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient); } /// @@ -194,11 +190,12 @@ namespace Barotrauma public static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() { - string speciesName = character.VariantOf ?? character.SpeciesName; - if (character.VariantOf != null && character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeString("folder", null) != null) + Identifier speciesName = character.SpeciesName; + if (!character.VariantOf.IsEmpty + && (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty()) { - // Use the overridden animations defined in the variant definition file. - speciesName = character.SpeciesName; + // Use the base animations defined in the base definition file. + speciesName = character.VariantOf; } return GetAnimParams(speciesName, animType, GetDefaultFileName(speciesName, animType)); } @@ -207,7 +204,7 @@ namespace Barotrauma /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! /// If a custom folder is used, it's defined in the character info file. /// - public static T GetAnimParams(string speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new() + public static T GetAnimParams(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new() { if (!allAnimations.TryGetValue(speciesName, out Dictionary anims)) { @@ -239,7 +236,7 @@ namespace Barotrauma } else { - selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); @@ -257,10 +254,11 @@ namespace Barotrauma throw new Exception("[AnimationParams] Selected file null!"); } DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}."); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; T a = new T(); - if (a.Load(selectedFile, speciesName)) + if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName)) { - fileName = Path.GetFileNameWithoutExtension(selectedFile); + fileName = IO.Path.GetFileNameWithoutExtension(selectedFile); if (!anims.ContainsKey(fileName)) { anims.Add(fileName, a); @@ -277,7 +275,7 @@ namespace Barotrauma public static void ClearCache() => allAnimations.Clear(); - public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type) + public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type type) { if (type == typeof(HumanWalkParams)) { @@ -317,7 +315,7 @@ namespace Barotrauma /// /// Note: Overrides old animations, if found! /// - public static T Create(string fullPath, string speciesName, AnimationType animationType) where T : AnimationParams, new() + public static T Create(string fullPath, Identifier speciesName, AnimationType animationType) where T : AnimationParams, new() { if (animationType == AnimationType.NotDefined) { @@ -328,7 +326,7 @@ namespace Barotrauma anims = new Dictionary(); allAnimations.Add(speciesName, anims); } - var fileName = Path.GetFileNameWithoutExtension(fullPath); + var fileName = IO.Path.GetFileNameWithoutExtension(fullPath); if (anims.ContainsKey(fileName)) { DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red); @@ -337,10 +335,12 @@ namespace Barotrauma var instance = new T(); XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString())); instance.doc = new XDocument(animationElement); - instance.UpdatePath(fullPath); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath); + instance.UpdatePath(contentPath); instance.IsLoaded = instance.Deserialize(animationElement); instance.Save(); - instance.Load(fullPath, speciesName); + instance.Load(contentPath, speciesName); anims.Add(fileName, instance); DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite); return instance; @@ -349,7 +349,7 @@ namespace Barotrauma public bool Serialize() => base.Serialize(); public bool Deserialize() => base.Deserialize(); - protected bool Load(string file, string speciesName) + protected bool Load(ContentPath file, Identifier speciesName) { if (Load(file)) { @@ -359,7 +359,7 @@ namespace Barotrauma return false; } - protected override void UpdatePath(string newPath) + protected override void UpdatePath(ContentPath newPath) { if (SpeciesName == null) { @@ -464,7 +464,8 @@ namespace Barotrauma var copy = new T { IsLoaded = true, - doc = new XDocument(doc) + doc = new XDocument(doc), + Path = Path }; copy.Deserialize(); copy.Serialize(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index c2257379b..e844e53a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -11,7 +11,7 @@ namespace Barotrauma } public static FishWalkParams GetAnimParams(Character character, string fileName = null) { - return Check(character) ? GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.Walk, fileName) : Empty; + return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName) : Empty; } protected static FishWalkParams Empty = new FishWalkParams(); @@ -27,7 +27,7 @@ namespace Barotrauma } public static FishRunParams GetAnimParams(Character character, string fileName = null) { - return Check(character) ? GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.Run, fileName) : Empty; + return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Run, fileName) : Empty; } protected static FishRunParams Empty = new FishRunParams(); @@ -40,7 +40,7 @@ namespace Barotrauma public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); public static FishSwimFastParams GetAnimParams(Character character, string fileName = null) { - return GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); } public override void StoreSnapshot() => StoreSnapshot(); @@ -51,7 +51,7 @@ namespace Barotrauma public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null) { - return GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); } public override void StoreSnapshot() => StoreSnapshot(); @@ -69,35 +69,35 @@ namespace Barotrauma return true; } - [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] public bool Flip { get; set; } - [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] public float FlipCooldown { get; set; } - [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] public float FlipDelay { get; set; } - [Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float HeadMoveForce { get; set; } - [Serialize(10.0f, true, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much force is used to move the torso to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float TorsoMoveForce { get; set; } - [Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(8.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveForce { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float LegTorque { get; set; } /// /// The angle of the collider when standing (i.e. out of water). /// In degrees. /// - [Serialize(0f, true, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] + [Serialize(0f, IsPropertySaveable.Yes, description: "The angle of the character's collider when standing."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] public float ColliderStandAngle { get => MathHelper.ToDegrees(ColliderStandAngleInRadians); @@ -105,7 +105,7 @@ namespace Barotrauma } public float ColliderStandAngleInRadians { get; private set; } - [Serialize(null, true), Editable] + [Serialize(null, IsPropertySaveable.Yes), Editable] public string FootAngles { get => ParseFootAngles(FootAnglesInRadians); @@ -120,7 +120,7 @@ namespace Barotrauma /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float TailAngle { get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians); @@ -137,41 +137,40 @@ namespace Barotrauma abstract class FishSwimParams : SwimParams, IFishAnimation { - [Serialize(false, true, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable] public bool UseSineMovement { get; set; } - [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] public bool Flip { get; set; } - [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] public float FlipCooldown { get; set; } - [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] public float FlipDelay { get; set; } - [Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")] public bool Mirror { get; set; } - [Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Disabling this will make mirroring instantaneous.")] public bool MirrorLerp { get; set; } - [Serialize(5f, true), Editable] + [Serialize(5f, IsPropertySaveable.Yes), Editable] public float WaveAmplitude { get; set; } - [Serialize(10.0f, true), Editable] + [Serialize(10.0f, IsPropertySaveable.Yes), Editable] public float WaveLength { get; set; } - [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] + [Serialize(1f, IsPropertySaveable.Yes, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] public float TailTorqueMultiplier { get; set; } - - [Serialize(null, true), Editable] + [Serialize(null, IsPropertySaveable.Yes), Editable] public string FootAngles { get => ParseFootAngles(FootAnglesInRadians); @@ -186,7 +185,7 @@ namespace Barotrauma /// /// In degrees. /// - [Serialize(float.NaN, true), Editable(-360f, 360f)] + [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float TailAngle { get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 43ed47198..2fdb19b98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -26,13 +26,13 @@ namespace Barotrauma class HumanCrouchParams : HumanGroundedParams { - [Serialize(0.0f, true, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] public float MoveDownAmountWhenStationary { get; set; } - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float ExtraHeadAngleWhenStationary { get; set; } - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float ExtraTorsoAngleWhenStationary { get; set; } public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); @@ -69,25 +69,25 @@ namespace Barotrauma abstract class HumanSwimParams : SwimParams, IHumanAnimation { - [Serialize(0.5f, true), Editable(DecimalCount = 2)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float LegMoveAmount { get; set; } - [Serialize(5.0f, true), Editable] + [Serialize(5.0f, IsPropertySaveable.Yes), Editable] public float LegCycleLength { get; set; } - [Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)] + [Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize(5.0f, true), Editable] + [Serialize(5.0f, IsPropertySaveable.Yes), Editable] public float HandCycleSpeed { get; set; } - [Serialize("0.0, 0.0", true), Editable(DecimalCount = 2)] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 HandMoveOffset { get; set; } /// /// In degrees. /// - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float FootAngle { get => MathHelper.ToDegrees(FootAngleInRadians); @@ -98,34 +98,34 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] public float ArmMoveStrength { get; set; } - [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation { - [Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] + [Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float GetUpForce { get; set; } - [Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] public float HeadLeanAmount { get; set; } - [Serialize(0.25f, true, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)] public float TorsoLeanAmount { get; set; } - [Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveStrength { get; set; } - [Serialize(0f, true, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] public float FootLiftHorizontalFactor { get; set; } /// /// In degrees. /// - [Serialize(0.0f, true), Editable(-360f, 360f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(-360f, 360f)] public float FootAngle { get => MathHelper.ToDegrees(FootAngleInRadians); @@ -136,25 +136,25 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] public Vector2 FootMoveOffset { get; set; } - [Serialize(10.0f, true, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float LegBendTorque { get; set; } - [Serialize("0.4, 0.15", true, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)] + [Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize("-0.15, 0.0", true, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)] + [Serialize("-0.15, 0.0", IsPropertySaveable.Yes, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)] public Vector2 HandMoveOffset { get; set; } - [Serialize(-1.0f, true, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)] + [Serialize(-1.0f, IsPropertySaveable.Yes, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)] public float HandClampY { get; set; } - [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ArmMoveStrength { get; set; } - [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 4d03cd6fd..5733c62f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using System.Xml; using System.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; #if CLIENT using SoundType = Barotrauma.CharacterSound.SoundType; #endif @@ -16,94 +17,94 @@ namespace Barotrauma /// class CharacterParams : EditableParams { - [Serialize("", true), Editable] - public string SpeciesName { get; private set; } + [Serialize("", IsPropertySaveable.Yes), Editable] + public Identifier SpeciesName { get; private set; } - [Serialize("", true, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable] public string SpeciesTranslationOverride { get; private set; } - [Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] public string DisplayName { get; private set; } - [Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] - public string Group { get; private set; } + [Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] + public Identifier Group { get; private set; } - [Serialize(false, true), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] public bool Humanoid { get; private set; } - [Serialize(false, true), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } - [Serialize(false, true, description: "Can the creature interact with items?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable] public bool CanInteract { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool Husk { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseHuskAppendage { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool NeedsAir { get; set; } - [Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } - [Serialize(false, false), Editable] + [Serialize(false, IsPropertySaveable.No), Editable] public bool CanSpeak { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool UseBossHealthBar { get; private set; } - [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)] public float Noise { get; set; } - [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)] public float Visibility { get; set; } - [Serialize("blood", true), Editable] + [Serialize("blood", IsPropertySaveable.Yes), Editable] public string BloodDecal { get; private set; } - [Serialize("blooddrop", true), Editable] + [Serialize("blooddrop", IsPropertySaveable.Yes), Editable] public string BleedParticleAir { get; private set; } - [Serialize("waterblood", true), Editable] + [Serialize("waterblood", IsPropertySaveable.Yes), Editable] public string BleedParticleWater { get; private set; } - [Serialize(1f, true), Editable] + [Serialize(1f, IsPropertySaveable.Yes), Editable] public float BleedParticleMultiplier { get; private set; } - - [Serialize(true, true, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] + + [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable] public bool CanEat { get; set; } - [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool UsePathFinding { get; set; } - [Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] + [Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] public float PathFinderPriority { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HideInSonar { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HideInThermalGoggles { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float SonarDisruption { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float DistantSonarRange { get; set; } - [Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] + [Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] public float DisableDistance { get; set; } - [Serialize(10f, true, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] + [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } - public readonly string File; + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -116,7 +117,7 @@ namespace Barotrauma public HealthParams Health { get; private set; } public AIParams AI { get; private set; } - public CharacterParams(string file) + public CharacterParams(CharacterFile file) { File = file; Load(); @@ -124,45 +125,58 @@ namespace Barotrauma protected override string GetName() => "Character Config File"; - public override XElement MainElement => doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; + public static XElement CreateVariantXml(XElement selfXml, XElement parentXml) + { + XElement newXml = selfXml.CreateVariantXML(parentXml); + + XElement selfAi = selfXml.GetChildElement("ai"); + XElement parentAi = parentXml.GetChildElement("ai"); + + if (parentAi is null || parentAi.Elements().None() + || selfAi is null || selfAi.Elements().None()) + { + return newXml; + } + + //discard the inherited targets, just keep the new ones + var finalAiElement = newXml.GetChildElement("ai"); + foreach (var finalTarget in finalAiElement!.Elements().ToArray()) + { + finalTarget.Remove(); + } + + foreach (var inheritorTarget in selfAi.Elements()) + { + finalAiElement.Add(new XElement(inheritorTarget)); + } + + return newXml; + } + public bool Load() { - bool success = base.Load(File); - if (doc.Root.IsCharacterVariant()) + UpdatePath(File.Path); + doc = XMLExtensions.TryLoadXml(Path); + Identifier variantOf = MainElement.VariantOf(); + if (!variantOf.IsEmpty) { VariantFile = doc; - var original = CharacterPrefab.FindBySpeciesName(doc.Root.GetAttributeString("inherit", string.Empty)); - success = Load(original.FilePath); - CreateSubParams(); - TryLoadOverride(this, VariantFile.Root, SerializableProperties); - foreach (XElement subElement in VariantFile.Root.Elements()) - { - var matchingParams = SubParams.FirstOrDefault(p => p.Name.Equals(subElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (matchingParams != null) - { - TryLoadOverride(matchingParams, subElement, matchingParams.SerializableProperties); - // TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion. - foreach (XElement subSubElement in subElement.Elements()) - { - if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } - var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (matchingSubParams != null) - { - TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties); - } - } - } - } - return success; + #warning TODO: determine that CreateVariantXML is equipped to do this + XElement newRoot = CreateVariantXml(MainElement, CharacterPrefab.FindBySpeciesName(variantOf).ConfigElement); + var oldElement = MainElement; + var parentElement = (XContainer)oldElement.Parent ?? doc; oldElement.Remove(); parentElement.Add(newRoot); } - if (string.IsNullOrEmpty(SpeciesName) && MainElement != null) + IsLoaded = Deserialize(MainElement); + OriginalElement = new XElement(MainElement).FromPackage(Path.ContentPackage); + if (SpeciesName.IsEmpty && MainElement != null) { //backwards compatibility - SpeciesName = MainElement.GetAttributeString("name", ""); + SpeciesName = MainElement.GetAttributeIdentifier("name", ""); } CreateSubParams(); - return success; + return IsLoaded; } public bool Save(string fileNameWithoutExtension = null) @@ -189,7 +203,7 @@ namespace Barotrauma return true; } - public bool CompareGroup(string group) => !string.IsNullOrWhiteSpace(group) && !string.IsNullOrWhiteSpace(Group) && group.Equals(Group, StringComparison.OrdinalIgnoreCase); + public bool CompareGroup(Identifier group) => group != Identifier.Empty && Group != Identifier.Empty && group == Group; protected void CreateSubParams() { @@ -239,26 +253,14 @@ namespace Barotrauma } } - private void TryLoadOverride(object parentObject, XElement element, Dictionary properties) - { - foreach (var property in properties) - { - var matchingAttribute = element.GetAttribute(property.Key); - if (matchingAttribute != null) - { - property.Value.TrySetValue(parentObject, matchingAttribute.Value); - } - } - } - public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true, bool loadDefaultValues = true) { if (base.Deserialize(element)) { //backwards compatibility - if (string.IsNullOrEmpty(SpeciesName)) + if (SpeciesName.IsEmpty) { - SpeciesName = element.GetAttributeString("name", "[NAME NOT GIVEN]"); + SpeciesName = element.GetAttributeIdentifier("name", "[NAME NOT GIVEN]"); } if (alsoChildren) { @@ -300,9 +302,9 @@ namespace Barotrauma } #endif - public bool AddSound() => TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds); + public bool AddSound() => TryAddSubParam(CreateElement("sound"), (e, c) => new SoundParams(e, c), out _, Sounds); - public void AddInventory() => TryAddSubParam(new XElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories); + public void AddInventory() => TryAddSubParam(CreateElement("inventory", new XElement("item")), (e, c) => new InventoryParams(e, c), out _, Inventories); public void AddBloodEmitter() => AddEmitter("bloodemitter"); public void AddGibEmitter() => AddEmitter("gibemitter"); @@ -313,13 +315,13 @@ namespace Barotrauma switch (type) { case "gibemitter": - TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters); + TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, GibEmitters); break; case "bloodemitter": - TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters); + TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters); break; case "damageemitter": - TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters); + TryAddSubParam(CreateElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters); break; default: throw new NotImplementedException(type); } @@ -342,7 +344,7 @@ namespace Barotrauma return true; } - protected bool TryAddSubParam(XElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam + protected bool TryAddSubParam(ContentXElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam { subParam = constructor(element, this); if (collection != null && filter != null) @@ -360,24 +362,39 @@ namespace Barotrauma { public override string Name => "Sound"; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string File { get; private set; } #if CLIENT - [Serialize(SoundType.Idle, true), Editable] + [Serialize(SoundType.Idle, IsPropertySaveable.Yes), Editable] public SoundType State { get; private set; } #endif - [Serialize(1000f, true), Editable(minValue: 0f, maxValue: 10000f)] + [Serialize(1000f, IsPropertySaveable.Yes), Editable(minValue: 0f, maxValue: 10000f)] public float Range { get; private set; } - [Serialize(1.0f, true), Editable(minValue: 0f, maxValue: 2.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(minValue: 0f, maxValue: 2.0f)] public float Volume { get; private set; } - [Serialize(Gender.None, true, description: "Is the sound gender specific?"), Editable()] - public Gender Gender { get; private set; } + [Serialize("", IsPropertySaveable.Yes, description: "Which tags are required for this sound to play?"), Editable()] + public string Tags + { + get { return string.Join(',', TagSet); } + private set { TagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet(); } + } - public SoundParams(XElement element, CharacterParams character) : base(element, character) { } + public ImmutableHashSet TagSet { get; private set; } + + public SoundParams(ContentXElement element, CharacterParams character) : base(element, character) + { + HashSet tags = TagSet.ToHashSet(); + Identifier genderFallback = element.GetAttributeIdentifier("gender", ""); + if (genderFallback != Identifier.Empty && genderFallback != "None") + { + tags.Add(genderFallback); + } + TagSet = tags.ToImmutableHashSet(); + } } public class ParticleParams : SubParam @@ -395,83 +412,83 @@ namespace Barotrauma } } - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string Particle { get; set; } - [Serialize(0f, true), Editable(-360f, 360f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(-360f, 360f, decimals: 0)] public float AngleMin { get; private set; } - [Serialize(0f, true), Editable(-360f, 360f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(-360f, 360f, decimals: 0)] public float AngleMax { get; private set; } - [Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)] public float ScaleMin { get; private set; } - [Serialize(1.0f, true), Editable(0f, 100f, decimals: 2)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)] public float ScaleMax { get; private set; } - [Serialize(0f, true), Editable(0f, 10000f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 10000f, decimals: 0)] public float VelocityMin { get; private set; } - [Serialize(0f, true), Editable(0f, 10000f, decimals: 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 10000f, decimals: 0)] public float VelocityMax { get; private set; } - [Serialize(0f, true), Editable(0f, 100f, decimals: 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0f, 100f, decimals: 2)] public float EmitInterval { get; private set; } - [Serialize(0, true), Editable(0, 1000)] + [Serialize(0, IsPropertySaveable.Yes), Editable(0, 1000)] public int ParticlesPerSecond { get; private set; } - [Serialize(0, true), Editable(0, 1000)] + [Serialize(0, IsPropertySaveable.Yes), Editable(0, 1000)] public int ParticleAmount { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HighQualityCollisionDetection { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool CopyEntityAngle { get; private set; } - public ParticleParams(XElement element, CharacterParams character) : base(element, character) { } + public ParticleParams(ContentXElement element, CharacterParams character) : base(element, character) { } } public class HealthParams : SubParam { public override string Name => "Health"; - [Serialize(100f, true, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How much (max) health does the character have?"), Editable(minValue: 1, maxValue: 10000f)] public float Vitality { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool DoesBleed { get; set; } - [Serialize(float.NegativeInfinity, true), Editable(minValue: float.NegativeInfinity, maxValue: 0)] + [Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)] public float CrushDepth { get; set; } // Make editable? - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool UseHealthWindow { get; set; } - [Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float BleedingReduction { get; private set; } - [Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float BurnReduction { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ConstantHealthRegeneration { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool StunImmunity { get; set; } - [Serialize(false, true, description: "Can afflictions affect the face/body tint of the character."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } // TODO: limbhealths, sprite? - public HealthParams(XElement element, CharacterParams character) : base(element, character) { } + public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { } } public class InventoryParams : SubParam @@ -480,26 +497,26 @@ namespace Barotrauma { public override string Name => "Item"; - [Serialize("", true, description: "Item identifier."), Editable()] + [Serialize("", IsPropertySaveable.Yes, description: "Item identifier."), Editable()] public string Identifier { get; private set; } - public InventoryItem(XElement element, CharacterParams character) : base(element, character) { } + public InventoryItem(ContentXElement element, CharacterParams character) : base(element, character) { } } public override string Name => "Inventory"; - [Serialize("Any, Any", true, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()] + [Serialize("Any, Any", IsPropertySaveable.Yes, description: "Which slots the inventory holds? Accepted types: None, Any, RightHand, LeftHand, Head, InnerClothes, OuterClothes, Headset, and Card."), Editable()] public string Slots { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AccessibleWhenAlive { get; private set; } - [Serialize(1.0f, true, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "What are the odds that this inventory is spawned on the character?"), Editable(minValue: 0f, maxValue: 1.0f)] public float Commonness { get; private set; } public List Items { get; private set; } = new List(); - public InventoryParams(XElement element, CharacterParams character) : base(element, character) + public InventoryParams(ContentXElement element, CharacterParams character) : base(element, character) { foreach (var itemElement in element.GetChildElements("item")) { @@ -512,7 +529,7 @@ namespace Barotrauma public void AddItem(string identifier = null) { identifier = identifier ?? ""; - var element = new XElement("item", new XAttribute("identifier", identifier)); + var element = CreateElement("item", new XAttribute("identifier", identifier)); Element.Add(element); var item = new InventoryItem(element, Character); SubParams.Add(item); @@ -526,89 +543,89 @@ namespace Barotrauma { public override string Name => "AI"; - [Serialize(1.0f, true, description: "How strong other characters think this character is? Only affects AI."), Editable()] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How strong other characters think this character is? Only affects AI."), Editable()] public float CombatStrength { get; private set; } - [Serialize(1.0f, true, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can see the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Sight { get; private set; } - [Serialize(1.0f, true, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Hearing { get; private set; } - [Serialize(100f, true, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] + [Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] public float AggressionHurt { get; private set; } - [Serialize(10f, true, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(10f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)] public float AggressionGreed { get; private set; } - [Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)] + [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; } - [Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] + [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; } - [Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } - [Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] + [Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] public float AvoidTime { get; private set; } - [Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] + [Serialize(20f, IsPropertySaveable.Yes, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] public float MinFleeTime { get; private set; } - [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the character try to break inside the sub?"), Editable] public bool AggressiveBoarding { get; private set; } - [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable] public bool EnforceAggressiveBehaviorForMissions { get; private set; } - [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the character target or ignore walls when it's outside the submarine."), Editable] public bool TargetOuterWalls { get; private set; } - [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable] public bool RandomAttack { get; private set; } - [Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } - [Serialize(false, true, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] public bool KeepDoorsClosed { get; private set; } - [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] + [Serialize(true, IsPropertySaveable.Yes, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] public bool AvoidAbyss { get; set; } - [Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] + [Serialize(false, IsPropertySaveable.Yes, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] public bool StayInAbyss { get; set; } - - [Serialize(false, true, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable] + + [Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable] public bool PatrolFlooded { get; set; } - [Serialize(false, true, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] + [Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] public bool PatrolDry { get; set; } - [Serialize(0f, true, description: ""), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] public float StartAggression { get; private set; } - [Serialize(100f, true, description: ""), Editable] + [Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable] public float MaxAggression { get; private set; } - [Serialize(0f, true, description: ""), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable] public float AggressionCumulation { get; private set; } - [Serialize(WallTargetingMethod.Target, true, description: ""), Editable] + [Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable] public WallTargetingMethod WallTargetingMethod { get; private set; } public IEnumerable Targets => targets; protected readonly List targets = new List(); - public AIParams(XElement element, CharacterParams character) : base(element, character) + public AIParams(ContentXElement element, CharacterParams character) : base(element, character) { if (element == null) { return; } element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _)); element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _)); } - private bool TryAddTarget(XElement targetElement, out TargetParams target) + private bool TryAddTarget(ContentXElement targetElement, out TargetParams target) { string tag = targetElement.GetAttributeString("tag", null); if (HasTag(tag)) @@ -628,9 +645,12 @@ namespace Barotrauma public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams); - public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams) + public bool TryAddNewTarget(string tag, AIState state, float priority, out TargetParams targetParams) => + TryAddNewTarget(tag.ToIdentifier(), state, priority, out targetParams); + + public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams) { - var element = TargetParams.CreateNewElement(tag, state, priority); + var element = TargetParams.CreateNewElement(Character, tag, state, priority); if (TryAddTarget(element, out targetParams)) { Element.Add(element); @@ -642,17 +662,22 @@ namespace Barotrauma } } - public bool HasTag(string tag) + public bool HasTag(string tag) => HasTag(tag.ToIdentifier()); + + public bool HasTag(Identifier tag) { if (tag == null) { return false; } - return targets.Any(t => t.Tag.Equals(tag, StringComparison.OrdinalIgnoreCase)); + return targets.Any(t => t.Tag == tag); } public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets); public bool TryGetTarget(string targetTag, out TargetParams target) + => TryGetTarget(targetTag.ToIdentifier(), out target); + + public bool TryGetTarget(Identifier targetTag, out TargetParams target) { - target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetTag, StringComparison.OrdinalIgnoreCase)); + target = targets.FirstOrDefault(t => t.Tag == targetTag); return target != null; } @@ -665,7 +690,7 @@ namespace Barotrauma return target != null; } - public bool TryGetTarget(IEnumerable tags, out TargetParams target) + public bool TryGetTarget(IEnumerable tags, out TargetParams target) { target = null; if (tags == null || tags.None()) { return false; } @@ -674,7 +699,7 @@ namespace Barotrauma { if (potentialTarget.Priority > priority) { - if (tags.Any(t => string.Equals(t, potentialTarget.Tag, StringComparison.OrdinalIgnoreCase))) + if (tags.Any(t => t == potentialTarget.Tag)) { target = potentialTarget; priority = target.Priority; @@ -685,7 +710,11 @@ namespace Barotrauma } public TargetParams GetTarget(string targetTag, bool throwError = true) + => GetTarget(targetTag.ToIdentifier(), throwError); + + public TargetParams GetTarget(Identifier targetTag, bool throwError = true) { + if (targetTag.IsEmpty) { return null; } if (!TryGetTarget(targetTag, out TargetParams target)) { if (throwError) @@ -701,102 +730,108 @@ namespace Barotrauma { public override string Name => "Target"; - [Serialize("", true, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()] + [Serialize("", IsPropertySaveable.Yes, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()] public string Tag { get; private set; } - [Serialize(AIState.Idle, true), Editable] + [Serialize(AIState.Idle, IsPropertySaveable.Yes), Editable] public AIState State { get; set; } - [Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)] public float Priority { get; set; } - [Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] public float ReactDistance { get; set; } - [Serialize(0f, true, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)] public float AttackDistance { get; set; } - [Serialize(0f, true, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable] public float Timer { get; set; } - [Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable] public bool IgnoreContained { get; set; } - [Serialize(false, true, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable] public bool IgnoreInside { get; set; } - [Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] public bool IgnoreOutside { get; set; } - [Serialize(false, true, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] public bool IgnoreIfNotInSameSub { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool IgnoreIncapacitated { get; set; } - [Serialize(0f, true, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable] + [Serialize(0f, IsPropertySaveable.Yes, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable] public float Threshold { get; private set; } - [Serialize(-1f, true, description: "A generic min threshold. Not used if set to negative."), Editable] + [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic min threshold. Not used if set to negative."), Editable] public float ThresholdMin { get; private set; } - [Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable] + [Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable] public float ThresholdMax { get; private set; } - [Serialize("0.0, 0.0", true), Editable] + [Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable] public Vector2 Offset { get; private set; } - [Serialize(AttackPattern.Straight, true), Editable] + [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable] public AttackPattern AttackPattern { get; set; } #region Sweep - [Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] public float SweepDistance { get; private set; } - [Serialize(10f, true, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)] + [Serialize(10f, IsPropertySaveable.Yes, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)] public float SweepStrength { get; private set; } - [Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] public float SweepSpeed { get; private set; } #endregion #region Circle - [Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] + [Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] public float CircleStartDistance { get; private set; } - [Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] public float CircleRotationSpeed { get; private set; } - [Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + [Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] public float CircleStrikeDistanceMultiplier { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] public float CircleMaxRandomOffset { get; private set; } #endregion - public TargetParams(XElement element, CharacterParams character) : base(element, character) { } + public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { } - public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { } + public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { } - public static XElement CreateNewElement(string tag, AIState state, float priority) + public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) => + CreateNewElement(character, tag.Value, state, priority); + + public static ContentXElement CreateNewElement(CharacterParams character, string tag, AIState state, float priority) { return new XElement("target", new XAttribute("tag", tag), new XAttribute("state", state), - new XAttribute("priority", priority)); + new XAttribute("priority", priority)).FromPackage(character.File.ContentPackage); } } public abstract class SubParam : ISerializableEntity { public virtual string Name { get; set; } - public Dictionary SerializableProperties { get; private set; } - public XElement Element { get; set; } + public Dictionary SerializableProperties { get; private set; } + public ContentXElement Element { get; set; } public List SubParams { get; set; } = new List(); public CharacterParams Character { get; private set; } - public SubParam(XElement element, CharacterParams character) + protected ContentXElement CreateElement(string name, params object[] attrs) + => new XElement(name, attrs).FromPackage(Element.ContentPackage); + + public SubParam(ContentXElement element, CharacterParams character) { Element = element; Character = character; @@ -843,12 +878,12 @@ namespace Barotrauma #if CLIENT public SerializableEntityEditor SerializableEntityEditor { get; protected set; } - public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, ScalableFont titleFont = null) + public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0, GUIFont titleFont = null) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: titleFont ?? GUIStyle.LargeFont); if (recursive) { - SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUI.SmallFont)); + SubParams.ForEach(sp => sp.AddToEditor(editor, true, titleFont: titleFont ?? GUIStyle.SmallFont)); } if (space > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 77fff0fd0..76e138579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Xml.Linq; using Microsoft.Xna.Framework; +using File = Barotrauma.IO.File; #if DEBUG using System.IO; using System.Xml; @@ -16,11 +17,13 @@ namespace Barotrauma public string Name { get; private set; } public string FileName { get; private set; } public string Folder { get; private set; } - public string FullPath { get; private set; } - public Dictionary SerializableProperties { get; protected set; } + public ContentPath Path { get; protected set; } = ContentPath.Empty; + public Dictionary SerializableProperties { get; protected set; } + protected ContentXElement rootElement; protected XDocument doc; - public XDocument Doc + + private XDocument Doc { get { @@ -31,16 +34,30 @@ namespace Barotrauma } return doc; } - protected set + set { doc = value; } } - public virtual XElement MainElement => doc.Root; - public XElement OriginalElement { get; protected set; } + public virtual ContentXElement MainElement + { + get + { + if (rootElement?.Element != doc.Root) + { + rootElement = doc.Root.FromPackage(Path.ContentPackage); + } + return rootElement; + } + } + + public ContentXElement OriginalElement { get; protected set; } - protected virtual string GetName() => Path.GetFileNameWithoutExtension(FullPath).FormatCamelCaseWithSpaces(); + protected ContentXElement CreateElement(string name, params object[] attrs) + => new XElement(name, attrs).FromPackage(Path.ContentPackage); + + protected virtual string GetName() => System.IO.Path.GetFileNameWithoutExtension(Path.Value).FormatCamelCaseWithSpaces(); protected virtual bool Deserialize(XElement element = null) { @@ -61,22 +78,22 @@ namespace Barotrauma return true; } - protected virtual bool Load(string file) + protected virtual bool Load(ContentPath file) { UpdatePath(file); - doc = XMLExtensions.TryLoadXml(FullPath); + doc = XMLExtensions.TryLoadXml(Path); if (doc == null) { return false; } IsLoaded = Deserialize(MainElement); - OriginalElement = new XElement(MainElement); + OriginalElement = new XElement(MainElement).FromPackage(MainElement.ContentPackage); return IsLoaded; } - protected virtual void UpdatePath(string fullPath) + protected virtual void UpdatePath(ContentPath fullPath) { - FullPath = fullPath; + Path = fullPath; Name = GetName(); - FileName = Path.GetFileName(FullPath); - Folder = Path.GetDirectoryName(FullPath); + FileName = System.IO.Path.GetFileName(Path.Value); + Folder = System.IO.Path.GetDirectoryName(Path.Value); } public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null) @@ -98,9 +115,9 @@ namespace Barotrauma } if (fileNameWithoutExtension != null) { - UpdatePath(Path.Combine(Folder, $"{fileNameWithoutExtension}.xml")); + UpdatePath(ContentPath.FromRaw(Path.ContentPackage, System.IO.Path.Combine(Folder, $"{fileNameWithoutExtension}.xml"))); } - using (var writer = XmlWriter.Create(FullPath, settings)) + using (var writer = XmlWriter.Create(Path.Value, settings)) { Doc.WriteTo(writer); writer.Flush(); @@ -112,7 +129,7 @@ namespace Barotrauma { if (forceReload) { - return Load(FullPath); + return Load(Path); } return Deserialize(OriginalElement); } @@ -126,7 +143,7 @@ namespace Barotrauma DebugConsole.ThrowError("[Params] Not loaded!"); return; } - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true, titleFont: GUIStyle.LargeFont); if (space > 0) { new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, space), editor.EditorBox.Content.RectTransform), style: null, color: ParamsEditor.Color) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 3d2c05297..c25fa3367 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -14,13 +14,13 @@ namespace Barotrauma { class HumanRagdollParams : RagdollParams { - public static HumanRagdollParams GetRagdollParams(string speciesName, string fileName = null) => GetRagdollParams(speciesName, fileName); - public static HumanRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); + public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams(speciesName, fileName); + public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); } class FishRagdollParams : RagdollParams { - public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams(speciesName); + public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams(speciesName); } class RagdollParams : EditableParams, IMemorizable @@ -29,15 +29,15 @@ namespace Barotrauma public const float MIN_SCALE = 0.1f; public const float MAX_SCALE = 2; - public string SpeciesName { get; private set; } + public Identifier SpeciesName { get; private set; } - [Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] + [Serialize("", IsPropertySaveable.Yes, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] public string Texture { get; set; } - - [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } - - [Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] + + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] public float SpritesheetOrientation { get; set; } public bool IsSpritesheetOrientationHorizontal @@ -51,85 +51,85 @@ namespace Barotrauma } private float limbScale; - [Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } private float jointScale; - [Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } // Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize? - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float TextureScale { get; set; } - [Serialize(45f, true, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)] + [Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)] public float ColliderHeightFromFloor { get; set; } - [Serialize(50f, true, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float ImpactTolerance { get; set; } - [Serialize(true, true, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] + [Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] public bool CanEnterSubmarine { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanWalk { get; set; } - [Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()] + [Serialize(true, IsPropertySaveable.Yes, description: "Can the character be dragged around by other creatures?"), Editable()] public bool Draggable { get; set; } - [Serialize(LimbType.Torso, true), Editable] + [Serialize(LimbType.Torso, IsPropertySaveable.Yes), Editable] public LimbType MainLimb { get; set; } - private readonly static Dictionary> allRagdolls = new Dictionary>(); + /// + /// key1: Species name + /// key2: File path + /// value: Ragdoll parameters + /// + private readonly static Dictionary> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); public List Limbs { get; private set; } = new List(); public List Joints { get; private set; } = new List(); protected IEnumerable GetAllSubParams() => - Colliders.Select(c => c as SubParam) - .Concat(Limbs.Select(j => j as SubParam) - .Concat(Joints.Select(j => j as SubParam))); + Colliders + .Concat(Limbs) + .Concat(Joints); - public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll"; - public static string GetDefaultFile(string speciesName, ContentPackage contentPackage = null) - => Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml"); + public static string GetDefaultFileName(Identifier speciesName) => $"{speciesName.Value.CapitaliseFirstInvariant()}DefaultRagdoll"; + public static string GetDefaultFile(Identifier speciesName, ContentPackage contentPackage = null) + => IO.Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName)}.xml"); - public static string GetFolder(string speciesName, ContentPackage contentPackage = null) + public static string GetFolder(Identifier speciesName, ContentPackage contentPackage = null) { - CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage)); - if (prefab?.XDocument == null) + CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage)); + if (prefab?.ConfigElement == null) { DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})"); return string.Empty; } - return GetFolder(prefab.XDocument, prefab.FilePath); + return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value); } - public static string GetFolder(XDocument doc, string filePath) + private static string GetFolder(ContentXElement root, string filePath) { - var root = doc.Root; - if (root?.IsOverride() ?? false) + var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value; + if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { - root = root.FirstElement(); - } - var folder = root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) - { - folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar; + folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar; } return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } - public static T GetDefaultRagdollParams(string speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); + public static T GetDefaultRagdollParams(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); /// /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! /// If a custom folder is used, it's defined in the character info file. /// - public static T GetRagdollParams(string speciesName, string fileName = null) where T : RagdollParams, new() + public static T GetRagdollParams(Identifier speciesName, string fileName = null) where T : RagdollParams, new() { - if (string.IsNullOrWhiteSpace(speciesName)) + if (speciesName.IsEmpty) { throw new Exception($"Species name null or empty!"); } @@ -138,66 +138,88 @@ namespace Barotrauma ragdolls = new Dictionary(); allRagdolls.Add(speciesName, ragdolls); } - if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) + + if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll)) { - string selectedFile = null; - string folder = GetFolder(speciesName); - if (Directory.Exists(folder)) + return (T)ragdoll; + } + + string selectedFile = null; + + void tryFolderForSpecies(Identifier species, out string err) + { + err = null; + string folder = GetFolder(species); + if (!Directory.Exists(folder)) { - List files = Directory.GetFiles(folder).ToList(); - if (files.None()) - { - DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - else if (string.IsNullOrEmpty(fileName)) - { - // Files found, but none specified - selectedFile = GetDefaultFile(speciesName); - } - else - { - selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); - if (selectedFile == null) - { - DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - } + 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); } else { - DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll."); - selectedFile = GetDefaultFile(speciesName); - } - if (selectedFile == null) - { - throw new Exception("[RagdollParams] Selected file null!"); - } - DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}."); - T r = new T(); - if (r.Load(selectedFile, speciesName)) - { - if (!ragdolls.ContainsKey(r.Name)) + selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); + if (selectedFile == null) { - ragdolls.Add(r.Name, r); + err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."; + selectedFile = GetDefaultFile(species); } - return r; - } - else - { - // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early. - throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); } } - return (T)ragdoll; + + 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!"); + } + 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 (!ragdolls.ContainsKey(r.Name)) + { + ragdolls.Add(r.Name, r); + } + return r; + } + else + { + // Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early. + throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}."); + } } /// /// Creates a default ragdoll for the species using a predefined configuration. /// Note: Use only to create ragdolls for new characters, because this overrides the old ragdoll! /// - public static T CreateDefault(string fullPath, string speciesName, XElement mainElement) where T : RagdollParams, new() + public static T CreateDefault(string fullPath, Identifier speciesName, XElement mainElement) where T : RagdollParams, new() { // Remove the old ragdolls, if found. if (allRagdolls.ContainsKey(speciesName)) @@ -211,10 +233,12 @@ namespace Barotrauma { doc = new XDocument(mainElement) }; - instance.UpdatePath(fullPath); + var characterPrefab = CharacterPrefab.Prefabs[speciesName]; + var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath); + instance.UpdatePath(contentPath); instance.IsLoaded = instance.Deserialize(mainElement); instance.Save(); - instance.Load(fullPath, speciesName); + instance.Load(contentPath, speciesName); ragdolls.Add(instance.Name, instance); DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite); return instance as T; @@ -222,7 +246,7 @@ namespace Barotrauma public static void ClearCache() => allRagdolls.Clear(); - protected override void UpdatePath(string fullPath) + protected override void UpdatePath(ContentPath fullPath) { if (SpeciesName == null) { @@ -259,7 +283,7 @@ namespace Barotrauma }); } - protected bool Load(string file, string speciesName) + protected bool Load(ContentPath file, Identifier speciesName) { if (Load(file)) { @@ -287,7 +311,7 @@ namespace Barotrauma { if (forceReload) { - return Load(FullPath, SpeciesName); + return Load(Path, SpeciesName); } // Don't use recursion, because the reset method might be overriden Deserialize(OriginalElement, alsoChildren: false, recursive: false); @@ -401,8 +425,10 @@ namespace Barotrauma } var copy = new RagdollParams { + SpeciesName = SpeciesName, IsLoaded = true, - doc = new XDocument(doc) + doc = new XDocument(doc), + Path = Path }; copy.CreateColliders(); copy.CreateLimbs(); @@ -453,7 +479,7 @@ namespace Barotrauma public class JointParams : SubParam { private string name; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public override string Name { get @@ -472,61 +498,61 @@ namespace Barotrauma public override string GenerateName() => $"Joint {Limb1} - {Limb2}"; - [Serialize(-1, true), Editable] + [Serialize(-1, IsPropertySaveable.Yes), Editable] public int Limb1 { get; set; } - [Serialize(-1, true), Editable] + [Serialize(-1, IsPropertySaveable.Yes), Editable] public int Limb2 { get; set; } /// /// Should be converted to sim units. /// - [Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb1."), Editable()] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "Local position of the joint in the Limb1."), Editable()] public Vector2 Limb1Anchor { get; set; } /// /// Should be converted to sim units. /// - [Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb2."), Editable()] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "Local position of the joint in the Limb2."), Editable()] public Vector2 Limb2Anchor { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool CanBeSevered { get; set; } - [Serialize(0f, true, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] public float SeveranceProbabilityModifier { get; set; } - [Serialize("gore", true), Editable] + [Serialize("gore", IsPropertySaveable.Yes), Editable] public string BreakSound { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool LimitEnabled { get; set; } /// /// In degrees. /// - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float UpperLimit { get; set; } /// /// In degrees. /// - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float LowerLimit { get; set; } - [Serialize(0.25f, true), Editable] + [Serialize(0.25f, IsPropertySaveable.Yes), Editable] public float Stiffness { get; set; } - [Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] + [Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable] public float Scale { get; set; } - [Serialize(false, false), Editable(ReadOnly = true)] + [Serialize(false, IsPropertySaveable.No), Editable(ReadOnly = true)] public bool WeldJoint { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool ClockWiseRotation { get; set; } - public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public JointParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } public class LimbParams : SubParam @@ -542,7 +568,7 @@ namespace Barotrauma public List DamageModifiers { get; private set; } = new List(); private string name; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public override string Name { get @@ -563,10 +589,10 @@ namespace Barotrauma public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams; - [Serialize(-1, true), Editable(ReadOnly = true)] + [Serialize(-1, IsPropertySaveable.Yes), Editable(ReadOnly = true)] public int ID { get; set; } - [Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()] public LimbType Type { get; set; } /// @@ -576,136 +602,136 @@ namespace Barotrauma public float GetSpriteOrientationInDegrees() => float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string Notes { get; set; } - [Serialize(1f, true), Editable(DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float Scale { get; set; } - [Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()] + [Serialize(true, IsPropertySaveable.Yes, description: "Does the limb flip when the character flips?"), Editable()] public bool Flip { get; set; } - [Serialize(false, true, description: "Currently only works with non-deformable (normal) sprites."), Editable()] + [Serialize(false, IsPropertySaveable.Yes, description: "Currently only works with non-deformable (normal) sprites."), Editable()] public bool MirrorVertically { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool MirrorHorizontally { get; set; } - [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] + [Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + [Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] public float SpriteOrientation { get; set; } - [Serialize(LimbType.None, true, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")] + [Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")] public LimbType InheritLimbDepth { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float SteerForce { get; set; } - [Serialize(0f, true, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Radius of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Radius { get; set; } - [Serialize(0f, true, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Height of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Height { get; set; } - [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(10f, IsPropertySaveable.Yes, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] public float Density { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool IgnoreCollisions { get; set; } - [Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable] + [Serialize(7f, IsPropertySaveable.Yes, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable] public float AngularDamping { get; set; } - [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] + [Serialize(1f, IsPropertySaveable.Yes, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] public float AttackPriority { get; set; } - [Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()] + [Serialize("0, 0", IsPropertySaveable.Yes, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()] public Vector2 PullPos { get; set; } - [Serialize("0, 0", true, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()] + [Serialize("0, 0", IsPropertySaveable.Yes, description: "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint."), Editable()] public Vector2 StepOffset { get; set; } - [Serialize(-1, true, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()] + [Serialize(-1, IsPropertySaveable.Yes, description: "The id of the refecence joint. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter. Only applicable if this limb is a foot."), Editable()] public int RefJoint { get; set; } - [Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)] + [Serialize("0, 0", IsPropertySaveable.Yes, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)] public Vector2 MouthPos { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float ConstantTorque { get; set; } - [Serialize(0f, true), Editable] + [Serialize(0f, IsPropertySaveable.Yes), Editable] public float ConstantAngle { get; set; } - [Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] public float AttackForceMultiplier { get; set; } - [Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(1f, IsPropertySaveable.Yes, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] public float MinSeveranceDamage { get; set; } - [Serialize(true, true, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable] + [Serialize(true, IsPropertySaveable.Yes, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable] public bool CanBeSeveredAlive { get; set; } //how long it takes for severed limbs to fade out - [Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] + [Serialize(10f, IsPropertySaveable.Yes, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)] public float SeveredFadeOutTime { get; set; } = 10.0f; - [Serialize(false, true, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable] public bool ApplyTailAngle { get; set; } - [Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)] public float SineFrequencyMultiplier { get; set; } - [Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)] public float SineAmplitudeMultiplier { get; set; } - [Serialize(0f, true), Editable(0, 100, ValueStep = 1, DecimalCount = 1)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0, 100, ValueStep = 1, DecimalCount = 1)] public float BlinkFrequency { get; set; } - [Serialize(0.2f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] + [Serialize(0.2f, IsPropertySaveable.Yes), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] public float BlinkDurationIn { get; set; } - [Serialize(0.5f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)] public float BlinkDurationOut { get; set; } - [Serialize(0f, true), Editable(0, 10, ValueStep = 1, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(0, 10, ValueStep = 1, DecimalCount = 2)] public float BlinkHoldTime { get; set; } - [Serialize(0f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] public float BlinkRotationIn { get; set; } - [Serialize(45f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] + [Serialize(45f, IsPropertySaveable.Yes), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)] public float BlinkRotationOut { get; set; } - [Serialize(50f, true), Editable] + [Serialize(50f, IsPropertySaveable.Yes), Editable] public float BlinkForce { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool OnlyBlinkInWater { get; set; } - [Serialize(TransitionMode.Linear, true), Editable] + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionIn { get; private set; } - [Serialize(TransitionMode.Linear, true), Editable] + [Serialize(TransitionMode.Linear, IsPropertySaveable.Yes), Editable] public TransitionMode BlinkTransitionOut { get; private set; } // Non-editable -> // TODO: make read-only - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int HealthIndex { get; set; } - [Serialize(0.3f, true)] + [Serialize(0.3f, IsPropertySaveable.Yes)] public float Friction { get; set; } - [Serialize(0.05f, true)] + [Serialize(0.05f, IsPropertySaveable.Yes)] public float Restitution { get; set; } - public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { var spriteElement = element.GetChildElement("sprite"); if (spriteElement != null) @@ -761,7 +787,7 @@ namespace Barotrauma public bool AddAttack() { if (Attack != null) { return false; } - TryAddSubParam(new XElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack); + TryAddSubParam(CreateElement("attack"), (e, c) => new AttackParams(e, c), out AttackParams newAttack); Attack = newAttack; return Attack != null; } @@ -770,7 +796,7 @@ namespace Barotrauma public bool AddSound() { if (Sound != null) { return false; } - TryAddSubParam(new XElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound); + TryAddSubParam(CreateElement("sound"), (e, c) => new SoundParams(e, c), out SoundParams newSound); Sound = newSound; return Sound != null; } @@ -778,14 +804,14 @@ namespace Barotrauma public bool AddLight() { if (LightSource != null) { return false; } - var lightSourceElement = new XElement("lightsource", + var lightSourceElement = CreateElement("lightsource", new XElement("lighttexture", new XAttribute("texture", "Content/Lights/pointlight_bright.png"))); TryAddSubParam(lightSourceElement, (e, c) => new LightSourceParams(e, c), out LightSourceParams newLightSource); LightSource = newLightSource; return LightSource != null; } - public bool AddDamageModifier() => TryAddSubParam(new XElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers); + public bool AddDamageModifier() => TryAddSubParam(CreateElement("damagemodifier"), (e, c) => new DamageModifierParams(e, c), out _, DamageModifiers); public bool RemoveAttack() { @@ -819,7 +845,7 @@ namespace Barotrauma public bool RemoveDamageModifier(DamageModifierParams damageModifier) => RemoveSubParam(damageModifier, DamageModifiers); - protected bool TryAddSubParam(XElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam + protected bool TryAddSubParam(ContentXElement element, Func constructor, out T subParam, IList collection = null, Func, bool> filter = null) where T : SubParam { subParam = constructor(element, Ragdoll); if (collection != null && filter != null) @@ -846,7 +872,7 @@ namespace Barotrauma public class DecorativeSpriteParams : SpriteParams { - public DecorativeSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DecorativeSpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { #if CLIENT DecorativeSprite = new DecorativeSprite(element); @@ -882,7 +908,7 @@ namespace Barotrauma { public DeformationParams Deformation { get; private set; } - public DeformSpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DeformSpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { Deformation = new DeformationParams(element, ragdoll); SubParams.Add(Deformation); @@ -891,40 +917,40 @@ namespace Barotrauma public class SpriteParams : SubParam { - [Serialize("0, 0, 0, 0", true), Editable] + [Serialize("0, 0, 0, 0", IsPropertySaveable.Yes), Editable] public Rectangle SourceRect { get; set; } - [Serialize("0.5, 0.5", true, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)] + [Serialize("0.5, 0.5", IsPropertySaveable.Yes, description: "The origin of the sprite relative to the collider."), Editable(DecimalCount = 3)] public Vector2 Origin { get; set; } - [Serialize(0f, true, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)] + [Serialize(0f, IsPropertySaveable.Yes, description: "The Z-depth of the limb relative to other limbs of the same character. 1 is front, 0 is behind."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 3)] public float Depth { get; set; } - [Serialize("", true), Editable()] + [Serialize("", IsPropertySaveable.Yes), Editable()] public string Texture { get; set; } - [Serialize(false, true), Editable()] + [Serialize(false, IsPropertySaveable.Yes), Editable()] public bool IgnoreTint { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()] public Color Color { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Target color when the character is dead."), Editable()] public Color DeadColor { get; set; } - [Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(0f, IsPropertySaveable.Yes, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)] public float DeadColorTime { get; set; } public override string Name => "Sprite"; - public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public SpriteParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } public string GetTexturePath() => string.IsNullOrWhiteSpace(Texture) ? Ragdoll.Texture : Texture; } public class DeformationParams : SubParam { - public DeformationParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DeformationParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { #if CLIENT Deformations = new Dictionary(); @@ -991,7 +1017,7 @@ namespace Barotrauma public class ColliderParams : SubParam { private string name; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public override string Name { get @@ -1008,16 +1034,16 @@ namespace Barotrauma } } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Radius { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Height { get; set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) + public ColliderParams(ContentXElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll) { Name = name; } @@ -1029,16 +1055,16 @@ namespace Barotrauma { public override string Name => "Light Texture"; - [Serialize("Content/Lights/pointlight_bright.png", true), Editable] + [Serialize("Content/Lights/pointlight_bright.png", IsPropertySaveable.Yes), Editable] public string Texture { get; private set; } - [Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)] + [Serialize("0.5, 0.5", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 Origin { get; set; } - [Serialize("1.0, 1.0", true), Editable(DecimalCount = 2)] + [Serialize("1.0, 1.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public Vector2 Size { get; set; } - public LightTexture(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public LightTexture(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } public LightTexture Texture { get; private set; } @@ -1047,7 +1073,7 @@ namespace Barotrauma public Lights.LightSourceParams LightSource { get; private set; } #endif - public LightSourceParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public LightSourceParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { #if CLIENT LightSource = new Lights.LightSourceParams(element); @@ -1088,9 +1114,10 @@ namespace Barotrauma { public Attack Attack { get; private set; } - public AttackParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public AttackParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { - Attack = new Attack(element, ragdoll.SpeciesName); + var prefab = CharacterPrefab.Prefabs[ragdoll.SpeciesName]; + Attack = new Attack(element, ragdoll.SpeciesName.Value); } public override bool Deserialize(XElement element = null, bool recursive = true) @@ -1117,7 +1144,7 @@ namespace Barotrauma public bool AddNewAffliction() { Serialize(); - var subElement = new XElement("affliction", + var subElement = CreateElement("affliction", new XAttribute("identifier", "internaldamage"), new XAttribute("strength", 0f), new XAttribute("probability", 1.0f)); @@ -1140,9 +1167,9 @@ namespace Barotrauma { public DamageModifier DamageModifier { get; private set; } - public DamageModifierParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) + public DamageModifierParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { - DamageModifier = new DamageModifier(element, ragdoll.SpeciesName); + DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value); } public override bool Deserialize(XElement element = null, bool recursive = true) @@ -1170,24 +1197,27 @@ namespace Barotrauma { public override string Name => "Sound"; - [Serialize("", true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string Tag { get; private set; } - public SoundParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } + public SoundParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { } } public abstract class SubParam : ISerializableEntity { public virtual string Name { get; set; } - public Dictionary SerializableProperties { get; private set; } - public XElement Element { get; set; } + public Dictionary SerializableProperties { get; private set; } + public ContentXElement Element { get; set; } public XElement OriginalElement { get; protected set; } public List SubParams { get; set; } = new List(); public RagdollParams Ragdoll { get; private set; } public virtual string GenerateName() => Element.Name.ToString(); - public SubParam(XElement element, RagdollParams ragdoll) + protected ContentXElement CreateElement(string name, params object[] attrs) + => new XElement(name, attrs).FromPackage(Element.ContentPackage); + + public SubParam(ContentXElement element, RagdollParams ragdoll) { Element = element; OriginalElement = new XElement(element); @@ -1226,7 +1256,7 @@ namespace Barotrauma public virtual void Reset() { // Don't use recursion, because the reset method might be overriden - Deserialize(OriginalElement, false); + Deserialize(OriginalElement, recursive: false); SubParams.ForEach(sp => sp.Reset()); } @@ -1235,21 +1265,21 @@ namespace Barotrauma public Dictionary AfflictionEditors { get; private set; } public virtual void AddToEditor(ParamsEditor editor, bool recursive = true, int space = 0) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); if (this is DecorativeSpriteParams decSpriteParams) { - new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUI.LargeFont); + new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, decSpriteParams.DecorativeSprite, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } else if (this is DeformSpriteParams deformSpriteParams) { foreach (var deformation in deformSpriteParams.Deformation.Deformations.Keys) { - new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUI.LargeFont); + new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, deformation, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } } else if (this is AttackParams attackParams) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, attackParams.Attack, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); if (AfflictionEditors == null) { AfflictionEditors = new Dictionary(); @@ -1267,11 +1297,11 @@ namespace Barotrauma } else if (this is LightSourceParams lightParams) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, lightParams.LightSource, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } else if (this is DamageModifierParams damageModifierParams) { - SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUI.LargeFont); + SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, damageModifierParams.DamageModifier, inGame: false, showName: true, titleFont: GUIStyle.LargeFont); } if (recursive) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index f023b51f7..579ef14a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -5,20 +5,17 @@ using System.Xml.Linq; namespace Barotrauma { - class SkillSettings : ISerializableEntity + class SkillSettings : Prefab, ISerializableEntity { - public static SkillSettings Current - { - get; - private set; - } + public readonly static PrefabSelector Prefabs = new PrefabSelector(); + public static SkillSettings Current => Prefabs.ActivePrefab; - [Serialize(4.0f, true)] + [Serialize(4.0f, IsPropertySaveable.Yes)] public float SingleRoundSkillGainMultiplier { get; set; } private float skillIncreasePerRepair; - [Serialize(5.0f, true)] + [Serialize(5.0f, IsPropertySaveable.Yes)] public float SkillIncreasePerRepair { get { return skillIncreasePerRepair * GetCurrentSkillGainMultiplier(); } @@ -26,7 +23,7 @@ namespace Barotrauma } private float skillIncreasePerSabotage; - [Serialize(3.0f, true)] + [Serialize(3.0f, IsPropertySaveable.Yes)] public float SkillIncreasePerSabotage { get { return skillIncreasePerSabotage * GetCurrentSkillGainMultiplier(); } @@ -34,7 +31,7 @@ namespace Barotrauma } private float skillIncreasePerCprRevive; - [Serialize(0.5f, true)] + [Serialize(0.5f, IsPropertySaveable.Yes)] public float SkillIncreasePerCprRevive { get { return skillIncreasePerCprRevive * GetCurrentSkillGainMultiplier(); } @@ -42,7 +39,7 @@ namespace Barotrauma } private float skillIncreasePerRepairedStructureDamage; - [Serialize(0.0025f, true)] + [Serialize(0.0025f, IsPropertySaveable.Yes)] public float SkillIncreasePerRepairedStructureDamage { get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); } @@ -50,7 +47,7 @@ namespace Barotrauma } private float skillIncreasePerSecondWhenSteering; - [Serialize(0.005f, true)] + [Serialize(0.005f, IsPropertySaveable.Yes)] public float SkillIncreasePerSecondWhenSteering { get { return skillIncreasePerSecondWhenSteering * GetCurrentSkillGainMultiplier(); } @@ -58,7 +55,7 @@ namespace Barotrauma } private float skillIncreasePerFabricatorRequiredSkill; - [Serialize(0.5f, true)] + [Serialize(0.5f, IsPropertySaveable.Yes)] public float SkillIncreasePerFabricatorRequiredSkill { get { return skillIncreasePerFabricatorRequiredSkill * GetCurrentSkillGainMultiplier(); } @@ -66,7 +63,7 @@ namespace Barotrauma } private float skillIncreasePerHostileDamage; - [Serialize(0.01f, true)] + [Serialize(0.01f, IsPropertySaveable.Yes)] public float SkillIncreasePerHostileDamage { get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); } @@ -74,7 +71,7 @@ namespace Barotrauma } private float skillIncreasePerSecondWhenOperatingTurret; - [Serialize(0.001f, true)] + [Serialize(0.001f, IsPropertySaveable.Yes)] public float SkillIncreasePerSecondWhenOperatingTurret { get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); } @@ -82,64 +79,40 @@ namespace Barotrauma } private float skillIncreasePerFriendlyHealed; - [Serialize(0.001f, true)] + [Serialize(0.001f, IsPropertySaveable.Yes)] public float SkillIncreasePerFriendlyHealed { get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); } set { skillIncreasePerFriendlyHealed = value; } } - [Serialize(1.1f, true)] + [Serialize(1.1f, IsPropertySaveable.Yes)] public float AssistantSkillIncreaseMultiplier { get; set; } - [Serialize(200.0f, true)] + [Serialize(200.0f, IsPropertySaveable.Yes)] public float MaximumSkillWithTalents { get; set; } - private SkillSettings(XElement element) + public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier()) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); } public string Name => "SkillSettings"; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; } - public static void Load(IEnumerable files) - { - //reverse order to respect content package load order (last file overrides others) - foreach (ContentFile file in files.Reverse()) - { - if (file.Type != ContentType.SkillSettings) - { - throw new ArgumentException(); - } - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - - Current = new SkillSettings(doc.Root); - break; - } - - if (Current == null) - { - DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values."); - Current = new SkillSettings(null); - } - } - private float GetCurrentSkillGainMultiplier() { if (GameMain.GameSession?.GameMode is CampaignMode) @@ -151,5 +124,7 @@ namespace Barotrauma return SingleRoundSkillGainMultiplier; } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index 959cf4148..1b3564d0f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Abilities public virtual bool AllowClientSimulation => true; - public AbilityCondition(CharacterTalent characterTalent, XElement conditionElement) + public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement) { this.characterTalent = characterTalent; character = characterTalent.Character; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs index 4d909aa81..ebd077561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities class AbilityConditionAffliction : AbilityConditionData { private readonly string[] afflictions; - public AbilityConditionAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index b45a81b25..950684465 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -1,5 +1,5 @@ -using Barotrauma.Items.Components; using System; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -25,10 +25,10 @@ namespace Barotrauma.Abilities private readonly string[] tags; private readonly WeaponType weapontype; private readonly bool ignoreNonHarmfulAttacks; - public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty); - tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false); string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any"); @@ -54,7 +54,7 @@ namespace Barotrauma.Abilities if (!string.IsNullOrEmpty(itemIdentifier)) { - if (item?.prefab.Identifier != itemIdentifier) + if (item?.Prefab.Identifier != itemIdentifier) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs index 58616eac5..b0a9864ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using System; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -8,11 +9,11 @@ namespace Barotrauma.Abilities class AbilityConditionAttackResult : AbilityConditionData { private readonly List targetTypes; - private readonly string[] afflictions; - public AbilityConditionAttackResult(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + private readonly Identifier[] afflictions; + public AbilityConditionAttackResult(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); - afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty())); + afflictions = conditionElement.GetAttributeIdentifierArray("afflictions", Array.Empty()); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index cc14b466a..43a16839d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -10,9 +10,9 @@ namespace Barotrauma.Abilities private List conditionals = new List(); - public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true)); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)); foreach (XElement subElement in conditionElement.Elements()) { 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 0d25f107e..7065cb683 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Abilities /// /// These conditions will return an error if used outside their limited intended use. /// - public AbilityConditionData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs index 2e1204fba..12538c312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionEvasiveManeuvers.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Abilities { class AbilityConditionEvasiveManeuvers : AbilityConditionData { - public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionEvasiveManeuvers(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs index 6ea6dd5e9..b72555ccf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionGeneHarvester.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities class AbilityConditionGeneHarvester : AbilityConditionData { - public AbilityConditionGeneHarvester(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionGeneHarvester(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs index 26a04a1a7..957779bb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities private readonly bool hittingCountsAsAiming; private readonly WeaponType weapontype; - public AbilityConditionIsAiming(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionIsAiming(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { hittingCountsAsAiming = conditionElement.GetAttributeBool("hittingcountsasaiming", false); switch (conditionElement.GetAttributeString("weapontype", "")) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index d4eb985d2..5811b3d66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities private readonly string[] identifiers; private readonly string[] tags; - public AbilityConditionItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty(), convertToLowerInvariant: true); tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs index 330bcfcd2..9bbb48bf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly SubmarineType? submarineType; - public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { if (conditionElement.Attribute("submarinetype") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index b2d70b0b3..59c8254b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -6,15 +7,15 @@ namespace Barotrauma.Abilities class AbilityConditionLocation : AbilityConditionData { private readonly bool? hasOutpost; - private readonly string[] locationIdentifiers; + private readonly Identifier[] locationIdentifiers; - public AbilityConditionLocation(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { if (conditionElement.Attribute("hasoutpost") != null) { hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); } - locationIdentifiers = conditionElement.GetAttributeStringArray("locationtype", new string[0]); + locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty()); } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index f7f0ffed4..0e19ec19e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class AbilityConditionMission : AbilityConditionData { private readonly MissionType missionType; - public AbilityConditionMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { string missionTypeString = conditionElement.GetAttributeString("missiontype", "None"); if (!Enum.TryParse(missionTypeString, out missionType)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs index 1068da08b..d3ece3bf8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -1,4 +1,5 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -7,9 +8,9 @@ namespace Barotrauma.Abilities private readonly string[] allowedTypes; private readonly string identifier; - public AbilityConditionReduceAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionReduceAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", new string[0], convertToLowerInvariant: true); + allowedTypes = conditionElement.GetAttributeStringArray("allowedtypes", Array.Empty(), convertToLowerInvariant: true); identifier = conditionElement.GetAttributeString("identifier", ""); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs index 52d189213..eb4be84c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs @@ -6,19 +6,19 @@ namespace Barotrauma.Abilities { private readonly string skillIdentifier; - public AbilityConditionSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); } - private bool MatchesConditionSpecific(string skillIdentifier) + private bool MatchesConditionSpecific(Identifier skillIdentifier) { return this.skillIdentifier == skillIdentifier; } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) + if (abilityObject is IAbilitySkillIdentifier { SkillIdentifier: Identifier skillIdentifier }) { return MatchesConditionSpecific(skillIdentifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs index 6be5969f8..3effceb4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionStatusEffectIdentifier.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private string effectIdentifier; - public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionStatusEffectIdentifier(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { effectIdentifier = conditionElement.GetAttributeString("effectidentifier", "").ToLowerInvariant(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs index 9fe8fa1a4..de5597011 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly float vitalityPercentage; - public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs index 29256ab7c..55a640a17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAlliesAboveVitality.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { float vitalityPercentage; - public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionAlliesAboveVitality(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { vitalityPercentage = conditionElement.GetAttributeFloat("vitalitypercentage", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs index 7525427eb..50c650f1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private readonly string jobIdentifier; - public AbilityConditionCoauthor(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionCoauthor(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { jobIdentifier = conditionElement.GetAttributeString("jobidentifier", string.Empty); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs index cd96edb58..cc44ec6ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrouched.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities class AbilityConditionCrouched : AbilityConditionDataless { - public AbilityConditionCrouched(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionCrouched(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } 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 ad7007fd6..a1e03fcb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Abilities { abstract class AbilityConditionDataless : AbilityCondition { - public AbilityConditionDataless(CharacterTalent characterTalent, XElement conditionElement) : base (characterTalent, conditionElement) { } + public AbilityConditionDataless(CharacterTalent characterTalent, ContentXElement conditionElement) : base (characterTalent, conditionElement) { } protected abstract bool MatchesConditionSpecific(); public override bool MatchesCondition() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs index 9f449e43c..68c46cd12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities private float minimumPercentage; - public AbilityConditionHasAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasAffliction(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { afflictionIdentifier = conditionElement.GetAttributeString("afflictionidentifier", ""); minimumPercentage = conditionElement.GetAttributeFloat("minimumpercentage", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs index 0f1707d3d..fc3e186c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasDifferentJobs.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class AbilityConditionHasDifferentJobs : AbilityConditionDataless { private readonly int amount; - public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasDifferentJobs(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { amount = conditionElement.GetAttributeInt("amount", 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 8f4fc7c35..4407fcb18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using System; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -14,9 +15,9 @@ namespace Barotrauma.Abilities private List items = new List(); - public AbilityConditionHasItem(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - tags = conditionElement.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); requireAll = conditionElement.GetAttributeBool("requireall", false); //this.invSlotType = invSlotType; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 2c3b26a5c..344a580f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -5,14 +5,14 @@ namespace Barotrauma.Abilities { class AbilityConditionHasPermanentStat : AbilityConditionDataless { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float min; - public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - statIdentifier = conditionElement.GetAttributeString("statidentifier", string.Empty); - if (string.IsNullOrEmpty(statIdentifier)) + statIdentifier = conditionElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); + if (statIdentifier.IsEmpty) { DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs index 60d5da1f7..865384e7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private readonly string skillIdentifier; private readonly float minValue; - public AbilityConditionHasSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty); minValue = conditionElement.GetAttributeFloat("minvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs index 2a22f2098..1b8d54fc3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities private readonly string tag; - public AbilityConditionHasStatusTag(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasStatusTag(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { tag = conditionElement.GetAttributeString("tag", ""); if (string.IsNullOrEmpty(tag)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs index d3aa75bde..d54fd0839 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasVelocity.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private readonly float velocity; - public AbilityConditionHasVelocity(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionHasVelocity(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { velocity = conditionElement.GetAttributeFloat("velocity", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs index 28f27ed9b..4bda9d8ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionInFriendlySubmarine : AbilityConditionDataless { - public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs index e08291e6b..484e0c7bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionInHull : AbilityConditionDataless { - public AbilityConditionInHull(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionInHull(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs index d93731514..a6191d470 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionInWater : AbilityConditionDataless { - public AbilityConditionInWater(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionInWater(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs index f2c4b2fb7..46e55c575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class AbilityConditionLevelsBehindHighest : AbilityConditionDataless { private readonly int levelsBehind; - public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { levelsBehind = conditionElement.GetAttributeInt("levelsbehind", 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs index bb4390106..177cd4bf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { class AbilityConditionNoCrewDied : AbilityConditionDataless { - public AbilityConditionNoCrewDied(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionNoCrewDied(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs index dac9a3f1a..ac5941a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionOnMission.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionOnMission : AbilityConditionDataless { - public AbilityConditionOnMission(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionOnMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs index 192ea6f4f..3cc7c0eac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRagdolled.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities class AbilityConditionRagdolled : AbilityConditionDataless { - public AbilityConditionRagdolled(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionRagdolled(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs index 3186b852f..78bbe3f68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionRunning.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class AbilityConditionRunning : AbilityConditionDataless { - public AbilityConditionRunning(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionRunning(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs index 5b582f799..b774712ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly float randomChance = 0f; public override bool AllowClientSimulation => false; - public AbilityConditionServerRandom(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionServerRandom(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { randomChance = conditionElement.GetAttributeFloat("randomchance", 1f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs index 9a99f4ce8..f75f98a89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionShipFlooded.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities class AbilityConditionShipFlooded : AbilityConditionDataless { private readonly float floodPercentage; - public AbilityConditionShipFlooded(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + public AbilityConditionShipFlooded(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { floodPercentage = conditionElement.GetAttributeFloat("floodpercentage", 0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs index ef57527d6..1d6c431e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -32,7 +32,7 @@ interface IAbilitySkillIdentifier { - public string SkillIdentifier { get; set; } + public Identifier SkillIdentifier { get; set; } } interface IAbilityAffliction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 6d7038f4c..9d1e093b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -15,5 +15,5 @@ namespace Barotrauma.Abilities } public Character Character { get; set; } } - + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 27d856553..4c29a5b61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Abilities /// protected float EffectDeltaTime => CharacterAbilityGroup is CharacterAbilityGroupInterval abilityGroupInterval ? abilityGroupInterval.TimeSinceLastUpdate : DefaultEffectTime; - public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) + public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) { CharacterAbilityGroup = characterAbilityGroup; CharacterTalent = characterAbilityGroup.CharacterTalent; @@ -93,7 +93,7 @@ namespace Barotrauma.Abilities } // XML - public static CharacterAbility Load(XElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true) + public static CharacterAbility Load(ContentXElement abilityElement, CharacterAbilityGroup characterAbilityGroup, bool errorMessages = true) { Type abilityType; string type = abilityElement.Name.ToString().ToLowerInvariant(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs index 7dde00097..e934eb2aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyForce.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Abilities private readonly HashSet limbTypes = new HashSet(); public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { force = abilityElement.GetAttributeFloat("force", 0f); maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index c30ac8152..70ec6e1ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities readonly List targets = new List(); - public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); applyToSelf = abilityElement.GetAttributeBool("applytoself", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index a2fc33e5c..3f9090376 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private readonly bool allowSelf; private readonly float maxDistance = float.MaxValue; - public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { allowSelf = abilityElement.GetAttributeBool("allowself", true); maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs index efca07622..d6fc8b329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects { - public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs index fc4291453..4594c5e1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToLastOrderedCharacter.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects { - public CharacterAbilityApplyStatusEffectsToLastOrderedCharacter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToLastOrderedCharacter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs index a0701c782..f8329aae2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities class CharacterAbilityApplyStatusEffectsToNearestAlly : CharacterAbilityApplyStatusEffects { protected float squaredMaxDistance; - public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs index 0f1fd20b2..c49c4b266 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs @@ -1,9 +1,7 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -15,7 +13,7 @@ namespace Barotrauma.Abilities public override bool AllowClientSimulation => false; - public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); allowDifferentSub = abilityElement.GetAttributeBool("mustbeonsamesub", true); @@ -24,18 +22,33 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - Character chosenCharacter = null; + ApplyEffect(Character); + } - chosenCharacter = Character.GetFriendlyCrew(Character).Where(c => - (allowSelf || c != Character) && + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter) + { + ApplyEffect(targetCharacter); + } + else + { + ApplyEffect(Character); + } + } + + private void ApplyEffect(Character thisCharacter) + { + Character chosenCharacter = + Character.GetFriendlyCrew(thisCharacter).Where(c => + (allowSelf || c != thisCharacter) && (allowDifferentSub || c.Submarine == Character.Submarine) && - Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) is float tempDistance && - tempDistance < squaredMaxDistance).GetRandom(); - + Vector2.DistanceSquared(thisCharacter.WorldPosition, c.WorldPosition) is float tempDistance && + tempDistance < squaredMaxDistance).GetRandomUnsynced(); if (chosenCharacter == null) { return; } ApplyEffectSpecific(chosenCharacter); - } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 9f37a0e03..1204769de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -5,12 +5,12 @@ namespace Barotrauma.Abilities { class CharacterAbilityGainSimultaneousSkill : CharacterAbility { - private readonly string skillIdentifier; + private readonly Identifier skillIdentifier; private readonly bool ignoreAbilitySkillGain; - public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs index 5f56b433a..9dcab6bb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -4,19 +4,19 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveAffliction : CharacterAbility { - private readonly string afflictionId; + private readonly Identifier afflictionId; private readonly float strength; private readonly string multiplyStrengthBySkill; private readonly bool setValue; - public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - afflictionId = abilityElement.GetAttributeString("afflictionid", abilityElement.GetAttributeString("affliction", string.Empty)); + afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); strength = abilityElement.GetAttributeFloat("strength", 0f); multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty); setValue = abilityElement.GetAttributeBool("setvalue", false); - if (string.IsNullOrEmpty(afflictionId)) + if (afflictionId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set."); } @@ -26,7 +26,7 @@ namespace Barotrauma.Abilities { if (abilityObject is IAbilityCharacter character) { - var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier.Equals(afflictionId, System.StringComparison.OrdinalIgnoreCase)); + var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId); if (afflictionPrefab == null) { DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\"."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs index 76b3960ea..e56bee86c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly AbilityFlags abilityFlag; // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types - public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index 11aa9934e..40148524c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -7,25 +7,25 @@ namespace Barotrauma.Abilities public override bool AppliesEffectOnIntervalUpdate => true; private readonly int amount; - private readonly string scalingStatIdentifier; + private readonly Identifier scalingStatIdentifier; - public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); - scalingStatIdentifier = abilityElement.GetAttributeString("scalingstatidentifier", string.Empty); + scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty); } private void ApplyEffectSpecific(Character targetCharacter) { float multiplier = 1f; - if (!string.IsNullOrEmpty(scalingStatIdentifier)) + if (!scalingStatIdentifier.IsEmpty) { multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier); } int totalAmount = (int)(multiplier * amount); targetCharacter.GiveMoney(totalAmount); - GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } protected override void ApplyEffect(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index 0998bf475..a0750d5d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities public override bool AllowClientSimulation => true; public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 253dd787b..347f69a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -4,15 +4,15 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveResistance : CharacterAbility { - private readonly string resistanceId; + private readonly Identifier resistanceId; private readonly float multiplier; - public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - resistanceId = abilityElement.GetAttributeString("resistanceid", abilityElement.GetAttributeString("resistance", string.Empty)); + resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty)); multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency - if (string.IsNullOrEmpty(resistanceId)) + if (resistanceId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs index c999d3999..a97ec2ee4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly StatTypes statType; private readonly float value; - public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs index 8ba1c9ef9..1eed1afae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveTalentPoints.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private readonly int amount; - public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs index 7a53e6d91..966bac5f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityIncreaseSkill.cs @@ -8,15 +8,15 @@ namespace Barotrauma.Abilities { public override bool AppliesEffectOnIntervalUpdate => true; - private readonly string skillIdentifier; + private readonly Identifier skillIdentifier; private readonly float skillIncrease; - public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityIncreaseSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - skillIdentifier = abilityElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); skillIncrease = abilityElement.GetAttributeFloat("skillincrease", 0f); - if (string.IsNullOrEmpty(skillIdentifier)) + if (skillIdentifier.IsEmpty) { DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill."); } @@ -45,9 +45,9 @@ namespace Barotrauma.Abilities private void ApplyEffectSpecific(Character character) { - if (skillIdentifier.Equals("random")) + if (skillIdentifier == "random") { - var skill = character.Info?.Job?.Skills?.GetRandom(); + var skill = character.Info?.Job?.Skills?.GetRandomUnsynced(); if (skill == null) { return; } character.Info?.IncreaseSkillLevel(skill.Identifier, skillIncrease, gainedFromAbility: true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index e3896090c..97b0302bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities private readonly float addedMultiplier; - public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index 9b4700fe5..a6141b79f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -11,9 +11,9 @@ namespace Barotrauma.Abilities private readonly float addedPenetration; private readonly bool implode; - public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - if (abilityElement.GetChildElement("afflictions") is XElement afflictionElements) + if (abilityElement.GetChildElement("afflictions") is ContentXElement afflictionElements) { afflictions = CharacterAbilityGroup.ParseAfflictions(CharacterTalent, afflictionElements); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs index 8993f0ccd..52f47c471 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private bool lastState; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs index 010f04f2f..ed5a5a35f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -4,18 +4,18 @@ namespace Barotrauma.Abilities { class CharacterAbilityModifyResistance : CharacterAbility { - private readonly string resistanceId; + private readonly Identifier resistanceId; private readonly float resistance; bool lastState; public override bool AllowClientSimulation => true; // should probably be split to different classes - public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - resistanceId = abilityElement.GetAttributeString("resistanceid", ""); + resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", ""); resistance = abilityElement.GetAttributeFloat("resistance", 1f); - if (string.IsNullOrEmpty(resistanceId)) + if (resistanceId.IsEmpty) { DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs index 74e04a098..c7f792475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStat.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities bool lastState; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs index 3269e078a..29d1fdf3c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToFlooding.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStatToFlooding(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStatToFlooding(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs index 14ac0324c..a3141b037 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); statPerLevel = abilityElement.GetAttributeFloat("statperlevel", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs index b2a01a4c1..3311a5ff6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 59a203a7f..6970c2e6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities private readonly float addedValue; private readonly float multiplyValue; - public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs index 3d280aeeb..10c1ddcd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityPutItem.cs @@ -4,14 +4,14 @@ namespace Barotrauma.Abilities { class CharacterAbilityPutItem : CharacterAbility { - private readonly string itemIdentifier; + private readonly Identifier itemIdentifier; private readonly int amount; public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityPutItem(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - itemIdentifier = abilityElement.GetAttributeString("itemidentifier", ""); + itemIdentifier = abilityElement.GetAttributeIdentifier("itemidentifier", ""); amount = abilityElement.GetAttributeInt("amount", 1); - if (string.IsNullOrEmpty(itemIdentifier)) + if (itemIdentifier.IsEmpty) { DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined."); } @@ -19,7 +19,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - if (string.IsNullOrEmpty(itemIdentifier)) + if (itemIdentifier.IsEmpty) { DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined."); return; @@ -46,7 +46,7 @@ namespace Barotrauma.Abilities } else { - Entity.Spawner.AddToSpawnQueue(itemPrefab, Character.Inventory); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, Character.Inventory); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 3716a6fbc..1660c54e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities public override bool AppliesEffectOnIntervalUpdate => true; public override bool AllowClientSimulation => true; - public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs index 7ed61e90f..21d679bd2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { public override bool AppliesEffectOnIntervalUpdate => true; - public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs index 919848c3a..4d6647f55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Abilities private readonly float randomChance; private readonly bool oncePerContainer; - public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs index 8a88acea6..c76b7fb01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs @@ -6,32 +6,31 @@ namespace Barotrauma.Abilities { class CharacterAbilityUnlockTree : CharacterAbility { - public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } public override void InitializeAbility(bool addingFirstTime) { - if (!addingFirstTime) { return; } - if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab))); + if (subTree == null) { return; } + + subTree.ForceUnlock = true; + if (!addingFirstTime) { return; } - if (subTree != null) + foreach (var talentOption in subTree.TalentOptionStages) { - subTree.ForceUnlock = true; - foreach (var talentOption in subTree.TalentOptionStages) + foreach (var talent in talentOption.Talents) { - foreach (var talent in talentOption.Talents) + if (talent == CharacterTalent.Prefab) { continue; } + if (Character.GiveTalent(talent)) { - if (talent == CharacterTalent.Prefab) { continue; } - if (Character.GiveTalent(talent)) - { - Character.Info.AdditionalTalentPoints++; - } + Character.Info.AdditionalTalentPoints++; } } - } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs index a6907ca41..9a67d1fd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Abilities private readonly float maxAddedDamageMultiplier; private readonly string[] tags; - public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f); maxAddedDamageMultiplier = abilityElement.GetAttributeFloat("maxaddedddamagemultiplier", float.MaxValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index 2cc7a26f1..e212b3224 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Abilities { private readonly bool ignoreAbilitySkillGain; - public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs index 5ab9361b8..745cb706e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs @@ -8,14 +8,14 @@ namespace Barotrauma.Abilities { private readonly float addedValue; private readonly float multiplyValue; - private readonly string[] tags; + private readonly Identifier[] tags; private readonly int maxMultiplyCount; - - public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + + public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); - tags = abilityElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty()); maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs index 00c39c0e0..75aa86835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityBountyHunter.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Abilities { private float vitalityPercentage; - public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { vitalityPercentage = abilityElement.GetAttributeFloat("vitalitypercentage", 0f); } @@ -18,7 +18,7 @@ namespace Barotrauma.Abilities { int totalAmount = (int)(vitalityPercentage * character.MaxVitality); Character.GiveMoney(totalAmount); - GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs index 66a39efaa..c3c99c080 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private readonly int experienceAmount; private readonly int max; - public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { moneyAmount = abilityElement.GetAttributeInt("moneyamount", 0); experienceAmount = abilityElement.GetAttributeInt("experienceamount", 0); @@ -30,7 +30,7 @@ namespace Barotrauma.Abilities if (!enemyCharacter.LockHands) { continue; } if (timesGiven > max) { continue; } Character.GiveMoney(moneyAmount); - GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); foreach (Character character in Character.GetFriendlyCrew(Character)) { character.Info?.GiveExperience(experienceAmount); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs index fe4073afc..3ea78fd56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Abilities private readonly int moneyPerMission; - public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0); } @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities { int totalAmount = moneyPerMission * info.MissionsCompletedSinceDeath; Character.GiveMoney(totalAmount); - GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs index c66630989..24b2a02e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -5,15 +5,15 @@ namespace Barotrauma.Abilities { class CharacterAbilityMultitasker : CharacterAbility { - private string lastSkillIdentifier; + private Identifier lastSkillIdentifier; - public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityMultitasker(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) + if (abilityObject is IAbilitySkillIdentifier { SkillIdentifier: Identifier skillIdentifier }) { if (skillIdentifier != lastSkillIdentifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs index 0f49b2e56..c9a160923 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities private float lastValue = 0f; public override bool AllowClientSimulation => true; - public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs index 574d9d7b1..85ef28d54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Abilities // seems like a minor issue for now private readonly List openedContainers = new List(); - public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 2fbb95ec9..5bcac5e83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Abilities { // this should just be its own class, misleading to inherit here private readonly string tag; - public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { tag = abilityElement.GetAttributeString("tag", ""); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 7c96b3d17..1f3795dea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -26,13 +26,13 @@ namespace Barotrauma.Abilities // separate dictionaries for each type of characterability? protected readonly List characterAbilities = new List(); - public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) + public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { AbilityEffectType = abilityEffectType; CharacterTalent = characterTalent; Character = CharacterTalent.Character; maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue); - foreach (XElement subElement in abilityElementGroup.Elements()) + foreach (var subElement in abilityElementGroup.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -54,9 +54,9 @@ namespace Barotrauma.Abilities } } - public void LoadConditions(XElement conditionElements) + public void LoadConditions(ContentXElement conditionElements) { - foreach (XElement conditionElement in conditionElements.Elements()) + foreach (ContentXElement conditionElement in conditionElements.Elements()) { AbilityCondition newCondition = ConstructCondition(CharacterTalent, conditionElement); @@ -87,7 +87,7 @@ namespace Barotrauma.Abilities } // XML - private AbilityCondition ConstructCondition(CharacterTalent characterTalent, XElement conditionElement, bool errorMessages = true) + private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true) { AbilityCondition newCondition = null; @@ -129,15 +129,15 @@ namespace Barotrauma.Abilities return newCondition; } - private void LoadAbilities(XElement abilityElements) + private void LoadAbilities(ContentXElement abilityElements) { - foreach (XElement abilityElementGroup in abilityElements.Elements()) + foreach (var abilityElementGroup in abilityElements.Elements()) { AddAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); } } - private CharacterAbility ConstructAbility(XElement abilityElement, CharacterTalent characterTalent) + private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent) { CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); @@ -150,7 +150,7 @@ namespace Barotrauma.Abilities return newAbility; } - public static List ParseStatusEffects(CharacterTalent characterTalent, XElement statusEffectElements) + public static List ParseStatusEffects(CharacterTalent characterTalent, ContentXElement statusEffectElements) { if (statusEffectElements == null) { @@ -160,7 +160,7 @@ namespace Barotrauma.Abilities List statusEffects = new List(); - foreach (XElement statusEffectElement in statusEffectElements.Elements()) + foreach (var statusEffectElement in statusEffectElements.Elements()) { var statusEffect = StatusEffect.Load(statusEffectElement, characterTalent.DebugIdentifier); statusEffects.Add(statusEffect); @@ -178,7 +178,7 @@ namespace Barotrauma.Abilities return statType; } - public static List ParseAfflictions(CharacterTalent characterTalent, XElement afflictionElements) + public static List ParseAfflictions(CharacterTalent characterTalent, ContentXElement afflictionElements) { if (afflictionElements == null) { @@ -191,10 +191,10 @@ namespace Barotrauma.Abilities // similar logic to affliction creation in statuseffects // might be worth unifying - foreach (XElement afflictionElement in afflictionElements.Elements()) + foreach (var afflictionElement in afflictionElements.Elements()) { - string afflictionIdentifier = afflictionElement.GetAttributeString("identifier", "").ToLowerInvariant(); - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + Identifier afflictionIdentifier = afflictionElement.GetAttributeIdentifier("identifier", ""); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index e4d488103..a12e2ce1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { - public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) : + public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { } public void CheckAbilityGroup(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index c7a302149..8682a47df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities private float effectDelayTimer; - public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, XElement abilityElementGroup) : + public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { // too many overlapping intervals could cause hitching? maybe randomize a little diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 3a79e1b2a..bfa5b6869 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -20,17 +20,17 @@ namespace Barotrauma private readonly List characterAbilityGroupIntervals = new List(); // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well - public List UnlockedRecipes { get; } = new List(); + public List UnlockedRecipes { get; } = new List(); public CharacterTalent(TalentPrefab talentPrefab, Character character) { Character = character; Prefab = talentPrefab; - XElement element = talentPrefab.ConfigElement; + var element = talentPrefab.ConfigElement; DebugIdentifier = talentPrefab.OriginalName; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -41,7 +41,7 @@ namespace Barotrauma LoadAbilityGroupInterval(subElement); break; case "addedrecipe": - if (subElement.GetAttributeString("itemidentifier", string.Empty) is string recipeIdentifier && recipeIdentifier != string.Empty) + if (subElement.GetAttributeIdentifier("itemidentifier", Identifier.Empty) is { IsEmpty: false } recipeIdentifier) { UnlockedRecipes.Add(recipeIdentifier); } @@ -85,12 +85,12 @@ namespace Barotrauma } // XML logic - private void LoadAbilityGroupInterval(XElement abilityGroup) + private void LoadAbilityGroupInterval(ContentXElement abilityGroup) { characterAbilityGroupIntervals.Add(new CharacterAbilityGroupInterval(AbilityEffectType.Undefined, this, abilityGroup)); } - private void LoadAbilityGroupEffect(XElement abilityGroup) + private void LoadAbilityGroupEffect(ContentXElement abilityGroup) { AbilityEffectType abilityEffectType = ParseAbilityEffectType(this, abilityGroup.GetAttributeString("abilityeffecttype", "none")); AddAbilityGroupEffect(new CharacterAbilityGroupEffect(abilityEffectType, this, abilityGroup), abilityEffectType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 492b857d9..ad164626d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -4,35 +4,33 @@ using System.Xml.Linq; namespace Barotrauma { - class TalentPrefab : IPrefab, IDisposable, IHasUintIdentifier + class TalentPrefab : PrefabWithUintIdentifier { - public string Identifier { get; private set; } - public string OriginalName => Identifier; - public ContentPackage ContentPackage { get; private set; } - public string FilePath { get; private set; } + public string OriginalName => Identifier.Value; - public string DisplayName { get; private set; } + public LocalizedString DisplayName { get; private set; } - public string Description { get; private set; } + public LocalizedString Description { get; private set; } public readonly Sprite Icon; public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); - public XElement ConfigElement + public ContentXElement ConfigElement { get; private set; } - public TalentPrefab(XElement element, string filePath) + public TalentPrefab(ContentXElement element, TalentsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { - FilePath = filePath; ConfigElement = element; - Identifier = element.GetAttributeString("identifier", "noidentifier"); - DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier; - foreach (XElement subElement in element.Elements()) + DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); + + Description = ""; + + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -40,112 +38,29 @@ namespace Barotrauma Icon = new Sprite(subElement); break; case "description": - string tempDescription = Description; + var tempDescription = Description; TextManager.ConstructDescription(ref tempDescription, subElement); Description = tempDescription; break; } } - if (string.IsNullOrEmpty(Description)) + if (element.Attribute("description") != null) { - if (element.Attribute("description") != null) - { - string description = element.GetAttributeString("description", string.Empty); - Description = TextManager.Get(description, returnNull: true) ?? description; - } - else - { - Description = TextManager.Get("talentdescription." + Identifier, returnNull: true) ?? string.Empty; - } + string description = element.GetAttributeString("description", string.Empty); + Description = Description.Fallback(TextManager.Get(description)).Fallback(description); } - -#if DEBUG - if (!TextManager.ContainsTag("talentname." + Identifier)) + else { - DebugConsole.AddWarning($"Name for the talent \"{Identifier}\" not found in the text files."); + Description = Description.Fallback(TextManager.Get($"talentdescription.{Identifier}")).Fallback(string.Empty); } - if (string.IsNullOrEmpty(Description)) - { - DebugConsole.AddWarning($"Description for the talent \"{Identifier}\" not configured"); - } - if (Description.Contains('[')) - { - DebugConsole.ThrowError($"Description for the talent \"{Identifier}\" contains brackets - was some variable not replaced correctly? ({Description})"); - } -#endif } private bool disposed = false; - public void Dispose() + public override void Dispose() { if (disposed) { return; } disposed = true; - TalentPrefabs.Remove(this); - } - - /// - /// Unique identifier that's generated by hashing the prefab's string identifier. - /// Used to reduce the amount of bytes needed to write talent data into network messages in multiplayer. - /// - public uint UIntIdentifier { get; set; } - - public static void RemoveByFile(string filePath) => TalentPrefabs.RemoveByFile(filePath); - - public static void LoadFromFile(ContentFile file) - { - DebugConsole.Log("Loading talent prefab: " + file.Path); - RemoveByFile(file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - void loadSinglePrefab(XElement element, bool isOverride) - { - var newPrefab = new TalentPrefab(element, file.Path) { ContentPackage = file.ContentPackage }; - TalentPrefabs.Add(newPrefab, isOverride); - newPrefab.CalculatePrefabUIntIdentifier(TalentPrefabs); - } - - void loadMultiplePrefabs(XElement element, bool isOverride) - { - foreach (var subElement in element.Elements()) - { - interpretElement(subElement, isOverride); - } - } - - void interpretElement(XElement subElement, bool isOverride) - { - if (subElement.IsOverride()) - { - loadMultiplePrefabs(subElement, true); - } - else if (subElement.Name.LocalName.Equals("talents", StringComparison.OrdinalIgnoreCase)) - { - loadMultiplePrefabs(subElement, isOverride); - } - else if (subElement.Name.LocalName.Equals("talent", StringComparison.OrdinalIgnoreCase)) - { - loadSinglePrefab(subElement, isOverride); - } - else - { - DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentPrefab)} prefab type: '{subElement.Name}' in {file.Path}"); - } - } - - interpretElement(doc.Root, false); - } - - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading talent prefabs: "); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 20f7865ae..0d6c08118 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class TalentTree : IPrefab, IDisposable + class TalentTree : Prefab { public enum TalentTreeStageState { @@ -20,124 +21,44 @@ namespace Barotrauma public readonly List TalentSubTrees = new List(); - public XElement ConfigElement + public ContentXElement ConfigElement { get; private set; } - public string OriginalName => Identifier; - - public string Identifier { get; } - - public string FilePath { get; } - - public ContentPackage ContentPackage { get; set; } - - public TalentTree(XElement element, string filePath) + public TalentTree(ContentXElement element, TalentTreesFile file) : base(file, element.GetAttributeIdentifier("jobIdentifier", "")) { ConfigElement = element; - FilePath = filePath; - Identifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant(); - if (string.IsNullOrEmpty(Identifier)) + if (Identifier.IsEmpty) { - DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!"); + DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); return; } - foreach (XElement subTreeElement in element.GetChildElements("subtree")) + foreach (var subTreeElement in element.GetChildElements("subtree")) { TalentSubTrees.Add(new TalentSubTree(subTreeElement)); } - - // talents found and unlocked using the identifier wihin the talent tree, so no duplicates may occur - HashSet duplicateSet = new HashSet(); - foreach (string talent in TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier)))) - { - TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase)); - if (talentPrefab == null) - { - DebugConsole.AddWarning($"Talent tree for job {Identifier} contains non-existent talent {talent}! Talent tree not added."); - return; - } - if (!duplicateSet.Add(talent)) - { - DebugConsole.ThrowError($"Talent tree for job {Identifier} contains duplicate talent {talent}! Talent tree not added."); - return; - } - } } - - public bool TalentIsInTree(string talentIdentifier) + + public bool TalentIsInTree(Identifier talentIdentifier) { return TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))).Any(c => c == talentIdentifier); } - public static void LoadFromFile(ContentFile file) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier) { - DebugConsole.Log("Loading talent tree: " + file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - void loadSinglePrefab(XElement element, bool isOverride) - { - JobTalentTrees.Add(new TalentTree(element, file.Path) { ContentPackage = file.ContentPackage }, isOverride); - } - - void loadMultiplePrefabs(XElement element, bool isOverride) - { - foreach (var subElement in element.Elements()) - { - interpretElement(subElement, isOverride); - } - } - - void interpretElement(XElement subElement, bool isOverride) - { - if (subElement.IsOverride()) - { - loadMultiplePrefabs(subElement, true); - } - else if (subElement.Name.LocalName.Equals("talenttrees", StringComparison.OrdinalIgnoreCase)) - { - loadMultiplePrefabs(subElement, isOverride); - } - else if (subElement.Name.LocalName.Equals("talenttree", StringComparison.OrdinalIgnoreCase)) - { - loadSinglePrefab(subElement, isOverride); - } - else - { - DebugConsole.ThrowError($"Invalid XML element for the {nameof(TalentTree)} prefab type: '{subElement.Name}' in {file.Path}"); - } - } - - interpretElement(doc.Root, false); - } - - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading talent tree: "); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - } - - public static bool IsViableTalentForCharacter(Character character, string talentIdentifier) - { - return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? Enumerable.Empty()); + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection)Array.Empty()); } // i hate this function - markus - public static TalentTreeStageState GetTalentOptionStageState(Character character, string subTreeIdentifier, int index, List selectedTalents) + public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List selectedTalents) { if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } - if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); @@ -187,12 +108,12 @@ namespace Barotrauma } - public static bool IsViableTalentForCharacter(Character character, string talentIdentifier, IEnumerable selectedTalents) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection selectedTalents) { if (character?.Info?.Job.Prefab == null) { return false; } if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } - if (!JobTalentTrees.TryGetValue(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } + if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } foreach (var subTree in talentTree.TalentSubTrees) { @@ -218,15 +139,15 @@ namespace Barotrauma return false; } - public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) + public static List CheckTalentSelection(Character controlledCharacter, IEnumerable selectedTalents) { - List viableTalents = new List(); + List viableTalents = new List(); bool canStillUnlock = true; // keep trying to unlock talents until none of the talents are unlockable while (canStillUnlock && selectedTalents.Any()) { canStillUnlock = false; - foreach (string talent in selectedTalents) + foreach (Identifier talent in selectedTalents) { if (!viableTalents.Contains(talent) && IsViableTalentForCharacter(controlledCharacter, talent, viableTalents)) { @@ -238,32 +159,26 @@ namespace Barotrauma return viableTalents; } - private bool disposed = false; - public void Dispose() - { - if (disposed) { return; } - disposed = true; - JobTalentTrees.Remove(this); - } + public override void Dispose() { } } class TalentSubTree { - public string Identifier { get; } + public Identifier Identifier { get; } - public string DisplayName { get; } + public LocalizedString DisplayName { get; } public bool ForceUnlock; public readonly List TalentOptionStages = new List(); - public TalentSubTree(XElement subTreeElement) + public TalentSubTree(ContentXElement subTreeElement) { - Identifier = subTreeElement.GetAttributeString("identifier", ""); + Identifier = subTreeElement.GetAttributeIdentifier("identifier", ""); - DisplayName = TextManager.Get("talenttree." + Identifier, returnNull: true) ?? Identifier; + DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value); - foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) + foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { TalentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } @@ -273,21 +188,20 @@ namespace Barotrauma class TalentOption { - public readonly List Talents = new List(); + private readonly ImmutableHashSet talentIdentifiers; - public TalentOption(XElement talentOptionsElement, string debugIdentifier) + public IEnumerable Talents + => talentIdentifiers.Select(id => TalentPrefab.TalentPrefabs[id]); + + public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) { - foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) + var talentIdentifiers = new HashSet(); + foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) { - string identifier = talentOptionElement.GetAttributeString("identifier", string.Empty); - - if (!TalentPrefab.TalentPrefabs.ContainsKey(identifier)) - { - DebugConsole.ThrowError($"Error in talent tree \"{debugIdentifier}\" - could not find a talent with the identifier \"{identifier}\"."); - return; - } - Talents.Add(TalentPrefab.TalentPrefabs[identifier]); + Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + talentIdentifiers.Add(identifier); } + this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs new file mode 100644 index 000000000..e3cfc518c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/AfflictionsFile.cs @@ -0,0 +1,113 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class AfflictionsFile : ContentFile + { + private readonly static ImmutableHashSet afflictionTypes; + static AfflictionsFile() + { + afflictionTypes = ReflectionUtils.GetDerivedNonAbstract() + .ToImmutableHashSet(); + } + + public AfflictionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + private void ParseElement(ContentXElement element, bool overriding) + { + Identifier elementName = element.NameAsIdentifier(); + if (element.IsOverride()) + { + element.Elements().ForEach(s => ParseElement(s, overriding: true)); + } + else if (elementName == "Afflictions") + { + element.Elements().ForEach(s => ParseElement(s, overriding: overriding)); + } + else if (elementName == "cprsettings") + { + var cprSettings = new CPRSettings(element, this); + CPRSettings.Prefabs.Add(cprSettings, overriding); + } + else if (elementName == "damageoverlay") + { +#if CLIENT + var damageOverlay = new CharacterHealth.DamageOverlayPrefab(element, this); + CharacterHealth.DamageOverlayPrefab.Prefabs.Add(damageOverlay, overriding); +#endif + } + else + { + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) + { + DebugConsole.ThrowError( + $"No identifier defined for the affliction '{elementName}' in file '{Path}'"); + return; + } + + if (AfflictionPrefab.Prefabs.ContainsKey(identifier)) + { + if (overriding) + { + DebugConsole.NewMessage( + $"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'", + Color.Yellow); + } + else + { + DebugConsole.ThrowError( + $"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'"); + return; + } + } + + var type = afflictionTypes.FirstOrDefault(t => + t.Name == elementName + || t.Name == $"Affliction{elementName}".ToIdentifier()) + ?? typeof(Affliction); + var prefab = CreatePrefab(element, type); + AfflictionPrefab.Prefabs.Add(prefab, overriding); + } + } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc?.Root is null) { return; } + ParseElement(doc.Root.FromPackage(ContentPackage), overriding: false); + } + + private AfflictionPrefab CreatePrefab(ContentXElement element, Type type) + { + if (type == typeof(AfflictionHusk)) { return new AfflictionPrefabHusk(element, this, type); } + return new AfflictionPrefab(element, this, type); + } + + public override void UnloadFile() + { +#if CLIENT + CharacterHealth.DamageOverlayPrefab.Prefabs.RemoveByFile(this); +#endif + CPRSettings.Prefabs.RemoveByFile(this); + AfflictionPrefab.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { +#if CLIENT + CharacterHealth.DamageOverlayPrefab.Prefabs.Sort(); +#endif + CPRSettings.Prefabs.Sort(); + AfflictionPrefab.Prefabs.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs new file mode 100644 index 000000000..f9c370c8e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BackgroundCreaturePrefabsFile.cs @@ -0,0 +1,9 @@ +namespace Barotrauma +{ + sealed class BackgroundCreaturePrefabsFile : OtherFile + { + public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + //this content type only comes into play when a level is generated, so LoadFile and UnloadFile don't have anything to do + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs new file mode 100644 index 000000000..1b45bc400 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BallastFloraFile.cs @@ -0,0 +1,19 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage, AlternativeContentTypeNames("MapCreature")] + sealed class BallastFloraFile : GenericPrefabFile + { + public BallastFloraFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors"; + protected override PrefabCollection prefabs => BallastFloraPrefab.Prefabs; + + protected override BallastFloraPrefab CreatePrefab(ContentXElement element) + { + return new BallastFloraPrefab(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs new file mode 100644 index 000000000..785fee918 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/BeaconStationFile.cs @@ -0,0 +1,8 @@ +namespace Barotrauma +{ + [RequiredByCorePackage] + public class BeaconStationFile : BaseSubFile + { + public BeaconStationFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs new file mode 100644 index 000000000..717057ff6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CaveGenerationParametersFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class CaveGenerationParametersFile : GenericPrefabFile + { + public CaveGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "cave"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters"; + 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 new file mode 100644 index 000000000..1a0b569d5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CharacterFile.cs @@ -0,0 +1,100 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class CharacterFile : ContentFile + { + public CharacterFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) + { + DebugConsole.ThrowError($"Loading character file failed: {Path}"); + return; + } + if (CharacterPrefab.Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.ContentFile == this))) + { + DebugConsole.ThrowError($"Duplicate path: {Path}"); + return; + } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool isOverride = mainElement.IsOverride(); + if (isOverride) { mainElement = mainElement.FirstElement(); } + if (!CharacterPrefab.CheckSpeciesName(mainElement, this, out Identifier n)) { return; } + var prefab = new CharacterPrefab(mainElement, this); + CharacterPrefab.Prefabs.Add(prefab, isOverride); + } + + public override void UnloadFile() + { + CharacterPrefab.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + CharacterPrefab.Prefabs.SortAll(); + } + + public override void Preload(Action addPreloadedSprite) + { +#if CLIENT + CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(Path.Value); + if (characterPrefab?.ConfigElement == null) + { + throw new Exception($"Failed to load the character config file from {Path}!"); + } + var mainElement = characterPrefab.ConfigElement; + mainElement.GetChildElements("sound").ForEach(e => RoundSound.Load(e)); + if (!CharacterPrefab.CheckSpeciesName(mainElement, this, out Identifier speciesName)) { return; } + bool humanoid = mainElement.GetAttributeBool("humanoid", false); + RagdollParams ragdollParams; + try + { + if (humanoid) + { + ragdollParams = RagdollParams.GetRagdollParams(speciesName); + } + else + { + ragdollParams = RagdollParams.GetRagdollParams(speciesName); + } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); + return; + } + + if (ragdollParams != null) + { + HashSet texturePaths = new HashSet + { + ragdollParams.Texture + }; + foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) + { + if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); } + if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); } + if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); } + foreach (var decorativeSprite in limb.decorativeSpriteParams) + { + if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); } + } + } + foreach (string texturePath in texturePaths) + { + addPreloadedSprite(new Sprite(texturePath, Vector2.Zero)); + } + } +#endif + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs new file mode 100644 index 000000000..9a6732858 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -0,0 +1,117 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + [AttributeUsage(AttributeTargets.Class, Inherited = true)] + public class NotSyncedInMultiplayer : Attribute { } + + /// + /// Base class for content file types, which are loaded + /// from filelist.xml via reflection. + /// PLEASE AVOID INHERITING FROM THIS CLASS DIRECTLY. + /// Inheriting from GenericPrefabFile<T> is likely what + /// you want. + /// + public abstract class ContentFile + { + public class TypeInfo + { + public readonly Type Type; + public readonly bool RequiredByCorePackage; + public readonly bool NotSyncedInMultiplayer; + public readonly ImmutableHashSet? AlternativeTypes; + public readonly ImmutableHashSet Names; + + public TypeInfo(Type type) + { + Type = type; + + var reqByCoreAttribute = type.GetCustomAttribute(); + RequiredByCorePackage = reqByCoreAttribute != null; + var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); + NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; + AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; + + HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; + if (type.GetCustomAttribute()?.Names is { } altNames) + { + names.UnionWith(altNames); + } + + Names = names.ToImmutableHashSet(); + } + + public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => + (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); + } + + public readonly static ImmutableHashSet Types; + static ContentFile() + { + Types = ReflectionUtils.GetDerivedNonAbstract() + .Select(t => new TypeInfo(t)) + .ToImmutableHashSet(); + } + + public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) + { + Result fail(string error) + => Result.Failure(error); + + Identifier elemName = element.NameAsIdentifier(); + var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); + var filePath = element.GetAttributeContentPath("file", contentPackage); + if (type is null) + { + return fail($"Invalid content type \"{elemName}\""); + } + + if (filePath is null) + { + return fail($"No content path defined for file of type \"{elemName}\""); + } + try + { + var file = type.CreateInstance(contentPackage, filePath); + return file is null + ? throw new Exception($"Content type is not implemented correctly") + : Result.Success(file); + } + catch (Exception e) + { + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}\n{e.StackTrace.CleanupStackTrace()}"); + } + } + + protected ContentFile(ContentPackage contentPackage, ContentPath path) + { + ContentPackage = contentPackage; + Path = path; + Hash = CalculateHash(); + } + + public readonly ContentPackage ContentPackage; + public readonly ContentPath Path; + public readonly Md5Hash Hash; + public abstract void LoadFile(); + public abstract void UnloadFile(); + public abstract void Sort(); + + public virtual void Preload(Action addPreloadedSprite) { } + + public virtual Md5Hash CalculateHash() + { + return Md5Hash.CalculateForFile(Path.Value, Md5Hash.StringHashOptions.IgnoreWhitespace); + } + + public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs new file mode 100644 index 000000000..b9eb4ddce --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/CorpsesFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class CorpsesFile : GenericPrefabFile + { + public CorpsesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses"; + 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/DecalsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DecalsFile.cs new file mode 100644 index 000000000..9c2562f04 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/DecalsFile.cs @@ -0,0 +1,22 @@ +namespace Barotrauma +{ + public sealed class DecalsFile : ContentFile + { + public DecalsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + DecalManager.LoadFromFile(this); + } + + public override void UnloadFile() + { + DecalManager.RemoveByFile(this); + } + + public override void Sort() + { + DecalManager.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs new file mode 100644 index 000000000..61f3b8651 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EnemySubmarineFile.cs @@ -0,0 +1,8 @@ +namespace Barotrauma +{ + [RequiredByCorePackage] + public class EnemySubmarineFile : BaseSubFile + { + public EnemySubmarineFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs new file mode 100644 index 000000000..298f618c5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/EventManagerSettingsFile.cs @@ -0,0 +1,17 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class EventManagerSettingsFile : GenericPrefabFile + { + public EventManagerSettingsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings"; + protected override PrefabCollection prefabs => EventManagerSettings.Prefabs; + protected override EventManagerSettings CreatePrefab(ContentXElement element) + { + return new EventManagerSettings(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs new file mode 100644 index 000000000..bb200e5c6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/FactionsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class FactionsFile : GenericPrefabFile + { + public FactionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "faction"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "factions"; + 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 new file mode 100644 index 000000000..794968346 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + abstract class GenericPrefabFile : ContentFile where T : Prefab + { + protected GenericPrefabFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected abstract bool MatchesSingular(Identifier identifier); + protected abstract bool MatchesPlural(Identifier identifier); + protected abstract PrefabCollection prefabs { get; } + protected abstract T CreatePrefab(ContentXElement element); + + private void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = parentElement.NameAsIdentifier(); + var childElements = parentElement.Elements() +#if DEBUG + .OrderBy(e => Rand.Int(int.MaxValue, Rand.RandSync.Unsynced)).ToArray() +#endif + ; + if (parentElement.IsOverride()) + { + foreach (var element in childElements) + { + LoadFromXElement(element, true); + } + } + else if (elemName == "clear") + { + prefabs.AddOverrideFile(this); + } + else if (MatchesSingular(elemName)) + { + T prefab = CreatePrefab(parentElement); + prefabs.Add(prefab, overriding); + } + else if (MatchesPlural(elemName)) + { + foreach (var element in childElements) + { + LoadFromXElement(element, overriding); + } + } + else + { + DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + } + } + + public override sealed void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override sealed void UnloadFile() + { + prefabs.RemoveByFile(this); + } + + public sealed override void Sort() + { + prefabs.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs new file mode 100644 index 000000000..a76a64ea0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/HashlessFile.cs @@ -0,0 +1,10 @@ +namespace Barotrauma +{ + [NotSyncedInMultiplayer] + public abstract class HashlessFile : ContentFile + { + public HashlessFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public sealed override Md5Hash CalculateHash() => Md5Hash.Blank; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs new file mode 100644 index 000000000..71f5cd664 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs @@ -0,0 +1,17 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class ItemAssemblyFile : GenericPrefabFile + { + public ItemAssemblyFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies"; + 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 new file mode 100644 index 000000000..5065470c2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class ItemFile : GenericPrefabFile + { + public ItemFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "items"; + 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/JobsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs new file mode 100644 index 000000000..7c2291e84 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/JobsFile.cs @@ -0,0 +1,60 @@ +using Barotrauma; +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + + [RequiredByCorePackage] + sealed class JobsFile : ContentFile + { + public JobsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + LoadElements(doc.Root.FromPackage(ContentPackage), false); + } + + private void LoadElements(ContentXElement mainElement, bool isOverride) + { + foreach (var element in mainElement.Elements()) + { + if (element.NameAsIdentifier() == "nojob") + { + JobPrefab.NoJobElement ??= element; + } + else if (element.NameAsIdentifier() == "ItemRepairPriorities") + { + foreach (var subElement in element.Elements()) + { + ItemRepairPriority prio = new ItemRepairPriority(subElement, this); + ItemRepairPriority.Prefabs.Add(prio, isOverride); + } + } + else if (element.IsOverride()) + { + LoadElements(element, true); + } + else + { + var job = new JobPrefab(element, this); + JobPrefab.Prefabs.Add(job, isOverride); + } + } + } + + public override void UnloadFile() + { + JobPrefab.Prefabs.RemoveByFile(this); + ItemRepairPriority.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + JobPrefab.Prefabs.SortAll(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs new file mode 100644 index 000000000..b590cc063 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelGenerationParametersFile.cs @@ -0,0 +1,68 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class LevelGenerationParametersFile : ContentFile + { + public LevelGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + private void LoadBiomes(ContentXElement element, bool isOverride) + { + foreach (var subElement in element.Elements()) + { + Biome biome = new Biome(subElement, this); + Biome.Prefabs.Add(biome, isOverride); + } + } + + private void LoadLevelGenerationParams(ContentXElement element, bool isOverride) + { + LevelGenerationParams lParams = new LevelGenerationParams(element, this); + LevelGenerationParams.LevelParams.Add(lParams, isOverride); + } + + private void LoadSubElements(ContentXElement element, bool overridePropagation) + { + foreach (var subElement in element.Elements()) + { + if (subElement.IsOverride()) + { + LoadSubElements(subElement, true); + } + else if (subElement.NameAsIdentifier() == "clear") + { + LevelGenerationParams.LevelParams.AddOverrideFile(this); + Biome.Prefabs.AddOverrideFile(this); + } + else if (subElement.NameAsIdentifier() == "biomes") + { + LoadBiomes(subElement, overridePropagation); + } + else + { + LoadLevelGenerationParams(subElement, overridePropagation); + } + } + } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc is null) { return; } + LoadSubElements(doc.Root.FromPackage(ContentPackage), false); + } + + public override void UnloadFile() + { + LevelGenerationParams.LevelParams.RemoveByFile(this); + Biome.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + LevelGenerationParams.LevelParams.SortAll(); + Biome.Prefabs.SortAll(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs new file mode 100644 index 000000000..228dca7cf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LevelObjectPrefabsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class LevelObjectPrefabsFile : GenericPrefabFile + { + public LevelObjectPrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects"; + 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 new file mode 100644 index 000000000..cd3cc4c91 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/LocationTypesFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class LocationTypesFile : GenericPrefabFile + { + public LocationTypesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes"; + 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/MapGenerationParametersFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MapGenerationParametersFile.cs new file mode 100644 index 000000000..b45d9bbf5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MapGenerationParametersFile.cs @@ -0,0 +1,34 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class MapGenerationParametersFile : ContentFile + { + public MapGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) + { + DebugConsole.ThrowError($"Loading map generation parameters file failed: {Path}"); + return; + } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool isOverride = mainElement.IsOverride(); + if (isOverride) { mainElement = mainElement.FirstElement(); } + var prefab = new MapGenerationParams(mainElement, this); + MapGenerationParams.Params.Add(prefab, isOverride); + } + + public override void UnloadFile() + { + MapGenerationParams.Params.RemoveByFile(this); + } + + public override void Sort() + { + MapGenerationParams.Params.Sort(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs new file mode 100644 index 000000000..11efb2d0c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/MissionsFile.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class MissionsFile : GenericPrefabFile + { + /*private readonly static ImmutableHashSet missionTypes; + static MissionsFile() + { + missionTypes = ReflectionUtils.GetDerivedNonAbstract() + .ToImmutableHashSet(); + }*/ + + public MissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) + => !MatchesPlural(identifier); + /*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 MissionPrefab CreatePrefab(ContentXElement element) + { + return new MissionPrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs new file mode 100644 index 000000000..f26a17cd7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCConversationsFile.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class NPCConversationsFile : ContentFile + { + public NPCConversationsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool allowOverriding = doc.Root.IsOverride(); + if (allowOverriding) + { + mainElement = mainElement.FirstElement(); + } + + var npcConversationCollection = new NPCConversationCollection(this, mainElement); + if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language)) + { + NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection()); + } + NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding); + } + + public override void UnloadFile() + { + foreach (var collection in NPCConversationCollection.Collections.Values) + { + collection.RemoveByFile(this); + } + } + + public override void Sort() + { + foreach (var collection in NPCConversationCollection.Collections.Values) + { + collection.SortAll(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs new file mode 100644 index 000000000..85b2548d9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/NPCSetsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class NPCSetsFile : GenericPrefabFile + { + public NPCSetsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets"; + 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 new file mode 100644 index 000000000..5699a0410 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OrdersFile.cs @@ -0,0 +1,70 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + #warning TODO: this is almost a GenericPrefabFile. Must refactor further. + [RequiredByCorePackage] + sealed class OrdersFile : ContentFile + { + public OrdersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = new Identifier(parentElement.Name.ToString()); + if (parentElement.IsOverride()) + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, true); + } + } + else if (elemName == "order") + { + OrderPrefab prefab = new OrderPrefab(parentElement, this); + OrderPrefab.Prefabs.Add(prefab, overriding); + } + else if (elemName == "ordercategory") + { + OrderCategoryIcon prefab = new OrderCategoryIcon(parentElement, this); + OrderCategoryIcon.OrderCategoryIcons.Add(prefab, overriding); + } + else if (elemName == "orders") + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, overriding); + } + } + else if (elemName == "clear") + { + OrderCategoryIcon.OrderCategoryIcons.AddOverrideFile(this); + OrderPrefab.Prefabs.AddOverrideFile(this); + } + else + { + DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + } + } + + public override sealed void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override sealed void UnloadFile() + { + OrderCategoryIcon.OrderCategoryIcons.RemoveByFile(this); + OrderPrefab.Prefabs.RemoveByFile(this); + } + + public override sealed void Sort() + { + OrderCategoryIcon.OrderCategoryIcons.SortAll(); + OrderPrefab.Prefabs.SortAll(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs new file mode 100644 index 000000000..57b5eaf64 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OtherFile.cs @@ -0,0 +1,16 @@ +using Barotrauma; + +namespace Barotrauma +{ + + [AlternativeContentTypeNames("None")] + public class OtherFile : HashlessFile + { + public OtherFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + //this content type is completely ignored by the game so LoadFile and UnloadFile don't do anything + public sealed override void LoadFile() { } + public sealed override void UnloadFile() { } + public sealed override void Sort() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs new file mode 100644 index 000000000..1972243dc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostConfigFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage(alternativeTypes: typeof(OutpostFile))] + sealed class OutpostConfigFile : GenericPrefabFile + { + public OutpostConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters"; + 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/OutpostFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostFile.cs new file mode 100644 index 000000000..e03d31d3b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostFile.cs @@ -0,0 +1,7 @@ +namespace Barotrauma +{ + public class OutpostFile : BaseSubFile + { + public OutpostFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs new file mode 100644 index 000000000..eb673d840 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/OutpostModuleFIle.cs @@ -0,0 +1,7 @@ +namespace Barotrauma +{ + public class OutpostModuleFile : BaseSubFile + { + public OutpostModuleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs new file mode 100644 index 000000000..de128d17c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ParticlesFile.cs @@ -0,0 +1,31 @@ +using System.Xml.Linq; +#if CLIENT +using Barotrauma.Particles; +#endif + +namespace Barotrauma +{ + [RequiredByCorePackage] + [NotSyncedInMultiplayer] +#if CLIENT + sealed class ParticlesFile : GenericPrefabFile + { + public ParticlesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + 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 ParticlePrefab CreatePrefab(ContentXElement element) + { + return new ParticlePrefab(element, this); + } + + public override Md5Hash CalculateHash() => Md5Hash.Blank; + } +#else + sealed class ParticlesFile : OtherFile + { + public ParticlesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } //this content type doesn't do anything on a server + } +#endif +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs new file mode 100644 index 000000000..d4c5b1c43 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RandomEventsFile.cs @@ -0,0 +1,91 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class RandomEventsFile : ContentFile + { + public RandomEventsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = new Identifier(parentElement.Name.ToString()); + if (parentElement.IsOverride()) + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, true); + } + } + else if (elemName == "randomevents") + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, overriding); + } + } + else if (elemName == "eventprefabs") + { + foreach (var subElement in parentElement.Elements()) + { + var prefab = new EventPrefab(subElement, this); + EventPrefab.Prefabs.Add(prefab, overriding); + } + } + else if (elemName == "eventsprites") + { +#if CLIENT + foreach (var subElement in parentElement.Elements()) + { + var prefab = new EventSprite(subElement, this); + EventSprite.Prefabs.Add(prefab, overriding); + } +#endif + } + else if (elemName == "eventset") + { + var prefab = new EventSet(parentElement, this); + EventSet.Prefabs.Add(prefab, overriding); + } + else if (elemName == "clear") + { + EventPrefab.Prefabs.AddOverrideFile(this); + EventSet.Prefabs.AddOverrideFile(this); +#if CLIENT + EventSprite.Prefabs.AddOverrideFile(this); +#endif + } + else + { + DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}"); + } + } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override void UnloadFile() + { + EventPrefab.Prefabs.RemoveByFile(this); + EventSet.Prefabs.RemoveByFile(this); +#if CLIENT + EventSprite.Prefabs.RemoveByFile(this); +#endif + } + + public override void Sort() + { + EventPrefab.Prefabs.SortAll(); + EventSet.Prefabs.SortAll(); +#if CLIENT + EventSprite.Prefabs.SortAll(); +#endif + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs new file mode 100644 index 000000000..9a8de8fd1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/RuinConfigFile.cs @@ -0,0 +1,19 @@ +using Barotrauma.RuinGeneration; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class RuinConfigFile : GenericPrefabFile + { + public RuinConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters"; + 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/SkillSettingsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SkillSettingsFile.cs new file mode 100644 index 000000000..01216f3bf --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SkillSettingsFile.cs @@ -0,0 +1,33 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + sealed class SkillSettingsFile : ContentFile + { + public SkillSettingsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + var mainElement = doc.Root.FromPackage(ContentPackage); + bool allowOverriding = mainElement.IsOverride(); + if (allowOverriding) + { + mainElement = mainElement.FirstElement(); + } + var prefab = new SkillSettings(mainElement, this); + SkillSettings.Prefabs.Add(prefab, allowOverriding); + } + + public override void UnloadFile() + { + SkillSettings.Prefabs.RemoveByFile(this); + } + + public override void Sort() + { + SkillSettings.Prefabs.Sort(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs new file mode 100644 index 000000000..57034f4d1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SoundsFile.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Immutable; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] +#if CLIENT + sealed class SoundsFile : GenericPrefabFile + { + public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override PrefabCollection prefabs => SoundPrefab.Prefabs; + + protected override SoundPrefab CreatePrefab(ContentXElement element) + { + var elemName = element.NameAsIdentifier(); + if (SoundPrefab.TagToDerivedPrefab.ContainsKey(elemName)) + { + return Activator.CreateInstance(SoundPrefab.TagToDerivedPrefab[elemName], new object[] { element, this }) as SoundPrefab; + } + return new SoundPrefab(element, this); + } + + protected override bool MatchesPlural(Identifier identifier) => identifier == "sounds"; + + protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier); + + public override Md5Hash CalculateHash() => Md5Hash.Blank; + } +#else + sealed class SoundsFile : OtherFile + { + public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +#endif +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs new file mode 100644 index 000000000..b961311ab --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/StructureFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class StructureFile : GenericPrefabFile + { + public StructureFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + 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 StructurePrefab CreatePrefab(ContentXElement element) + { + return new StructurePrefab(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs new file mode 100644 index 000000000..dcd3107ff --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/SubmarineFile.cs @@ -0,0 +1,38 @@ +using System; +using System.Security.Cryptography; + +namespace Barotrauma +{ + public abstract class BaseSubFile : ContentFile + { + protected BaseSubFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) + { + using var md5 = MD5.Create(); + #warning TODO: this doesn't account for collisions, this should probably be using the PrefabCollection class like everything else + UintIdentifier = ToolBox.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5); + } + + public readonly UInt32 UintIdentifier; + + public override void LoadFile() + { + SubmarineInfo.RefreshSavedSub(Path.Value); + } + + public override void UnloadFile() + { + SubmarineInfo.RefreshSavedSub(Path.Value); + } + + public override void Sort() + { + //Overrides for subs don't exist! Should we change this? + } + } + + [NotSyncedInMultiplayer] + public class SubmarineFile : BaseSubFile + { + public SubmarineFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs new file mode 100644 index 000000000..6ca1f9c68 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentTreesFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TalentTreesFile : GenericPrefabFile + { + public TalentTreesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees"; + protected override PrefabCollection prefabs => TalentTree.JobTalentTrees; + protected override TalentTree CreatePrefab(ContentXElement element) + { + return new TalentTree(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs new file mode 100644 index 000000000..1b5b05f4f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TalentsFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TalentsFile : GenericPrefabFile + { + public TalentsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "talent"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "talents"; + protected override PrefabCollection prefabs => TalentPrefab.TalentPrefabs; + protected override TalentPrefab CreatePrefab(ContentXElement element) + { + return new TalentPrefab(element, this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs new file mode 100644 index 000000000..de37b623a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public sealed class TextFile : ContentFile + { + public TextFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public override void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + var mainElement = doc.Root.FromPackage(ContentPackage); + + var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value); + + LanguageIdentifier language = languageName.ToLanguageIdentifier(); + if (!TextManager.TextPacks.ContainsKey(language)) + { + TextManager.TextPacks.TryAdd(language, ImmutableHashSet.Empty); + } + + var newPack = new TextPack(this, mainElement, language); + var newHashSet = TextManager.TextPacks[language].Add(newPack); + TextManager.TextPacks.TryRemove(language, out _); + TextManager.TextPacks.TryAdd(language, newHashSet); + TextManager.IncrementLanguageVersion(); + } + + public override void UnloadFile() + { + foreach (var kvp in TextManager.TextPacks.ToArray()) + { + var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet(); + TextManager.TextPacks.TryRemove(kvp.Key, out _); + if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); } + } + TextManager.IncrementLanguageVersion(); + } + + public override void Sort() + { + //Overrides for text packs don't exist! Should we change this? + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs new file mode 100644 index 000000000..3a58364e0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TraitorMissionsFile.cs @@ -0,0 +1,26 @@ +using System.Xml.Linq; + +#warning TODO: This file is just about the only thing that's actually somewhat okay about the current traitor system. Gut the whole thing. + +#if CLIENT +using PrefabType = Barotrauma.TraitorMissionPrefab; +#elif SERVER +using PrefabType = Barotrauma.TraitorMissionPrefab.TraitorMissionEntry; +#endif + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class TraitorMissionsFile : GenericPrefabFile + { + public TraitorMissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions"; + 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/UIStyleFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UIStyleFile.cs new file mode 100644 index 000000000..78571aec0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UIStyleFile.cs @@ -0,0 +1,100 @@ +using System; +using Barotrauma.Extensions; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ +#if CLIENT + public sealed class UIStyleFile : HashlessFile + { + public UIStyleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public void LoadFromXElement(ContentXElement parentElement, bool overriding) + { + Identifier elemName = parentElement.NameAsIdentifier(); + Identifier elemNameWithFontSuffix = elemName.AppendIfMissing("Font"); + if (parentElement.IsOverride()) + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, true); + } + } + else if (GUIStyle.Fonts.ContainsKey(elemNameWithFontSuffix)) + { + GUIFontPrefab prefab = new GUIFontPrefab(parentElement, this); + GUIStyle.Fonts[elemNameWithFontSuffix].Prefabs.Add(prefab, overriding); + } + else if (GUIStyle.Sprites.ContainsKey(elemName)) + { + GUISpritePrefab prefab = new GUISpritePrefab(parentElement, this); + GUIStyle.Sprites[elemName].Prefabs.Add(prefab, overriding); + } + else if (GUIStyle.SpriteSheets.ContainsKey(elemName)) + { + GUISpriteSheetPrefab prefab = new GUISpriteSheetPrefab(parentElement, this); + GUIStyle.SpriteSheets[elemName].Prefabs.Add(prefab, overriding); + } + else if (GUIStyle.Colors.ContainsKey(elemName)) + { + GUIColorPrefab prefab = new GUIColorPrefab(parentElement, this); + GUIStyle.Colors[elemName].Prefabs.Add(prefab, overriding); + } + else if (elemName == "cursor") + { + GUICursorPrefab prefab = new GUICursorPrefab(parentElement, this); + GUIStyle.CursorSprite.Prefabs.Add(prefab, overriding); + } + else if (elemName == "style") + { + foreach (var element in parentElement.Elements()) + { + LoadFromXElement(element, overriding); + } + } + else + { + GUIComponentStyle prefab = new GUIComponentStyle(parentElement, this); + GUIStyle.ComponentStyles.Add(prefab, overriding); + } + } + + public override sealed void LoadFile() + { + XDocument doc = XMLExtensions.TryLoadXml(Path); + if (doc == null) { return; } + + var rootElement = doc.Root.FromPackage(ContentPackage); + LoadFromXElement(rootElement, false); + } + + public override sealed void UnloadFile() + { + GUIStyle.ComponentStyles.RemoveByFile(this); + GUIStyle.CursorSprite.Prefabs.RemoveByFile(this); + GUIStyle.Fonts.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + GUIStyle.Sprites.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + GUIStyle.SpriteSheets.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + GUIStyle.Colors.Values.ForEach(p => p.Prefabs.RemoveByFile(this)); + } + + public override sealed void Sort() + { + GUIStyle.ComponentStyles.SortAll(); + GUIStyle.CursorSprite.Prefabs.Sort(); + GUIStyle.Fonts.Values.ForEach(p => p.Prefabs.Sort()); + GUIStyle.Sprites.Values.ForEach(p => p.Prefabs.Sort()); + GUIStyle.SpriteSheets.Values.ForEach(p => p.Prefabs.Sort()); + GUIStyle.Colors.Values.ForEach(p => p.Prefabs.Sort()); + } + } +#else + public sealed class UIStyleFile : OtherFile + { + public UIStyleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +#endif +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs new file mode 100644 index 000000000..61de9e96b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/UpgradeModulesFile.cs @@ -0,0 +1,31 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class UpgradeModulesFile : GenericPrefabFile + { + public UpgradeModulesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => + identifier == "upgrademodule" || + identifier == "upgradecategory"; + + protected override bool MatchesPlural(Identifier identifier) => + identifier == "upgrademodules"; + + protected override PrefabCollection prefabs => UpgradeContentPrefab.PrefabsAndCategories; + protected override UpgradeContentPrefab CreatePrefab(ContentXElement element) + { + Identifier elemName = element.NameAsIdentifier(); + if (elemName == "upgradecategory") + { + return new UpgradeCategory(element, this); + } + else + { + return new UpgradePrefab(element, this); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs new file mode 100644 index 000000000..54a445ff0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckAIConfigFile.cs @@ -0,0 +1,18 @@ +using System.Xml.Linq; + +namespace Barotrauma +{ + [RequiredByCorePackage] + sealed class WreckAIConfigFile : GenericPrefabFile + { + public WreckAIConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig"; + protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs"; + protected override PrefabCollection prefabs => WreckAIConfig.Prefabs; + protected override WreckAIConfig CreatePrefab(ContentXElement element) + { + return new WreckAIConfig(element, this); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs new file mode 100644 index 000000000..89dfa873c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/WreckFile.cs @@ -0,0 +1,7 @@ +namespace Barotrauma +{ + public class WreckFile : BaseSubFile + { + public WreckFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs new file mode 100644 index 000000000..c097e2aa4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -0,0 +1,306 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Steam; + +namespace Barotrauma +{ + public abstract class ContentPackage + { + #warning TODO: make this independent of the current version + public static readonly Version MinimumHashCompatibleVersion = GameMain.Version; + + public const string LocalModsDir = "LocalMods"; + public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine( + SaveUtil.SaveFolder, + "WorkshopMods", + "Installed"); + + public const string FileListFileName = "filelist.xml"; + public const string DefaultModVersion = "1.0.0"; + + public readonly string Name; + public readonly ImmutableArray AltNames; + public readonly string Path; + public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? ""; + public readonly UInt64 SteamWorkshopId; + + public readonly Version GameVersion; + public readonly string ModVersion; + public readonly Md5Hash Hash; + public readonly DateTime? InstallTime; + + public readonly ImmutableArray Files; + public readonly ImmutableArray Errors; + + public async Task IsUpToDate() + { + if (SteamWorkshopId != 0 && InstallTime.HasValue) + { + Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId); + if (item is null) { return true; } + return item.Value.LatestUpdateTime <= InstallTime; + } + return true; + } + + public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); + + #warning TODO: remove this, unless we truly believe that determining "multiplayer-incompatible content" is something we should do + public readonly bool HasMultiplayerIncompatibleContent; + + protected ContentPackage(XDocument doc, string path) + { + Path = path.CleanUpPathCrossPlatform(); + XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null."); + + Name = rootElement.GetAttributeString("name", "").Trim(); + AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty()) + .Select(n => n.Trim()).ToImmutableArray(); + AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); + SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0); + + GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version); + ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); + if (rootElement.Attribute("installtime") != null) + { + InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0)); + } + else + { + InstallTime = null; + } + + var fileResults = rootElement.Elements() + .Select(e => ContentFile.CreateFromXElement(this, e)) + .ToArray(); + + Files = fileResults + .OfType>() + .Select(f => f.Value) + .ToImmutableArray(); + + Errors = fileResults + .OfType>() + .Select(f => f.Error) + .ToImmutableArray(); + + HasMultiplayerIncompatibleContent = Files.Any(f => !f.NotSyncedInMultiplayer); + + Hash = CalculateHash(); + var expectedHash = rootElement.GetAttributeString("expectedhash", ""); + if (HashMismatches(expectedHash)) + { + DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})"); + } + } + + public bool HashMismatches(string expectedHash) + => GameVersion >= MinimumHashCompatibleVersion && + !expectedHash.IsNullOrWhiteSpace() && + !expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase); + + public IEnumerable GetFiles() where T : ContentFile => Files.Where(f => f is T).Cast(); + + public IEnumerable GetFiles(Type type) + => !type.IsSubclassOf(typeof(ContentFile)) + ? throw new ArgumentException($"Type must be subclass of ContentFile, got {type.Name}") + : Files.Where(f => f.GetType() == type || f.GetType().IsSubclassOf(type)); + + public bool NameMatches(Identifier name) + => Name == name || AltNames.Any(n => n == name); + + public bool NameMatches(string name) + => NameMatches(name.ToIdentifier()); + + public static ContentPackage? TryLoad(string path) + { + XDocument doc = XMLExtensions.TryLoadXml(path); + + try + { + if (doc.Root.GetAttributeBool("corepackage", false)) + { + return new CorePackage(doc, path); + } + else + { + return new RegularPackage(doc, path); + } + } + catch (Exception e) + { + while (e.InnerException != null) { e = e.InnerException; } + DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}"); + return null; + } + } + + public Md5Hash CalculateHash(bool logging = false) + { + using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5); + + if (logging) + { + DebugConsole.NewMessage("****************************** Calculating content package hash " + Name); + } + + foreach (ContentFile file in Files) + { + try + { + var hash = file.Hash; + if (logging) + { + DebugConsole.NewMessage(" " + file.Path + ": " + hash.StringRepresentation); + } + incrementalHash.AppendData(hash.ByteRepresentation); + } + + catch (Exception e) + { + DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e); + break; + } + } + + var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset()); + if (logging) + { + DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.StringRepresentation); + } + + return md5Hash; + } + + protected void AssertCondition(bool condition, string errorMsg) + { + if (!condition) + { + throw new InvalidOperationException($"Failed to load \"{Name ?? Path}\": {errorMsg}"); + } + } + + public void LoadFilesOfType() where T : ContentFile + { + Files.Where(f => f is T).ForEach(f => f.LoadFile()); + } + + public void UnloadFilesOfType() where T : ContentFile + { + Files.Where(f => f is T).ForEach(f => f.UnloadFile()); + } + + public enum LoadResult + { + Success, + Failure + } + + public LoadResult LoadPackage() + { + foreach (var p in LoadPackageEnumerable()) + { + if (p.Exception != null) { return LoadResult.Failure; } + } + return LoadResult.Success; + } + + public IEnumerable LoadPackageEnumerable() + { + ContentFile[] getFilesToLoad(Predicate predicate) + => Files.Where(predicate.Invoke).ToArray() +#if DEBUG + //The game should be able to work just fine with a completely arbitrary file load order. + //To make sure we don't mess this up, debug builds randomize it so it has a higher chance + //of breaking anything that's not implemented correctly. + .Randomize() +#endif + ; + + IEnumerable loadFiles(ContentFile[] filesToLoad, int indexOffset) + { + for (int i = 0; i < filesToLoad.Length; i++) + { + Exception? exception = null; + try + { + //do not allow exceptions thrown here to crash the game + filesToLoad[i].LoadFile(); + } + catch (Exception e) + { + exception = e; + } + if (exception != null) + { + yield return ContentPackageManager.LoadProgress.Failure(exception); + break; + } + yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length); + } + } + + //Load the UI files first. This is to allow the game to render + //the text in the loading screen as soon as possible. + var priorityFiles = getFilesToLoad(f => f is UIStyleFile); + + var remainder = getFilesToLoad(f => !priorityFiles.Contains(f)); + + var loadEnumerable = + loadFiles(priorityFiles, 0) + .Concat(loadFiles(remainder, priorityFiles.Length)); + + foreach (var p in loadEnumerable) + { + if (p.Exception != null) + { + HandleLoadException(p.Exception); + yield return p; + break; + } + yield return p; + } + } + + protected abstract void HandleLoadException(Exception e); + + public void UnloadPackage() + { + Files.ForEach(f => f.UnloadFile()); + } + + public override int GetHashCode() + { + byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4)); + return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3]; + } + + public static bool PathAllowedAsLocalModFile(string path) + { +#if DEBUG + if (GameMain.VanillaContent.Files.Any(f => f.Path == path)) + { + //file is in vanilla package, this is allowed + return true; + } +#endif + + while (true) + { + string temp = Barotrauma.IO.Path.GetDirectoryName(path) ?? ""; + if (string.IsNullOrEmpty(temp)) { break; } + path = temp; + } + return path == LocalModsDir; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs new file mode 100644 index 000000000..33daaec39 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -0,0 +1,52 @@ +using Barotrauma.Extensions; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Barotrauma +{ + [AttributeUsage(AttributeTargets.Class)] + public class RequiredByCorePackage : Attribute + { + public readonly ImmutableHashSet AlternativeTypes; + public RequiredByCorePackage(params Type[] alternativeTypes) + { + AlternativeTypes = alternativeTypes.ToImmutableHashSet(); + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class AlternativeContentTypeNames : Attribute + { + public readonly ImmutableHashSet Names; + public AlternativeContentTypeNames(params string[] names) + { + Names = names.ToIdentifiers().ToImmutableHashSet(); + } + } + + public class CorePackage : ContentPackage + { + public CorePackage(XDocument doc, string path) : base(doc, path) + { + AssertCondition(doc.Root.GetAttributeBool("corepackage", false), + "Expected a core package, got a regular package"); + + var missingFileTypes = ContentFile.Types.Where( + t => t.RequiredByCorePackage + && !Files.Any(f => t.Type == f.GetType() + || t.AlternativeTypes.Contains(f.GetType()))); + AssertCondition(!missingFileTypes.Any(), + "Core package requires at least one of the following content types: " + + string.Join(", ", missingFileTypes.Select(t => t.Type.Name))); + } + + protected override void HandleLoadException(Exception e) + { + throw new Exception($"An exception was thrown while loading \"{Name}\"", e); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs new file mode 100644 index 000000000..b66bb5c10 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/RegularPackage.cs @@ -0,0 +1,19 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class RegularPackage : ContentPackage + { + public RegularPackage(XDocument doc, string path) : base(doc, path) + { + AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package"); + } + + protected override void HandleLoadException(Exception e) + { + UnloadPackage(); + DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs new file mode 100644 index 000000000..58d0bd8a4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -0,0 +1,461 @@ +#nullable enable +using Barotrauma.Extensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Steam; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public static partial class ContentPackageManager + { + public const string CopyIndicatorFileName = ".copying"; + public const string VanillaFileList = "Content/ContentPackages/Vanilla.xml"; + + public const string CorePackageElementName = "corepackage"; + public const string RegularPackagesElementName = "regularpackages"; + public const string RegularPackagesSubElementName = "package"; + + public static class EnabledPackages + { + public static CorePackage? Core { get; private set; } = null; + + private static readonly List regular = new List(); + public static IReadOnlyList Regular => regular; + + public static IEnumerable All => + Core != null + ? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular) + : Enumerable.Empty(); + + private static class BackupPackages + { + public static CorePackage? Core; + public static ImmutableArray? Regular; + } + + public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume(); + + public static IEnumerable SetCoreEnumerable(CorePackage newCore) + { + var oldCore = Core; + if (newCore == oldCore) { yield break; } + Core?.UnloadPackage(); + Core = newCore; + foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } + ThrowIfDuplicates(All); + yield return new LoadProgress(1.0f); + } + + public static void ReloadCore() + { + if (Core == null) { return; } + Core.UnloadPackage(); + Core.LoadPackage(); + ThrowIfDuplicates(All); + } + + public static void EnableRegular(RegularPackage p) + { + if (regular.Contains(p)) { return; } + + var newRegular = regular.ToList(); + newRegular.Add(p); + SetRegular(newRegular); + } + + public static void SetRegular(IReadOnlyList newRegular) + => SetRegularEnumerable(newRegular).Consume(); + + public static IEnumerable SetRegularEnumerable(IReadOnlyList inNewRegular) + { + if (ReferenceEquals(inNewRegular, regular)) { yield break; } + if (inNewRegular.SequenceEqual(regular)) { yield break; } + ThrowIfDuplicates(inNewRegular); + var newRegular = inNewRegular.ToList(); + IEnumerable toUnload = regular.Where(r => !newRegular.Contains(r)); + RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray(); + toUnload.ForEach(r => r.UnloadPackage()); + + Range loadingRange = new Range(0.0f, 1.0f); + + for (int i = 0; i < toLoad.Length; i++) + { + var package = toLoad[i]; + loadingRange = new Range(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length); + foreach (var progress in package.LoadPackageEnumerable()) + { + if (progress.Exception != null) + { + //If an exception was thrown while loading this package, refuse to add it to the list of enabled packages + newRegular.Remove(package); + break; + } + yield return progress.Transform(loadingRange); + } + } + regular.Clear(); regular.AddRange(newRegular); + SortContent(); + yield return new LoadProgress(1.0f); + } + + public static void ThrowIfDuplicates(IEnumerable pkgs) + { + var contentPackages = pkgs as IList ?? pkgs.ToArray(); + if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2))) + { + throw new InvalidOperationException($"Input contains duplicate packages"); + } + } + + private class TypeComparer : IEqualityComparer + { + public bool Equals([AllowNull] T x, [AllowNull] T y) + { + if (x is null || y is null) + { + return x is null == y is null; + } + return x.GetType() == y.GetType(); + } + + public int GetHashCode([DisallowNull] T obj) + { + return obj.GetType().GetHashCode(); + } + } + + public static void SortContent() + { + ThrowIfDuplicates(All); + All + .SelectMany(r => r.Files) + .Distinct(new TypeComparer()) + .ForEach(f => f.Sort()); + } + + public static int IndexOf(ContentPackage contentPackage) + { + if (contentPackage is CorePackage core) + { + if (core == Core) { return 0; } + return -1; + } + else if (contentPackage is RegularPackage reg) + { + return Regular.IndexOf(reg) + 1; + } + return -1; + } + + public static void DisableRemovedMods() + { + if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) + { + SetCore(ContentPackageManager.CorePackages.First()); + } + SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray()); + } + + public static void BackUp() + { + if (BackupPackages.Core != null || BackupPackages.Regular != null) + { + throw new InvalidOperationException("Tried to back up enabled packages multiple times"); + } + + BackupPackages.Core = Core; + BackupPackages.Regular = Regular.ToImmutableArray(); + } + + public static void Restore() + { + if (BackupPackages.Core == null || BackupPackages.Regular == null) + { + DebugConsole.AddWarning("Tried to restore enabled packages multiple times/without performing a backup"); + return; + } + + SetCore(BackupPackages.Core); + SetRegular(BackupPackages.Regular); + + BackupPackages.Core = null; + BackupPackages.Regular = null; + } + } + + public sealed partial class PackageSource : ICollection + { + private readonly Predicate? skipPredicate; + + public PackageSource(string dir, Predicate? skipPredicate) + { + this.skipPredicate = skipPredicate; + directory = dir; + Directory.CreateDirectory(directory); + } + + public void SwapPackage(ContentPackage oldPackage, ContentPackage newPackage) + { + bool contains = false; + if (oldPackage is CorePackage oldCore && corePackages.Contains(oldCore)) + { + corePackages.Remove(oldCore); + contains = true; + } + else if (oldPackage is RegularPackage oldRegular && regularPackages.Contains(oldRegular)) + { + regularPackages.Remove(oldRegular); + contains = true; + } + + if (contains) + { + if (newPackage is CorePackage newCore) + { + corePackages.Add(newCore); + } + else if (newPackage is RegularPackage newRegular) + { + regularPackages.Add(newRegular); + } + } + } + + public void Refresh() + { + //remove packages that have been deleted from the directory + corePackages.RemoveWhere(p => !File.Exists(p.Path)); + regularPackages.RemoveWhere(p => !File.Exists(p.Path)); + + //load packages that have been added to the directory + var subDirs = Directory.GetDirectories(directory); + foreach (string subDir in subDirs) + { + var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform(); + if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; } + if (File.Exists(fileListPath)) + { + if (skipPredicate?.Invoke(fileListPath) is true) { continue; } + + ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath); + if (newPackage is CorePackage corePackage) + { + corePackages.Add(corePackage); + } + else if (newPackage is RegularPackage regularPackage) + { + regularPackages.Add(regularPackage); + } + + if (!(newPackage is null)) + { + Debug.WriteLine($"Loaded \"{newPackage.Name}\""); + } + } + } + } + + private readonly string directory; + private readonly HashSet regularPackages = new HashSet(); + public IEnumerable Regular => regularPackages; + + private readonly HashSet corePackages = new HashSet(); + public IEnumerable Core => corePackages; + + public IEnumerator GetEnumerator() + { + foreach (var core in Core) { yield return core; } + foreach (var regular in Regular) { yield return regular; } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void ICollection.Add(ContentPackage item) => throw new InvalidOperationException(); + + void ICollection.Clear() => throw new InvalidOperationException(); + + public bool Contains(ContentPackage item) + => item switch + { + CorePackage core => corePackages.Contains(core), + RegularPackage regular => this.regularPackages.Contains(regular), + _ => throw new ArgumentException($"Expected regular or core package, got {item.GetType().Name}") + }; + + void ICollection.CopyTo(ContentPackage[] array, int arrayIndex) + { + foreach (var package in corePackages) + { + array[arrayIndex] = package; + arrayIndex++; + } + + foreach (var package in regularPackages) + { + array[arrayIndex] = package; + arrayIndex++; + } + } + + bool ICollection.Remove(ContentPackage item) => throw new InvalidOperationException(); + + public int Count => corePackages.Count + regularPackages.Count; + public bool IsReadOnly => true; + } + + public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null); + public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath); + + public static CorePackage? VanillaCorePackage { get; private set; } = null; + + public static IEnumerable CorePackages + => (VanillaCorePackage is null + ? Enumerable.Empty() + : VanillaCorePackage.ToEnumerable()) + .CollectionConcat(LocalPackages.Core.CollectionConcat(WorkshopPackages.Core)); + + public static IEnumerable RegularPackages + => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); + + public static IEnumerable AllPackages + => LocalPackages.CollectionConcat(WorkshopPackages); + + public static void UpdateContentPackageList() + { + LocalPackages.Refresh(); + WorkshopPackages.Refresh(); + EnabledPackages.DisableRemovedMods(); + } + + public static ContentPackage? ReloadContentPackage(ContentPackage p) + { + ContentPackage? newPackage = ContentPackage.TryLoad(p.Path); + if (newPackage is CorePackage core) + { + if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); } + } + else if (newPackage is RegularPackage regular) + { + int index = EnabledPackages.Regular.IndexOf(p); + if (index >= 0) + { + var newRegular = EnabledPackages.Regular.ToArray(); + newRegular[index] = regular; + EnabledPackages.SetRegular(newRegular); + } + } + + if (newPackage != null) + { + LocalPackages.SwapPackage(p, newPackage); + WorkshopPackages.SwapPackage(p, newPackage); + } + EnabledPackages.DisableRemovedMods(); + return newPackage; + } + + public readonly struct LoadProgress + { + public readonly float Value; + public readonly Exception? Exception; + + public LoadProgress(float value) + { + Value = value; + Exception = null; + } + + private LoadProgress(Exception exception) + { + Value = -1f; + Exception = exception; + } + + public static LoadProgress Failure(Exception exception) + => new LoadProgress(exception); + + public LoadProgress Transform(Range range) + => Exception != null + ? this + : new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value)); + } + + public static void LoadVanillaFileList() + { + VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); + } + + public static IEnumerable Init() + { + Range loadingRange = new Range(0.0f, 1.0f); + + SteamManager.Workshop.DeleteFailedCopies(); + UpdateContentPackageList(); + + if (VanillaCorePackage is null) { LoadVanillaFileList(); } + + CorePackage enabledCorePackage = VanillaCorePackage!; + List enabledRegularPackages = new List(); + +#if CLIENT + TaskPool.Add("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { }); +#else + #warning TODO: implement Workshop updates for servers at some point +#endif + + var contentPackagesElement = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath)?.Root + ?.GetChildElement("ContentPackages"); + if (contentPackagesElement != null) + { + T? findPackage(IEnumerable packages, XElement? elem) where T : ContentPackage + { + if (elem is null) { return null; } + string name = elem.GetAttributeString("name", ""); + string path = elem.GetAttributeStringUnrestricted("path", "").CleanUpPathCrossPlatform(correctFilenameCase: false); + return + packages.FirstOrDefault(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase)) + ?? packages.FirstOrDefault(p => p.NameMatches(name)); + } + + var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName); + enabledCorePackage = findPackage(CorePackages, corePackageElement) ?? VanillaCorePackage!; + + var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName); + if (regularPackagesElement != null) + { + XElement[] regularPackageElements = regularPackagesElement.GetChildElements(RegularPackagesSubElementName).ToArray(); + for (int i = 0; i < regularPackageElements.Length; i++) + { + var regularPackage = findPackage(RegularPackages, regularPackageElements[i]); + if (regularPackage != null) { enabledRegularPackages.Add(regularPackage); } + } + } + } + + int pkgCount = 1 + enabledRegularPackages.Count; //core + regular + + loadingRange = new Range(0.01f, 1.0f / pkgCount); + foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage)) + { + yield return p.Transform(loadingRange); + } + + loadingRange = new Range(1.0f / pkgCount, 1.0f); + foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages)) + { + yield return p.Transform(loadingRange); + } + + yield return new LoadProgress(1.0f); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs new file mode 100644 index 000000000..3d0e13e7a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -0,0 +1,149 @@ +#nullable enable + +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Barotrauma.IO; + +namespace Barotrauma +{ + public sealed class ContentPath + { + public readonly static ContentPath Empty = new ContentPath(null, ""); + + public const string ModDirStr = "%ModDir%"; + public const string OtherModDirFmt = "%ModDir:{0}%"; + private static readonly Regex OtherModDirRegex = new Regex( + string.Format(OtherModDirFmt, "(.+?)")); + + public readonly string? RawValue; + + public readonly ContentPackage? ContentPackage; + + private string? cachedValue; + private string? cachedFullPath; + + public string Value + { + get + { + if (RawValue.IsNullOrEmpty()) { return ""; } + if (!cachedValue.IsNullOrEmpty()) { return cachedValue!; } + + string? modName = ContentPackage?.Name; + + var otherMods = OtherModDirRegex.Matches(RawValue ?? throw new NullReferenceException($"{nameof(RawValue)} is null.")) + .Select(m => m.Groups[1].Value.Trim().ToIdentifier()) + .Distinct().Where(id => !id.IsEmpty && id != modName).ToHashSet(); + cachedValue = RawValue!; + if (!(ContentPackage is null)) + { + string modPath = Path.GetDirectoryName(ContentPackage.Path)!; + cachedValue = cachedValue + .Replace(ModDirStr, modPath, StringComparison.OrdinalIgnoreCase) + .Replace(string.Format(OtherModDirFmt, ContentPackage.Name), modPath, StringComparison.OrdinalIgnoreCase); + if (ContentPackage.SteamWorkshopId != 0) + { + cachedValue = cachedValue + .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); + } + } + var allPackages = ContentPackageManager.AllPackages; + foreach (Identifier otherModName in otherMods) + { + if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; } + ContentPackage? otherMod = + allPackages.FirstOrDefault(p => workshopId != 0 && p.SteamWorkshopId != 0 && workshopId == p.SteamWorkshopId) + ?? allPackages.FirstOrDefault(p => p.Name == otherModName) + ?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName)) + ?? throw new MissingContentPackageException(ContentPackage, otherModName.Value); + cachedValue = cachedValue.Replace(string.Format(OtherModDirFmt, otherModName.Value), Path.GetDirectoryName(otherMod.Path)); + } + cachedValue = cachedValue.CleanUpPath(); + return cachedValue; + } + } + + public string FullPath + { + get + { + if (cachedFullPath.IsNullOrEmpty()) + { + if (Value.IsNullOrEmpty()) + { + return ""; + } + cachedFullPath = Path.GetFullPath(Value).CleanUpPathCrossPlatform(correctFilenameCase: false); + } + return cachedFullPath!; + } + } + + private ContentPath(ContentPackage? contentPackage, string? rawValue) + { + ContentPackage = contentPackage; + RawValue = rawValue; + cachedValue = null; + cachedFullPath = null; + } + + public static ContentPath FromRaw(string? rawValue) + => new ContentPath(null, rawValue); + + public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue) + => new ContentPath(contentPackage, rawValue); + + public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue) + { + throw new NotImplementedException(); + } + + private static bool StringEquality(string? a, string? b) + => (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || + string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), + Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), + StringComparison.OrdinalIgnoreCase); + + public static bool operator==(ContentPath a, ContentPath b) + => StringEquality(a.Value, b.Value); + + public static bool operator!=(ContentPath a, ContentPath b) => !(a == b); + + public static bool operator==(ContentPath a, string? b) + => StringEquality(a.Value, b); + + public static bool operator!=(ContentPath a, string? b) => !(a == b); + + public static bool operator==(string? a, ContentPath b) + => StringEquality(a, b.Value); + + public static bool operator!=(string? a, ContentPath b) => !(a == b); + + protected bool Equals(ContentPath other) + { + return RawValue == other.RawValue && Equals(ContentPackage, other.ContentPackage) && cachedValue == other.cachedValue && cachedFullPath == other.cachedFullPath; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ContentPath)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(RawValue, ContentPackage, cachedValue, cachedFullPath); + } + + public bool IsNullOrEmpty() => string.IsNullOrEmpty(Value); + public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value); + + public bool EndsWith(string suffix) => Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); + + public override string? ToString() => Value; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs new file mode 100644 index 000000000..60f048a4e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -0,0 +1,130 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Xml.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public sealed class ContentXElement + { + public ContentPackage? ContentPackage { get; private set; } + public readonly XElement Element; + + public ContentXElement(ContentPackage? contentPackage, XElement element) + { + ContentPackage = contentPackage; + Element = element; + } + + public static implicit operator XElement?(ContentXElement? cxe) => cxe?.Element; + //public static implicit operator ContentXElement?(XElement? xe) => xe is null ? null : new ContentXElement(null, xe); + + public XName Name => Element.Name; + public Identifier NameAsIdentifier() => Element.NameAsIdentifier(); + + public string BaseUri => Element.BaseUri; + + public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null."); + + public ContentXElement? FirstElement() => Elements().FirstOrDefault(); + + public ContentXElement? Parent => Element.Parent is null ? null : new ContentXElement(ContentPackage, Element.Parent); + public bool HasElements => Element.HasElements; + + public bool IsOverride() => Element.IsOverride(); + + public bool ComesAfter(ContentXElement other) => Element.ComesAfter(other.Element); + + public ContentXElement? GetChildElement(string name) + => Element.GetChildElement(name) is { } elem ? new ContentXElement(ContentPackage, elem) : null; + + public IEnumerable Elements() + => Element.Elements().Select(e => new ContentXElement(ContentPackage, e)); + + public IEnumerable ElementsBeforeSelf() + => Element.ElementsBeforeSelf().Select(e => new ContentXElement(ContentPackage, e)); + + public IEnumerable Descendants() + => Element.Descendants().Select(e => new ContentXElement(ContentPackage, e)); + + public IEnumerable GetChildElements(string name) + => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase)); + + public XAttribute? Attribute(string name) => Element.Attribute(name); + + public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); + + public IEnumerable Attributes() => Element.Attributes(); + public IEnumerable Attributes(string name) => Element.Attributes(name); + + public string ElementInnerText() => Element.ElementInnerText(); + + public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); + public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); + public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); + public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); + public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); + public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage); + public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def); + public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def); + public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def); + public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def); + public float[]? GetAttributeFloatArray(string key, float[]? def) => Element.GetAttributeFloatArray(key, def); + public float GetAttributeFloat(float def, params string[] keys) => Element.GetAttributeFloat(def, keys); + public bool GetAttributeBool(string key, bool def) => Element.GetAttributeBool(key, def); + public Point GetAttributePoint(string key, in Point def) => Element.GetAttributePoint(key, def); + public Vector2 GetAttributeVector2(string key, in Vector2 def) => Element.GetAttributeVector2(key, def); + public Vector4 GetAttributeVector4(string key, in Vector4 def) => Element.GetAttributeVector4(key, def); + public Color GetAttributeColor(string key, in Color def) => Element.GetAttributeColor(key, def); + public Color? GetAttributeColor(string key) => Element.GetAttributeColor(key); + public Color[]? GetAttributeColorArray(string key, Color[]? def) => Element.GetAttributeColorArray(key, def); + public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def); + public T GetAttributeEnum(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def); + public (T1, T2) GetAttributeTuple(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def); + public (T1, T2)[] GetAttributeTupleArray(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def); + + public Identifier VariantOf() => Element.VariantOf(); + + public bool DoesAttributeReferenceFileNameAlone(string key) => Element.DoesAttributeReferenceFileNameAlone(key); + + public string ParseContentPathFromUri() => Element.ParseContentPathFromUri(); + + public void SetAttributeValue(string key, string val) => Element.SetAttributeValue(key, val); + + public void Add(ContentXElement elem) + { + Element.Add(elem.Element); + elem.ContentPackage = ContentPackage; + #warning TODO: update %ModDir% instances in case the content package changes + } + + public void AddFirst(ContentXElement elem) + { + Element.AddFirst(elem.Element); + elem.ContentPackage = ContentPackage; + #warning TODO: update %ModDir% instances in case the content package changes + } + + public void AddAfterSelf(ContentXElement elem) + { + Element.AddAfterSelf(elem.Element); + elem.ContentPackage = ContentPackage; + #warning TODO: update %ModDir% instances in case the content package changes + } + + public void Remove() => Element.Remove(); + } + + public static class ContentXElementExtensions + { + public static ContentXElement FromPackage(this XElement element, ContentPackage? contentPackage) + => new ContentXElement(contentPackage, element); + + public static IEnumerable Elements(this IEnumerable elements) + => elements.SelectMany(e => e.Elements()); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs new file mode 100644 index 000000000..b19d0f034 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs @@ -0,0 +1,160 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Barotrauma +{ + // Identifier struct to eliminate case-sensitive comparisons + public readonly struct Identifier : IComparable, IEquatable + { + public readonly static Identifier Empty = default; + + private readonly static int emptyHash = "".GetHashCode(StringComparison.OrdinalIgnoreCase); + + private readonly string? value; + private readonly Lazy? hashCode; + + public string Value => value ?? ""; + public int HashCode => hashCode?.Value ?? emptyHash; + + public Identifier(string? str) + { + value = str; + hashCode = new Lazy(() => (str ?? "").GetHashCode(StringComparison.OrdinalIgnoreCase)); + } + + public bool IsEmpty => Value.IsNullOrEmpty(); + + public Identifier IfEmpty(in Identifier id) + => IsEmpty ? id : this; + + public Identifier Replace(in Identifier subStr, in Identifier newStr) + => Replace(subStr.Value ?? "", newStr.Value ?? ""); + + public Identifier Replace(string subStr, string newStr) + => (Value?.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + + public Identifier Remove(Identifier subStr) + => Remove(subStr.Value ?? ""); + + public Identifier Remove(string subStr) + => (Value?.Remove(subStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + + public override bool Equals(object? obj) => + obj switch + { + Identifier i => this == i, + string s => this == s, + _ => base.Equals(obj) + }; + + public bool StartsWith(string str) => Value?.StartsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + + public bool StartsWith(Identifier id) => StartsWith(id.Value ?? ""); + + public bool EndsWith(string str) => Value?.EndsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + + public bool EndsWith(Identifier id) => EndsWith(id.Value ?? ""); + + public Identifier AppendIfMissing(string suffix) + => EndsWith(suffix) ? this : $"{this}{suffix}".ToIdentifier(); + + public Identifier RemoveFromEnd(string suffix) + => (Value?.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase)).ToIdentifier(); + + public bool Contains(string str) => Value?.Contains(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty(); + + public bool Contains(in Identifier id) => Contains(id.Value ?? ""); + + public override string ToString() => Value ?? ""; + + public override int GetHashCode() => HashCode; + + public int CompareTo(object? obj) + { + return string.Compare(Value, obj?.ToString() ?? "", StringComparison.InvariantCultureIgnoreCase); + } + + public bool Equals([AllowNull] Identifier other) + { + return this == other; + } + + private static bool StringEquality(string? a, string? b) + => (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || string.Equals(a, b, StringComparison.OrdinalIgnoreCase); + + public static bool operator ==(in Identifier a, in Identifier b) => + StringEquality(a.Value, b.Value); + + public static bool operator !=(in Identifier a, in Identifier b) => + !(a == b); + + public static bool operator ==(in Identifier identifier, string? str) => + StringEquality(identifier.Value, str); + + public static bool operator !=(in Identifier identifier, string? str) => + !(identifier == str); + + public static bool operator ==(string? str, in Identifier identifier) => + identifier == str; + + public static bool operator !=(string? str, in Identifier identifier) => + !(identifier == str); + + public static bool operator ==(in Identifier? a, in Identifier? b) => + StringEquality(a?.Value, b?.Value); + + public static bool operator !=(in Identifier? a, in Identifier? b) => + !(a == b); + + public static bool operator ==(in Identifier? a, string? b) => + StringEquality(a?.Value, b); + + public static bool operator !=(in Identifier? a, string? b) => + !(a == b); + + public static bool operator ==(string str, in Identifier? identifier) => + identifier == str; + + public static bool operator !=(string str, in Identifier? identifier) => + !(identifier == str); + } + + public static class IdentifierExtensions + { + public static IEnumerable ToIdentifiers(this IEnumerable strings) + { + foreach (string s in strings) + { + if (string.IsNullOrEmpty(s)) { continue; } + yield return new Identifier(s); + } + } + + public static Identifier[] ToIdentifiers(this string[] strings) + => ((IEnumerable)strings).ToIdentifiers().ToArray(); + + public static Identifier ToIdentifier(this string? s) + { + return new Identifier(s); + } + + public static Identifier ToIdentifier(this T t) where T: notnull + { + return t.ToString().ToIdentifier(); + } + + public static bool Contains(this ISet set, string identifier) + { + return set.Contains(identifier.ToIdentifier()); + } + + public static bool ContainsKey(this IReadOnlyDictionary dictionary, string key) + { + return dictionary.ContainsKey(key.ToIdentifier()); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs new file mode 100644 index 000000000..2c6d0df5e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs @@ -0,0 +1,17 @@ +#nullable enable +using System; + +namespace Barotrauma +{ + public sealed class MissingContentPackageException : Exception + { + public override string Message { get; } + + public MissingContentPackageException(ContentPackage? whoAsked, string? missingPackage) + { + Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " + + $"with name or ID \"{missingPackage ?? "[NULL]"}\" " + + $"that is not currently installed."; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs new file mode 100644 index 000000000..e69de29bb diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs deleted file mode 100644 index dfcd2b32d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ /dev/null @@ -1,875 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.Steam; - -namespace Barotrauma -{ - public enum ContentType - { - None, - Submarine, - Jobs, - Item, - ItemAssembly, - Character, - Structure, - Outpost, - OutpostModule, - OutpostConfig, - BeaconStation, - NPCSets, - Factions, - Text, - ServerExecutable, - LocationTypes, - MapGenerationParameters, - LevelGenerationParameters, - CaveGenerationParameters, - LevelObjectPrefabs, - RandomEvents, - Missions, - BackgroundCreaturePrefabs, - Sounds, - RuinConfig, - Particles, - Decals, - NPCConversations, - Afflictions, - Tutorials, - UIStyle, - TraitorMissions, - EventManagerSettings, - Orders, - SkillSettings, - Wreck, - Corpses, - WreckAIConfig, - UpgradeModules, - MapCreature, - EnemySubmarine, - Talents, - TalentTrees, - } - - public class ContentPackage - { - public static string Folder = "Data/ContentPackages/"; - - private static readonly List regularPackages = new List(); - public static IReadOnlyList RegularPackages - { - get { return regularPackages; } - } - - private static readonly List corePackages = new List(); - public static IReadOnlyList CorePackages - { - get { return corePackages; } - } - - public static IEnumerable AllPackages - { - get { return corePackages.Concat(regularPackages); } - } - - //these types of files are included in the MD5 hash calculation, - //meaning that the players must have the exact same files to play together - public static HashSet MultiplayerIncompatibleContent { get; private set; } = new HashSet - { - ContentType.Jobs, - ContentType.Item, - ContentType.Character, - ContentType.Structure, - ContentType.LocationTypes, - ContentType.NPCSets, - ContentType.Factions, - ContentType.MapGenerationParameters, - ContentType.LevelGenerationParameters, - ContentType.CaveGenerationParameters, - ContentType.Missions, - ContentType.LevelObjectPrefabs, - ContentType.RuinConfig, - ContentType.Outpost, - ContentType.OutpostModule, - ContentType.OutpostConfig, - ContentType.Wreck, - ContentType.WreckAIConfig, - ContentType.BeaconStation, - ContentType.Afflictions, - ContentType.Orders, - ContentType.Corpses, - ContentType.UpgradeModules, - ContentType.MapCreature, - ContentType.EnemySubmarine, - ContentType.Talents, - }; - - //at least one file of each these types is required in core content packages - private static readonly HashSet corePackageRequiredFiles = new HashSet - { - ContentType.Jobs, - ContentType.Item, - ContentType.Character, - ContentType.Structure, - //TODO: there needs to be either outpost files or outpost generation parameters, both aren't required - //ContentType.Outpost, - //ContentType.OutpostGenerationParams, - ContentType.Factions, - ContentType.Wreck, - ContentType.WreckAIConfig, - ContentType.BeaconStation, - ContentType.Text, - ContentType.ServerExecutable, - ContentType.LocationTypes, - ContentType.MapGenerationParameters, - ContentType.LevelGenerationParameters, - ContentType.CaveGenerationParameters, - ContentType.RandomEvents, - ContentType.Missions, - ContentType.RuinConfig, - ContentType.Afflictions, - ContentType.UIStyle, - ContentType.EventManagerSettings, - ContentType.Orders, - ContentType.Corpses, - ContentType.UpgradeModules, - ContentType.EnemySubmarine, - ContentType.Talents, - }; - - public static IEnumerable CorePackageRequiredFiles - { - get { return corePackageRequiredFiles; } - } - - public static bool IngameModSwap = false; - - public string Name { get; set; } = string.Empty; - - public string Path - { - get; - set; - } - - public ulong SteamWorkshopId; - public DateTime? InstallTime; - - public bool HideInWorkshopMenu - { - get; - private set; - } - - private Md5Hash md5Hash; - public Md5Hash MD5hash - { - get - { - if (md5Hash == null) - { - //TODO: before re-enabling content package hash caching, make sure the hash gets recalculated when any file in the content package changes, not just when the filelist.xml changes. - /*md5Hash = Md5Hash.FetchFromCache(Path); - if (md5Hash == null) - { - CalculateHash(); - md5Hash.SaveToCache(Path); - }*/ - CalculateHash(); - } - return md5Hash; - } - } - - //core packages are content packages that are required for the game to work - //e.g. they include the executable, some location types, level generation params and other files the game won't work without - //one (and only one) core package must always be selected - private bool isCorePackage; - public bool IsCorePackage - { - get { return isCorePackage; } - set - { - isCorePackage = value; - if (isCorePackage && regularPackages.Contains(this)) - { - corePackages.AddOnMainThread(this); - regularPackages.RemoveOnMainThread(this); - } - else if (!isCorePackage && corePackages.Contains(this)) - { - regularPackages.AddOnMainThread(this); - corePackages.RemoveOnMainThread(this); - } - } - } - - public Version GameVersion - { - get; set; - } - - - private readonly List files; - private readonly List filesToAdd; - private readonly List filesToRemove; - - public IReadOnlyList Files - { - get { return files; } - } - - public IEnumerable FilesUnsaved - { - get { return files.Where(f => !filesToRemove.Contains(f)).Concat(filesToAdd); } - } - - public IReadOnlyList FilesToAdd - { - get { return filesToAdd; } - } - - public IReadOnlyList FilesToRemove - { - get { return filesToRemove; } - } - - public bool HasMultiplayerIncompatibleContent - { - get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); } - } - - public bool IsCorrupt - { - get; - private set; - } - - private ContentPackage() - { - files = new List(); - filesToAdd = new List(); - filesToRemove = new List(); - } - - public ContentPackage(string filePath, string setPath = "") - : this() - { - filePath = filePath.CleanUpPath(); - if (!string.IsNullOrEmpty(setPath)) { setPath = setPath.CleanUpPath(); } - XDocument doc = XMLExtensions.TryLoadXml(filePath); - - Path = setPath == string.Empty ? filePath : setPath; - - if (doc?.Root == null) - { - DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!"); - IsCorrupt = true; - return; - } - - Name = doc.Root.GetAttributeString("name", ""); - HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false); - isCorePackage = doc.Root.GetAttributeBool("corepackage", false); - SteamWorkshopId = doc.Root.GetAttributeUInt64("steamworkshopid", 0); - string workshopUrl = doc.Root.GetAttributeString("steamworkshopurl", ""); - if (!string.IsNullOrEmpty(workshopUrl)) - { - SteamWorkshopId = SteamManager.GetWorkshopItemIDFromUrl(workshopUrl); - } - string versionStr = doc.Root.GetAttributeString("gameversion", "0.0.0.0"); - try - { - GameVersion = new Version(versionStr); - } - catch - { - DebugConsole.ThrowError($"Invalid version number in content package \"{Name}\" ({versionStr})."); - GameVersion = GameMain.Version; - } - if (doc.Root.Attribute("installtime") != null) - { - InstallTime = ToolBox.Epoch.ToDateTime(doc.Root.GetAttributeUInt("installtime", 0)); - } - - List errorMsgs = new List(); - foreach (XElement subElement in doc.Root.Elements()) - { - if (subElement.Name.ToString().Equals("executable", StringComparison.OrdinalIgnoreCase)) { continue; } - if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type)) - { - errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type."); - type = ContentType.None; - } - files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this)); - } - - if (Files.Count == 0) - { - //no files defined, find a submarine in here - //because somehow people have managed to upload - //mods without contentfile definitions - string folder = System.IO.Path.GetDirectoryName(filePath); - if (File.Exists(System.IO.Path.Combine(folder, Name+".sub"))) - { - files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this)); - } - else - { - errorMsgs.Add("Error in content package \"" + Name + "\" - no content files defined."); - } - } - - bool compatible = IsCompatible(); - //If we know that the package is not compatible, don't display error messages. - if (compatible) - { - foreach (string errorMsg in errorMsgs) - { - DebugConsole.ThrowError(errorMsg); - } - } - } - - private bool? hasErrors; - public bool HasErrors - { - get - { - if (!hasErrors.HasValue) - { - hasErrors = !CheckErrors(out _); - } - return hasErrors.Value; - } - } - - private List errorMessages; - public IEnumerable ErrorMessages - { - get - { - if (errorMessages == null) { CheckErrors(out _); } - return errorMessages; - } - } - - public override string ToString() - { - return Name; - } - - public bool IsCompatible() - { - if (Files.All(f => f.Type == ContentType.Submarine)) - { - return true; - } - - //content package compatibility checks were added in 0.8.9.1 - //v0.8.9.1 is not compatible with older content packages - if (GameVersion < new Version(0, 8, 9, 1)) - { - return false; - } - - //do additional checks here if later versions add changes that break compatibility - - return true; - } - - public bool ContainsRequiredCorePackageFiles() - { - return corePackageRequiredFiles.All(fileType => Files.Any(file => file.Type == fileType)); - } - - public bool ContainsRequiredCorePackageFiles(out List missingContentTypes) - { - missingContentTypes = new List(); - foreach (ContentType contentType in corePackageRequiredFiles) - { - if (!Files.Any(file => file.Type == contentType)) - { - missingContentTypes.Add(contentType); - } - } - return missingContentTypes.Count == 0; - } - - public bool CheckErrors(out List errorMessages) - { - this.errorMessages = errorMessages = new List(); - foreach (ContentFile file in Files) - { - switch (file.Type) - { - case ContentType.ServerExecutable: - case ContentType.None: - case ContentType.Outpost: - case ContentType.OutpostModule: - case ContentType.Submarine: - case ContentType.Wreck: - case ContentType.BeaconStation: - case ContentType.EnemySubmarine: - break; - default: - try - { - using FileStream stream = File.Open(file.Path, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using var reader = XMLExtensions.CreateReader(stream); - XDocument.Load(reader); - } - catch (Exception e) - { - if (TextManager.Initialized) - { - errorMessages.Add(TextManager.GetWithVariables("xmlfileinvalid", - new string[] { "[filepath]", "[errormessage]" }, - new string[] { file.Path, e.Message })); - } - else - { - errorMessages.Add($"XML File Invalid. PATH: {file.Path}, ERROR: {e.Message}"); -#if DEBUG - throw; -#endif - } - } - break; - } - } - - if (IsCorePackage && !ContainsRequiredCorePackageFiles(out List missingContentTypes)) - { - errorMessages.Add(TextManager.GetWithVariables("ContentPackageCantMakeCorePackage", - new string[2] { "[packagename]", "[missingfiletypes]" }, - new string[2] { Name, string.Join(", ", missingContentTypes) }, - new bool[2] { false, true })); - } - VerifyFiles(out List missingFileMessages); - - errorMessages.AddRange(missingFileMessages); - hasErrors = errorMessages.Count > 0; - return !hasErrors.Value; - } - - /// - /// Make sure all the files defined in the content package are present - /// - /// - public bool VerifyFiles(out List errorMessages) - { - errorMessages = new List(); - foreach (ContentFile file in Files) - { - //TODO: determine executable extension on platform and check for the presence of the executables - if (file.Type == ContentType.ServerExecutable) { continue; } - - if (!File.Exists(file.Path)) - { - errorMessages.Add("File \"" + file.Path + "\" not found."); - continue; - } - } - - return errorMessages.Count == 0; - } - - public static ContentPackage CreatePackage(string name, string path, bool corePackage) - { - ContentPackage newPackage = new ContentPackage() - { - Name = name, - Path = path, - isCorePackage = corePackage, - GameVersion = GameMain.Version - }; - - return newPackage; - } - - public ContentFile AddFile(string path, ContentType type) - { - if (Files.Concat(FilesToAdd).Any(file => file.Path == path && file.Type == type)) return null; - - ContentFile cf = new ContentFile(path, type) - { - ContentPackage = this - }; - filesToAdd.Add(cf); - - return cf; - } - - public void AddFile(ContentFile file) - { - if (filesToRemove.Contains(file)) { filesToRemove.Remove(file); } - if (Files.Concat(FilesToAdd).Any(f => f.Path == file.Path && f.Type == file.Type)) return; - - filesToAdd.Add(file); - } - - public void RemoveFile(ContentFile file) - { - if (filesToAdd.Contains(file)) { filesToAdd.Remove(file); } - if (files.Contains(file) && !filesToRemove.Contains(file)) { filesToRemove.Add(file); } - } - - public void Save(string filePath, bool reload = true) - { - var packagesToDeselect = corePackages.Concat(regularPackages).Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList(); - bool refreshFiles = false; - - if (packagesToDeselect.Any()) - { - foreach (var p in packagesToDeselect) - { - if (p.IsCorePackage) - { - if (GameMain.Config.CurrentCorePackage == p) - { - refreshFiles = true; - } - corePackages.RemoveOnMainThread(p); - } - else - { - if (GameMain.Config.EnabledRegularPackages.Contains(p)) - { - refreshFiles = true; - } - regularPackages.RemoveOnMainThread(p); - } - } - if (IsCorePackage) - { - corePackages.AddOnMainThread(this); - } - else - { - regularPackages.AddOnMainThread(this); - } - - if (refreshFiles) - { - GameMain.Config.DisableContentPackageItems(filesToRemove); - GameMain.Config.EnableContentPackageItems(filesToAdd); - GameMain.Config.RefreshContentPackageItems(filesToRemove.Concat(filesToAdd).Distinct()); - } - } - files.RemoveAll(f => filesToRemove.Contains(f)); - files.AddRange(filesToAdd); - filesToRemove.Clear(); filesToAdd.Clear(); - - XDocument doc = new XDocument(); - doc.Add(new XElement("contentpackage", - new XAttribute("name", Name), - new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)), - new XAttribute("corepackage", IsCorePackage))); - - doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString())); - - if (SteamWorkshopId != 0) - { - doc.Root.Add(new XAttribute("steamworkshopid", SteamWorkshopId.ToString())); - } - - if (InstallTime != null) - { - doc.Root.Add(new XAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value))); - } - - foreach (ContentFile file in Files) - { - doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform()))); - } - - doc.SaveSafe(filePath); - } - - public void CalculateHash(bool logging = false) - { - List hashes = new List(); - - if (logging) - { - DebugConsole.NewMessage("****************************** Calculating cp hash " + Name); - } - - foreach (ContentFile file in Files) - { - if (!MultiplayerIncompatibleContent.Contains(file.Type)) { continue; } - - try - { - var hash = CalculateFileHash(file); - if (logging) - { - var fileMd5 = new Md5Hash(hash); - DebugConsole.NewMessage(" " + file.Path + ": " + fileMd5.Hash); - } - hashes.Add(hash); - } - - catch (Exception e) - { - DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e); - break; - } - } - - byte[] bytes = new byte[hashes.Count * 16]; - for (int i = 0; i < hashes.Count; i++) - { - hashes[i].CopyTo(bytes, i * 16); - } - - md5Hash = new Md5Hash(bytes); - if (logging) - { - DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.Hash); - } - } - - private byte[] CalculateFileHash(ContentFile file) - { - using (MD5 md5 = MD5.Create()) - { - List filePaths = new List { file.Path }; - List data = new List(); - - switch (file.Type) - { - case ContentType.Character: - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - var ragdollFolder = RagdollParams.GetFolder(doc, file.Path); - if (Directory.Exists(ragdollFolder)) - { - Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f)); - } - var animationFolder = AnimationParams.GetFolder(doc, file.Path); - if (Directory.Exists(animationFolder)) - { - Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f)); - } - break; - } - - if (filePaths.Count > 1) - { - using (MD5 tempMd5 = MD5.Create()) - { - filePaths = filePaths.OrderBy(f => ToolBox.StringToUInt32Hash(f.CleanUpPathCrossPlatform(true).ToLowerInvariant(), tempMd5)).ToList(); - } - } - - foreach (string filePath in filePaths) - { - if (!File.Exists(filePath)) continue; - - using (var stream = File.OpenRead(filePath)) - { - byte[] fileData = new byte[stream.Length]; - stream.Read(fileData, 0, (int)stream.Length); - if (filePath.EndsWith(".xml", true, System.Globalization.CultureInfo.InvariantCulture)) - { - string text = System.Text.Encoding.UTF8.GetString(fileData); - text = text.Replace("\n", "").Replace("\r", "").Replace("\\","/"); - fileData = System.Text.Encoding.UTF8.GetBytes(text); - } - data.AddRange(fileData); - } - } - return md5.ComputeHash(data.ToArray()); - } - } - - public static bool IsModFilePathAllowed(ContentFile contentFile) - { - string path = contentFile.Path; - return IsModFilePathAllowed(path); - } - /// - /// Returns whether mods are allowed to install a file into the specified path. - /// Currently mods are only allowed to install files into the Mods folder. - /// The only exception to this rule is the Vanilla content package. - /// - /// - /// - public static bool IsModFilePathAllowed(string path) - { - if (GameMain.VanillaContent.Files.Any(f => string.Equals(System.IO.Path.GetFullPath(f.Path).CleanUpPath(), - System.IO.Path.GetFullPath(path).CleanUpPath(), - StringComparison.InvariantCultureIgnoreCase))) - { - //file is in vanilla package, this is allowed - return true; - } - - while (true) - { - string temp = Barotrauma.IO.Path.GetDirectoryName(path); - if (string.IsNullOrEmpty(temp)) { break; } - path = temp; - } - return path == "Mods"; - } - - /// - /// Returns all xml files from all the loaded content packages. - /// - public static IEnumerable GetAllContentFiles(IEnumerable contentPackages) - { - return contentPackages.SelectMany(f => f.Files).Select(f => f.Path).Where(p => p.EndsWith(".xml")); - } - - public static IEnumerable GetFilesOfType(IEnumerable contentPackages, ContentType type) - { - return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type); - } - public static IEnumerable GetFilesOfType(IEnumerable contentPackages, params ContentType[] types) - { - return contentPackages.SelectMany(f => f.Files).Where(f => types.Contains(f.Type)); - } - - public IEnumerable GetFilesOfType(ContentType type) - { - return Files.Where(f => f.Type == type).Select(f => f.Path); - } - - public static void AddPackage(ContentPackage newPackage) - { - if (corePackages.Concat(regularPackages).Any(p => p.Name.Equals(newPackage.Name, StringComparison.OrdinalIgnoreCase))) - { - DebugConsole.ThrowError($"Attempted to add \"{newPackage.Name}\" more than once!\n{Environment.StackTrace}"); - } - if (newPackage.IsCorePackage) - { - corePackages.AddOnMainThread(newPackage); - } - else - { - regularPackages.AddOnMainThread(newPackage); - } - } - - public static void RemovePackage(ContentPackage package) - { - if (package.IsCorePackage) { corePackages.RemoveOnMainThread(package); } - else { regularPackages.RemoveOnMainThread(package); } - } - - public static void LoadAll() - { - string folder = Folder; - if (!Directory.Exists(folder)) - { - try - { - Directory.CreateDirectory(folder); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to create directory \"" + folder + "\"", e); - return; - } - } - - IEnumerable files = Directory.GetFiles(folder, "*.xml"); - - corePackages.ClearOnMainThread(); - var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList(); - regularPackages.ClearOnMainThread(); - - foreach (string filePath in files) - { - var newPackage = new ContentPackage(filePath); - if (!newPackage.IsCorrupt) { AddPackage(newPackage); } - } - - IEnumerable modDirectories = Directory.GetDirectories("Mods"); - foreach (string modDirectory in modDirectories) - { - if (Barotrauma.IO.Path.GetFileName(modDirectory.TrimEnd(Barotrauma.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } - string modFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); - string copyingFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); - if (File.Exists(copyingFilePath)) - { - //this mod didn't clean up its copying file; assume it's corrupted and delete it - Directory.Delete(modDirectory, true); - } - else if (File.Exists(modFilePath)) - { - var newPackage = new ContentPackage(modFilePath); - if (!newPackage.IsCorrupt) - { - AddPackage(newPackage); - } - } - } - SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant())); - GameMain.Config?.SortContentPackages(); - } - - public static void SortContentPackages(Func order, bool refreshAll = false, GameSettings config = null) - { - var ordered = regularPackages - .OrderBy(p => order(p)) - .ThenBy(p => regularPackages.IndexOf(p)) - .ToList(); - regularPackages.ClearOnMainThread(); regularPackages.AddRangeOnMainThread(ordered); - (config ?? GameMain.Config)?.SortContentPackages(refreshAll); - } - - public void Delete() - { - try - { - if (IsCorePackage) - { - corePackages.RemoveOnMainThread(this); - if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); } - } - else - { - regularPackages.RemoveOnMainThread(this); - if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); } - } - GameMain.Config.SaveNewPlayerConfig(); - File.Delete(Path); - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to delete content package \"" + Name + "\".", e); - return; - } - } - } - - public class ContentFile - { - public string Path; - public ContentType Type; - - public ContentPackage ContentPackage; - - public ContentFile(string path, ContentType type, ContentPackage contentPackage = null) - { - Path = path.CleanUpPath(); - - Type = type; - ContentPackage = contentPackage; - } - - public override string ToString() - { - return Path; - } - } - -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index cd253c368..dfbf631f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -159,7 +159,7 @@ namespace Barotrauma return new string[][] { commands.SelectMany(c => c.names).ToArray(), - new string[0] + Array.Empty() }; })); @@ -169,7 +169,7 @@ namespace Barotrauma NewMessage("***************", Color.Cyan); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (string.IsNullOrEmpty(itemPrefab.Name)) continue; + if (itemPrefab.Name.IsNullOrEmpty()) { continue; } string text = $"- {itemPrefab.Name}"; if (itemPrefab.Tags.Any()) { @@ -194,20 +194,14 @@ namespace Barotrauma commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { - List characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).Select(f => f.Path).ToList(); - for (int i = 0; i < characterFiles.Count; i++) - { - characterFiles[i] = Path.GetFileNameWithoutExtension(characterFiles[i]).ToLowerInvariant(); - } - - foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) - { - characterFiles.Add(jobPrefab.Name); - } + string[] creatureAndJobNames = + CharacterPrefab.Prefabs.Select(p => p.Identifier.Value) + .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value)) + .ToArray(); return new string[][] { - characterFiles.ToArray(), + creatureAndJobNames.ToArray(), new string[] { "near", "inside", "outside", "cursor" } }; }, isCheat: true)); @@ -239,9 +233,9 @@ namespace Barotrauma List itemNames = new List(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - if (!itemNames.Contains(itemPrefab.Name)) + if (!itemNames.Contains(itemPrefab.Name.Value)) { - itemNames.Add(itemPrefab.Name); + itemNames.Add(itemPrefab.Name.Value); } } @@ -330,7 +324,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - PermissionPreset.List.Select(pp => pp.Name).ToArray() + PermissionPreset.List.Select(pp => pp.Name.Value).ToArray() }; })); @@ -521,7 +515,7 @@ namespace Barotrauma if (targetCharacter == null) { return; } targetCharacter.GodMode = !targetCharacter.GodMode; - NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on " + targetCharacter.Name), Color.White); + NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name, Color.White); }, () => { @@ -599,7 +593,7 @@ namespace Barotrauma } foreach (Character character in Character.CharacterList) { - if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName.Equals(args[0], StringComparison.OrdinalIgnoreCase)) + if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName == args[0]) { ThrowError(character.ID + ": " + character.Name.ToString()); } @@ -612,7 +606,7 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + a.Identifier == args[0]); if (afflictionPrefab == null) { ThrowError("Affliction \"" + args[0] + "\" not found."); @@ -650,7 +644,7 @@ namespace Barotrauma { return new string[][] { - AfflictionPrefab.List.Select(a => a.Name).ToArray(), + AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray(), new string[] { "1" }, Character.CharacterList.Select(c => c.Name).ToArray(), Enum.GetNames(typeof(LimbType)).ToArray() @@ -751,10 +745,10 @@ namespace Barotrauma commands.Add(new Command("triggerevent", "triggerevent [identifier]: Created a new event.", (string[] args) => { - List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList(); + List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList(); if (GameMain.GameSession?.EventManager != null && args.Length > 0) { - EventPrefab eventPrefab = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase)); + EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]); if (eventPrefab != null) { @@ -776,11 +770,11 @@ namespace Barotrauma NewMessage("Failed to trigger event", Color.Red); }, isCheat: true, getValidArgs: () => { - List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList(); + List eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList(); return new[] { - eventPrefabs.Select(prefab => prefab.Identifier).Distinct().ToArray() + eventPrefabs.Select(prefab => prefab.Identifier).Distinct().Select(id => id.Value).ToArray() }; })); @@ -792,7 +786,7 @@ namespace Barotrauma return; } - string skillIdentifier = args[0]; + Identifier skillIdentifier = args[0].ToIdentifier(); string levelString = args[1]; Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : Character.Controlled; @@ -807,7 +801,7 @@ namespace Barotrauma if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax) { if (isMax) { level = 100; } - if (skillIdentifier.Equals("all", StringComparison.OrdinalIgnoreCase)) + if (skillIdentifier == "all") { foreach (Skill skill in character.Info.Job.Skills) { @@ -829,7 +823,7 @@ namespace Barotrauma { return new[] { - Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier).ToArray() ?? new string[0], + Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty(), new[]{ "max" }, Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), }; @@ -848,7 +842,7 @@ namespace Barotrauma if (character != null) { TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => - c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + c.Identifier == args[0] || c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { @@ -864,12 +858,12 @@ namespace Barotrauma List talentNames = new List(); foreach (TalentPrefab talent in TalentPrefab.TalentPrefabs) { - talentNames.Add(talent.DisplayName); + talentNames.Add(talent.DisplayName.Value); } return new string[][] { - talentNames.ToArray(), + talentNames.Select(id => id).ToArray(), Character.CharacterList.Select(c => c.Name).Distinct().ToArray() }; }, isCheat: true)); @@ -892,7 +886,7 @@ namespace Barotrauma ThrowError($"Failed to find the job \"{args[0]}\"."); return; } - if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + if (!TalentTree.JobTalentTrees.TryGet(job.Identifier, out TalentTree talentTree)) { ThrowError($"No talents configured for the job \"{args[0]}\"."); return; @@ -918,7 +912,7 @@ namespace Barotrauma () => { List availableArgs = new List() { "All" }; - availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name)); + availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name.Value)); return new string[][] { availableArgs.ToArray(), @@ -979,7 +973,7 @@ namespace Barotrauma { NewMessage("Level seed: " + Level.Loaded.Seed); NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier); - NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none") + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none")); + NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier())); NewMessage("Mirrored: " + Level.Loaded.Mirrored); NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y); NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); @@ -1066,7 +1060,7 @@ namespace Barotrauma Character character = FindMatchingCharacter(args, false); if (character == null) { return; } - Entity.Spawner?.AddToRemoveQueue(character); + Entity.Spawner?.AddEntityToRemoveQueue(character); }, () => { @@ -1096,17 +1090,17 @@ namespace Barotrauma IEnumerable TestLevels() { SubmarineInfo selectedSub = null; - string subName = GameMain.Config.QuickStartSubmarineName; - if (!string.IsNullOrEmpty(subName)) + Identifier subName = GameSettings.CurrentConfig.QuickStartSub; + if (subName != Identifier.Empty) { - selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(subName, StringComparison.OrdinalIgnoreCase)); + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); } int count = 0; while (true) { var gamesession = new GameSession( - SubmarineInfo.SavedSubmarines.GetRandom(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), + SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)), GameModePreset.DevSandbox); string seed = ToolBox.RandomSeed(16); gamesession.StartRound(seed); @@ -1188,7 +1182,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)) is { } faction) + if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == args[0]) is { } faction) { if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation)) { @@ -1210,7 +1204,7 @@ namespace Barotrauma } }, () => { - return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier).ToArray() }; + return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray() }; }, true)); commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => @@ -1266,7 +1260,7 @@ namespace Barotrauma return; } - var upgradePrefab = UpgradePrefab.Find(args[0]); + var upgradePrefab = UpgradePrefab.Find(args[0].ToIdentifier()); if (upgradePrefab == null) { @@ -1301,7 +1295,7 @@ namespace Barotrauma foreach (MapEntity targetItem in targetItems) { - Upgrade existingUpgrade = targetItem.GetUpgrade(args[0]); + Upgrade existingUpgrade = targetItem.GetUpgrade(args[0].ToIdentifier()); if (!(targetItem is ISerializableEntity sEntity)) { continue; } @@ -1333,7 +1327,7 @@ namespace Barotrauma { return new[] { - UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray() + UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray() }; }, true)); @@ -1362,11 +1356,11 @@ namespace Barotrauma foreach (UpgradeCategory category in UpgradeCategory.Categories) { - if (!string.IsNullOrWhiteSpace(categoryIdentifier) && !category.Identifier.Equals(categoryIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; } + if (!string.IsNullOrWhiteSpace(categoryIdentifier) && category.Identifier != categoryIdentifier) { continue; } foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) { if (!prefab.UpgradeCategories.Contains(category)) { continue; } - if (!string.IsNullOrWhiteSpace(prefabIdentifier) && !prefab.Identifier.Equals(prefabIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; } + if (!string.IsNullOrWhiteSpace(prefabIdentifier) && prefab.Identifier != prefabIdentifier) { continue; } int targetLevel = prefab.MaxLevel - upgradeManager.GetRealUpgradeLevel(prefab, category); for (int i = 0; i < targetLevel; i++) @@ -1382,8 +1376,8 @@ namespace Barotrauma { return new[] { - UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().ToArray(), - UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray() + UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray(), + UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray() }; }, true)); @@ -1404,7 +1398,7 @@ namespace Barotrauma commands.Add(new Command("oxygen|air", "oxygen/air: Replenishes the oxygen levels in every room to 100%.", (string[] args) => { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.OxygenPercentage = 100.0f; } @@ -1432,7 +1426,7 @@ namespace Barotrauma c.SetAllDamage(200.0f, 0.0f, 0.0f); } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.BallastFlora?.Kill(); } @@ -1597,14 +1591,14 @@ namespace Barotrauma if (pumps.Any()) { - BallastFloraPrefab prefab = string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument); + BallastFloraPrefab prefab = string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument.ToIdentifier()); if (prefab == null) { ThrowError($"No such behavior: {secondaryArgument}"); return; } - Pump random = pumps.GetRandom(); + Pump random = pumps.GetRandomUnsynced(); random.InfectBallast(prefab.Identifier, allowMultiplePerShip: true); NewMessage($"Infected {random.Name} with {prefab.Identifier} in {random.Item.CurrentHull.DisplayName}.", Color.Green); return; @@ -1617,7 +1611,7 @@ namespace Barotrauma { if (int.TryParse(secondaryArgument, out int value)) { - foreach (Hull hull in Hull.hullList.Where(h => h.BallastFlora != null)) + foreach (Hull hull in Hull.HullList.Where(h => h.BallastFlora != null)) { BallastFloraBehavior bs = hull.BallastFlora; bs.GrowthWarps = value; @@ -1632,7 +1626,7 @@ namespace Barotrauma }, isCheat: true, getValidArgs: () => { string[] primaries = { "infect", "growthwarp" }; - string[] identifiers = BallastFloraPrefab.Prefabs.Select(bfp => bfp.Identifier).Distinct().ToArray(); + string[] identifiers = BallastFloraPrefab.Prefabs.Select(bfp => bfp.Identifier).Distinct().Select(i => i.Value).ToArray(); return new[] { primaries, identifiers }; })); @@ -1660,8 +1654,10 @@ namespace Barotrauma commands.Add(new Command("verboselogging", "verboselogging: Toggle verbose console logging on/off. When on, additional debug information is written to the debug console.", (string[] args) => { - GameSettings.VerboseLogging = !GameSettings.VerboseLogging; - NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); + var config = GameSettings.CurrentConfig; + config.VerboseLogging = !GameSettings.CurrentConfig.VerboseLogging; + GameSettings.SetCurrentConfig(config); + NewMessage((GameSettings.CurrentConfig.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); }, isCheat: false)); commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); })); @@ -1671,7 +1667,7 @@ namespace Barotrauma if (args.Length > 0) { string packageName = string.Join(" ", args); - var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase)); + var package = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase)); if (package == null) { ThrowError("Content package \"" + packageName + "\" not found."); @@ -1683,14 +1679,14 @@ namespace Barotrauma } else { - GameMain.Config.AllEnabledPackages.First().CalculateHash(logging: true); + ContentPackageManager.EnabledPackages.Core.CalculateHash(logging: true); } }, () => { return new string[][] { - GameMain.Config.AllEnabledPackages.Select(cp => cp.Name).ToArray() + ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name).ToArray() }; })); @@ -1776,9 +1772,9 @@ namespace Barotrauma msg += "\nBalance: " + location.StoreCurrentBalance; msg += "\nPrice modifier: " + location.StorePriceModifier + "%"; msg += "\nDaily specials:"; - location.DailySpecials.ForEach(i => msg += "\n - " + i.Name); + location.DailySpecials.ForEach(i => msg += "\n - " + i.Name.Value); msg += "\nRequested goods:"; - location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name); + location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name.Value); NewMessage(msg); } else @@ -2051,7 +2047,7 @@ namespace Barotrauma switch (args[1].ToLowerInvariant()) { case "inside": - spawnPoint = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); + spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub); break; case "outside": spawnPoint = WayPoint.GetRandom(SpawnType.Enemy); @@ -2098,7 +2094,7 @@ namespace Barotrauma } catch { - DebugConsole.ThrowError($"\"{args[2]}\" is not a valid team id."); + ThrowError($"\"{args[2]}\" is not a valid team id."); } } @@ -2106,8 +2102,8 @@ namespace Barotrauma if (human) { - var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.Server) : 0; - CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant); + var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0; + CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant); spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8)); if (GameMain.GameSession != null) { @@ -2121,7 +2117,7 @@ namespace Barotrauma } else { - if (CharacterPrefab.FindBySpeciesName(args[0]) != null) + if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) != null) { Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8)); } @@ -2143,7 +2139,7 @@ namespace Barotrauma if (itemPrefab == null) { errorMsg = "Item \"" + itemNameOrId + "\" not found!"; - var matching = ItemPrefab.Prefabs.Find(me => me.Name.ToLowerInvariant().StartsWith(itemNameOrId) && me is ItemPrefab); + var matching = ItemPrefab.Prefabs.Find(me => me.Name.StartsWith(itemNameOrId, StringComparison.OrdinalIgnoreCase) && me is ItemPrefab); if (matching != null) { errorMsg += $" Did you mean \"{matching.Name}\"?"; @@ -2204,7 +2200,7 @@ namespace Barotrauma } else { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnPos.Value); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value); } } else if (spawnInventory != null) @@ -2217,7 +2213,7 @@ namespace Barotrauma } else { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); } static void onItemSpawned(Item item) @@ -2246,6 +2242,9 @@ namespace Barotrauma NewMessage(command, color.Value, isCommand: true, isError: false); } + public static void NewMessage(LocalizedString msg, Color? color = null, bool debugOnly = false) + => NewMessage(msg.Value, color, debugOnly); + public static void NewMessage(string msg, Color? color = null, bool debugOnly = false) { color ??= Color.White; @@ -2284,7 +2283,7 @@ namespace Barotrauma #if CLIENT activeQuestionText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, 0), listBox.Content.RectTransform), - " >>" + question, font: GUI.SmallFont, wrap: true) + " >>" + question, font: GUIStyle.SmallFont, wrap: true) { CanBeFocused = false, TextColor = Color.Cyan @@ -2352,14 +2351,21 @@ namespace Barotrauma public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); + public static void Log(LocalizedString message) => Log(message?.Value); + public static void Log(string message) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { NewMessage(message, Color.Gray); } } + public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) + { + ThrowError(error.Value); + } + public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { if (e != null) @@ -2383,7 +2389,7 @@ namespace Barotrauma { error += "\n" + Environment.StackTrace.CleanupStackTrace(); } - System.Diagnostics.Debug.WriteLine(error); + System.Diagnostics.Debug.WriteLine($"ThrowError: {error}"); #if CLIENT if (createMessageBox) @@ -2408,11 +2414,6 @@ namespace Barotrauma #if CLIENT private static IEnumerable CreateMessageBox(string errorMsg) { - while (GUI.Style == null) - { - yield return null; - } - new GUIMessageBox(TextManager.Get("Error"), errorMsg); yield return CoroutineStatus.Success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs index 2d7792808..2b0cd4756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -1,102 +1,92 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { - class DecalManager + public class GrimeSprite : Prefab { - public PrefabCollection Prefabs { get; private set; } - - public readonly List GrimeSprites = new List(); - private Dictionary> grimeSpritesByFile = new Dictionary>(); - - public DecalManager() + public GrimeSprite(Sprite spr, DecalsFile file, int indexInFile) : base(file, $"{nameof(GrimeSprite)}{indexInFile}".ToIdentifier()) { - Prefabs = new PrefabCollection(); - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals)) - { - LoadFromFile(configFile); - } + Sprite = spr; + IndexInFile = indexInFile; } - public void LoadFromFile(ContentFile configFile) + public readonly int IndexInFile; + + public Sprite Sprite { get; private set; } + + public override void Dispose() + { + Sprite?.Remove(); Sprite = null; + } + } + + static class DecalManager + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public static int GrimeSpriteCount { get; private set; } = 0; + + public static readonly PrefabCollection GrimeSprites = new PrefabCollection( + onAdd: (sprite, b) => GrimeSpriteCount = Math.Max(GrimeSpriteCount, sprite.IndexInFile+1), + onRemove: (s) => + GrimeSpriteCount = GrimeSprites.AllPrefabs + .SelectMany(kvp => kvp.Value) + .Where(p => p != s).Select(p => p.IndexInFile+1).MaxOrNull() ?? 0, + onSort: null, onAddOverrideFile: null, onRemoveOverrideFile: null); + + public static void LoadFromFile(DecalsFile configFile) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { return; } - if (grimeSpritesByFile.ContainsKey(configFile.Path)) - { - foreach (Sprite sprite in grimeSpritesByFile[configFile.Path]) - { - sprite.Remove(); - GrimeSprites.Remove(sprite); - } - grimeSpritesByFile.Remove(configFile.Path); - } - bool allowOverriding = false; - var mainElement = doc.Root; + var mainElement = doc.Root.FromPackage(configFile.ContentPackage); if (doc.Root.IsOverride()) { - mainElement = doc.Root.FirstElement(); + mainElement = mainElement.FirstElement(); allowOverriding = true; } - foreach (XElement sourceElement in mainElement.Elements()) + int grimeIndex = 0; + foreach (var sourceElement in mainElement.Elements()) { var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; + bool isOverride = allowOverriding || sourceElement.IsOverride(); string name = element.Name.ToString().ToLowerInvariant(); switch (name) { case "grime": - if (!grimeSpritesByFile.ContainsKey(configFile.Path)) - { - grimeSpritesByFile.Add(configFile.Path, new List()); - } - var grimeSprite = new Sprite(element); - GrimeSprites.Add(grimeSprite); - grimeSpritesByFile[configFile.Path].Add(grimeSprite); + GrimeSprites.Add(new GrimeSprite(new Sprite(element), configFile, grimeIndex), isOverride); + grimeIndex++; break; default: - if (Prefabs.ContainsKey(name)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " + - "Use tags to override prefabs."); - continue; - } - } - var newPrefab = new DecalPrefab(element, configFile); - Prefabs.Add(newPrefab, allowOverriding || sourceElement.IsOverride()); - newPrefab.CalculatePrefabUIntIdentifier(Prefabs); + var prefab = new DecalPrefab(element, configFile); + Prefabs.Add(prefab, isOverride); break; } } } - public void RemoveByFile(string filePath) + public static void RemoveByFile(DecalsFile configFile) { - Prefabs.RemoveByFile(filePath); - if (grimeSpritesByFile.ContainsKey(filePath)) - { - foreach (Sprite sprite in grimeSpritesByFile[filePath]) - { - sprite.Remove(); - GrimeSprites.Remove(sprite); - } - grimeSpritesByFile.Remove(filePath); - } + Prefabs.RemoveByFile(configFile); + GrimeSprites.RemoveByFile(configFile); } - public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) + public static void SortAll() + { + Prefabs.SortAll(); + GrimeSprites.SortAll(); + } + + public static Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null) { string lowerCaseDecalName = decalName.ToLowerInvariant(); if (!Prefabs.ContainsKey(lowerCaseDecalName)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs index bc0551501..8d8a0d6b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalPrefab.cs @@ -5,29 +5,11 @@ using System.Xml.Linq; namespace Barotrauma { - class DecalPrefab : IPrefab, IHasUintIdentifier, IDisposable + class DecalPrefab : PrefabWithUintIdentifier { - public readonly string Name; + public string Name => Identifier.Value; - public string OriginalName { get { return Name; } } - - public string Identifier - { - get; - private set; - } - - /// - /// Unique identifier that's generated by hashing the prefab's string identifier. - /// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer. - /// - public uint UIntIdentifier { get; set; } - - public string FilePath { get; private set; } - - public ContentPackage ContentPackage { get; private set; } - - public void Dispose() + public override void Dispose() { foreach (Sprite spr in Sprites) { @@ -44,19 +26,11 @@ namespace Barotrauma public readonly float FadeOutTime; public readonly float FadeInTime; - public DecalPrefab(XElement element, ContentFile file) + public DecalPrefab(ContentXElement element, DecalsFile file) : base(file, new Identifier(element.Name.LocalName)) { - Name = element.Name.ToString(); - - Identifier = Name.ToLowerInvariant(); - - FilePath = file.Path; - - ContentPackage = file.ContentPackage; - Sprites = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index af68ef218..3feeabf1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -26,7 +26,7 @@ namespace Barotrauma public override string ToString() { - return "ArtifactEvent (" + (itemPrefab == null ? "null" : itemPrefab.Name) + ")"; + return $"ArtifactEvent ({(itemPrefab == null ? "null" : itemPrefab.Name)})"; } public ArtifactEvent(EventPrefab prefab) @@ -56,7 +56,7 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { spawnPos = Level.Loaded.GetRandomItemPos( - (Rand.Value(Rand.RandSync.Server) < 0.5f) ? + (Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : Level.PositionType.Cave | Level.PositionType.Ruin, 500.0f, 10000.0f, 30.0f, SpawnPosFilter); @@ -79,7 +79,7 @@ namespace Barotrauma if (itemContainer.Combine(item, user: null)) break; // Placement successful } - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Initialized ArtifactEvent (" + item.Name + ")", Color.White); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs index 6e770f730..454867de5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/AfflictionAction.cs @@ -7,19 +7,19 @@ namespace Barotrauma { class AfflictionAction : EventAction { - [Serialize("", true)] - public string Affliction { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Affliction { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Strength { get; set; } - [Serialize(LimbType.None, true)] + [Serialize(LimbType.None, IsPropertySaveable.Yes)] public LimbType LimbType { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public AfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public AfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; @@ -36,7 +36,7 @@ namespace Barotrauma public override void Update(float deltaTime) { if (isFinished) { return; } - var afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(p => p.Identifier.Equals(Affliction, StringComparison.InvariantCultureIgnoreCase)); + var afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(p => p.Identifier == Affliction); if (afflictionPrefab != null) { var targets = ParentEvent.GetTargets(TargetTag); @@ -44,14 +44,28 @@ namespace Barotrauma { if (target != null && target is Character character) { - var limb = LimbType != LimbType.None ? character.AnimController.GetLimb(LimbType) : null; - if (Strength > 0.0f) + if (LimbType != LimbType.None) { - character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength)); + var limb = character.AnimController.GetLimb(LimbType); + if (Strength > 0.0f) + { + character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength)); + } + else if (Strength < 0.0f) + { + character.CharacterHealth.ReduceAfflictionOnLimb(limb, Affliction, -Strength); + } } - else if (Strength < 0.0f) + else { - character.CharacterHealth.ReduceAffliction(limb, Affliction, -Strength); + if (Strength > 0.0f) + { + character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength)); + } + else if (Strength < 0.0f) + { + character.CharacterHealth.ReduceAfflictionOnAllLimbs(Affliction, -Strength); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs index 5eb38c547..05f27d592 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/BinaryOptionAction.cs @@ -11,9 +11,9 @@ namespace Barotrauma public SubactionGroup Failure = null; protected bool? succeeded = null; - public BinaryOptionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public BinaryOptionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - foreach (XElement elem in element.Elements()) + foreach (var elem in element.Elements()) { string elemName = elem.Name.LocalName; if (elemName.Equals("success", StringComparison.InvariantCultureIgnoreCase)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs index b5ff3c16a..1aca440b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -8,23 +8,23 @@ namespace Barotrauma { internal class CheckAfflictionAction : BinaryOptionAction { - [Serialize("", true)] - public string Identifier { get; set; } = ""; + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", true)] - public string TargetTag { get; set; } = ""; + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } = Identifier.Empty; - [Serialize(LimbType.None, true, "Only check afflictions on the specified limb type")] + [Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")] public LimbType TargetLimb { get; set; } - [Serialize(true, true, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] + [Serialize(true, IsPropertySaveable.Yes, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] public bool AllowLimbAfflictions { get; set; } - public CheckAfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CheckAfflictionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { - if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(TargetTag)) { return false; } + if (Identifier.IsEmpty || TargetTag.IsEmpty) { return false; } List targets = ParentEvent.GetTargets(TargetTag).OfType().ToList(); foreach (var target in targets) @@ -42,7 +42,7 @@ namespace Barotrauma return limbType == TargetLimb || true; }); - if (afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } + if (afflictions.Any(a => a.Identifier == Identifier)) { return true; } } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index fd60cddb2..92ff33999 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -1,21 +1,22 @@ #nullable enable using System; +using System.Linq; using System.Xml.Linq; namespace Barotrauma { class CheckDataAction : BinaryOptionAction { - [Serialize("", true)] - public string Identifier { get; set; } = null!; + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } = Identifier.Empty; - [Serialize("", true)] - public string Condition { get; set; } = null!; + [Serialize("", IsPropertySaveable.Yes)] + public string Condition { get; set; } = ""; - [Serialize(false, true, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] + [Serialize(false, IsPropertySaveable.Yes, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] public bool ForceString { get; set; } - [Serialize(false, true, "Performs the comparison against a metadata by identifier instead of a constant value")] + [Serialize(false, IsPropertySaveable.Yes, "Performs the comparison against a metadata by identifier instead of a constant value")] public bool CheckAgainstMetadata { get; set; } protected object? value2; @@ -23,11 +24,11 @@ namespace Barotrauma protected PropertyConditional.OperatorType Operator { get; set; } - public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (string.IsNullOrEmpty(Condition)) { - Condition = element.GetAttributeString("value", string.Empty); + Condition = element.GetAttributeString("value", string.Empty)!; if (string.IsNullOrEmpty(Condition)) { DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element})."); @@ -43,10 +44,8 @@ namespace Barotrauma string value = Condition; if (splitString.Length > 0) { - for (int i = 1; i < splitString.Length; i++) - { - value = splitString[i] + (i > 1 && i < splitString.Length ? " " : ""); - } + #warning Is this correct? + value = string.Join(" ", splitString.Skip(1)); } else { @@ -61,7 +60,7 @@ namespace Barotrauma if (CheckAgainstMetadata) { object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier); - object? metadata2 = campaignMode.CampaignMetadata.GetValue(value); + object? metadata2 = campaignMode.CampaignMetadata.GetValue(value.ToIdentifier()); if (metadata1 == null || metadata2 == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs index b1ba89a2a..b7874d26a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckItemAction.cs @@ -6,22 +6,22 @@ namespace Barotrauma { class CheckItemAction : BinaryOptionAction { - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string ItemIdentifiers { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string ItemTags { get; set; } - private readonly string[] itemIdentifierSplit; - private readonly string[] itemTags; + private readonly Identifier[] itemIdentifierSplit; + private readonly Identifier[] itemTags; - public CheckItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public CheckItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - itemIdentifierSplit = ItemIdentifiers.Split(','); - itemTags = ItemTags.Split(","); + itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers(); + itemTags = ItemTags.Split(",").ToIdentifiers(); } protected override bool? DetermineSuccess() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs index f19e759a9..8d3141adf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -4,10 +4,10 @@ namespace Barotrauma { class CheckMoneyAction : BinaryOptionAction { - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } - public CheckMoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs index b34f80de9..5f6f19e47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckReputationAction.cs @@ -7,10 +7,10 @@ namespace Barotrauma { class CheckReputationAction : CheckDataAction { - [Serialize(ReputationAction.ReputationType.None, true)] + [Serialize(ReputationAction.ReputationType.None, IsPropertySaveable.Yes)] public ReputationAction.ReputationType TargetType { get; set; } - public CheckReputationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CheckReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override float GetFloat(CampaignMode campaignMode) { @@ -18,7 +18,7 @@ namespace Barotrauma { case ReputationAction.ReputationType.Faction: { - Faction? faction = campaignMode.Factions.Find(f => f.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); + Faction? faction = campaignMode.Factions.Find(f => f.Prefab.Identifier == Identifier); if (faction != null) { return faction.Reputation.Value; } break; } @@ -54,7 +54,7 @@ namespace Barotrauma } return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckReputationAction)} -> (Type: {TargetType.ColorizeObject()}, " + - $"{(string.IsNullOrWhiteSpace(Identifier) ? string.Empty : $"Identifier: {Identifier.ColorizeObject()}, ")}" + + $"{(Identifier.IsEmpty ? string.Empty : $"Identifier: {Identifier.ColorizeObject()}, ")}" + $"Success: {succeeded.ColorizeObject()}, Expression: {condition})"; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs index 73263a685..4de2f7c4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs @@ -4,12 +4,12 @@ namespace Barotrauma { class ClearTagAction : EventAction { - [Serialize("", true)] - public string Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Tag { get; set; } private bool isFinished; - public ClearTagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public ClearTagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goToLabel) => isFinished; @@ -22,7 +22,7 @@ namespace Barotrauma { if (isFinished) { return; } - if (!string.IsNullOrWhiteSpace(Tag)) + if (!Tag.IsEmpty) { ParentEvent.RemoveTag(Tag); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs index c42a96330..26320d4f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CombatAction.cs @@ -7,25 +7,25 @@ namespace Barotrauma { class CombatAction : EventAction { - [Serialize(AIObjectiveCombat.CombatMode.Offensive, true)] + [Serialize(AIObjectiveCombat.CombatMode.Offensive, IsPropertySaveable.Yes)] public AIObjectiveCombat.CombatMode CombatMode { get; set; } - [Serialize(false, true, description: "Did this NPC start the fight (as an aggressor)?")] + [Serialize(false, IsPropertySaveable.Yes, description: "Did this NPC start the fight (as an aggressor)?")] public bool IsInstigator { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, true)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] public AIObjectiveCombat.CombatMode GuardReaction { get; set; } - [Serialize(AIObjectiveCombat.CombatMode.None, true)] + [Serialize(AIObjectiveCombat.CombatMode.None, IsPropertySaveable.Yes)] public AIObjectiveCombat.CombatMode WitnessReaction { get; set; } - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize("", true)] - public string EnemyTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier EnemyTag { get; set; } - [Serialize(120.0f, true)] + [Serialize(120.0f, IsPropertySaveable.Yes)] public float CoolDown { get; set; } private bool isFinished = false; @@ -33,7 +33,7 @@ namespace Barotrauma private IEnumerable affectedNpcs = null; - public CombatAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public CombatAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override void Update(float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 9f53430a9..00c439419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -26,37 +26,37 @@ namespace Barotrauma /// const float BlockOtherConversationsDuration = 5.0f; - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Text { get; set; } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int DefaultOption { get; set; } - [Serialize("", true)] - public string SpeakerTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SpeakerTag { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool WaitForInteraction { get; set; } - [Serialize("", true, "Tag to assign to whoever invokes the conversation")] - public string InvokerTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, "Tag to assign to whoever invokes the conversation")] + public Identifier InvokerTag { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool FadeToBlack { get; set; } - [Serialize(true, true, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] + [Serialize(true, IsPropertySaveable.Yes, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] public bool EndEventIfInterrupted { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string EventSprite { get; set; } - [Serialize(DialogTypes.Regular, true)] + [Serialize(DialogTypes.Regular, IsPropertySaveable.Yes)] public DialogTypes DialogType { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool ContinueConversation { get; set; } private Character speaker; @@ -79,12 +79,12 @@ namespace Barotrauma private bool interrupt; - public ConversationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { actionCount++; Identifier = actionCount; Options = new List(); - foreach (XElement elem in element.Elements()) + foreach (var elem in element.Elements()) { if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase)) { @@ -230,7 +230,7 @@ namespace Barotrauma return; } - if (!string.IsNullOrEmpty(SpeakerTag)) + if (!SpeakerTag.IsEmpty) { if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; } speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character; @@ -254,7 +254,7 @@ namespace Barotrauma #if CLIENT speaker.SetCustomInteract( TryStartConversation, - TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameMain.Config.KeyBindText(InputType.Use))); + TextManager.GetWithVariable("CampaignInteraction.Talk", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else speaker.SetCustomInteract( TryStartConversation, @@ -286,7 +286,7 @@ namespace Barotrauma private bool ShouldInterrupt() { IEnumerable targets = Enumerable.Empty(); - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); if (!targets.Any()) { return true; } @@ -294,7 +294,7 @@ namespace Barotrauma if (speaker != null) { - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { if (targets.All(t => Vector2.DistanceSquared(t.WorldPosition, speaker.WorldPosition) > InterruptDistance * InterruptDistance)) { return true; } } @@ -324,7 +324,7 @@ namespace Barotrauma private void TryStartConversation(Character speaker, Character targetCharacter = null) { IEnumerable targets = Enumerable.Empty(); - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e)); if (!targets.Any() || IsBlockedByAnotherConversation(targets)) { return; } @@ -335,8 +335,7 @@ namespace Barotrauma prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); humanAI.SetForcedOrder( - Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), - option: string.Empty, orderGiver: null); + new Order(OrderPrefab.Prefabs["wait"], Barotrauma.Identifier.Empty, null, orderGiver: null)); if (targets.Any()) { Entity closestTarget = null; @@ -357,7 +356,7 @@ namespace Barotrauma } } - if (targetCharacter != null && !string.IsNullOrWhiteSpace(InvokerTag)) + if (targetCharacter != null && !InvokerTag.IsEmpty) { ParentEvent.AddTarget(InvokerTag, targetCharacter); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index 083e54b3a..edd2baefa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -28,12 +28,12 @@ namespace Barotrauma } } - public SubactionGroup(ScriptedEvent scriptedEvent, XElement elem) + public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem) { Text = elem.Attribute("text")?.Value ?? ""; Actions = new List(); EndConversation = elem.GetAttributeBool("endconversation", false); - foreach (XElement e in elem.Elements()) + foreach (var e in elem.Elements()) { if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { @@ -100,7 +100,7 @@ namespace Barotrauma public readonly ScriptedEvent ParentEvent; - public EventAction(ScriptedEvent parentEvent, XElement element) + public EventAction(ScriptedEvent parentEvent, ContentXElement element) { ParentEvent = parentEvent; SerializableProperty.DeserializeProperties(this, element); @@ -132,7 +132,7 @@ namespace Barotrauma public virtual void Update(float deltaTime) { } - public static EventAction Instantiate(ScriptedEvent scriptedEvent, XElement element) + public static EventAction Instantiate(ScriptedEvent scriptedEvent, ContentXElement element) { Type actionType = null; try @@ -146,7 +146,7 @@ namespace Barotrauma return null; } - ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(XElement) }); + ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) }); try { return constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs index 8ef70a6a6..0adf73e69 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/FireAction.cs @@ -8,13 +8,13 @@ namespace Barotrauma { class FireAction : EventAction { - [Serialize(10.0f, true)] + [Serialize(10.0f, IsPropertySaveable.Yes)] public float Size { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public FireAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public FireAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index 2da290284..12290178a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -6,18 +6,18 @@ namespace Barotrauma { class GiveSkillExpAction : EventAction { - [Serialize("", true)] - public string Skill { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Skill { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Amount { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public GiveSkillExpAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public GiveSkillExpAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrEmpty(TargetTag)) + if (TargetTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": GiveSkillExpAction without a target tag (the action needs to know whose skill to check)."); } @@ -40,7 +40,7 @@ namespace Barotrauma var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); foreach (var target in targets) { - target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount); + target.Info?.IncreaseSkillLevel(Skill, Amount); } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs index 44d1895dc..6f186b127 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GoTo.cs @@ -4,10 +4,10 @@ namespace Barotrauma { class GoTo : EventAction { - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Name { get; set; } - public GoTo(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs index aecef1c64..47ff662f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GodModeAction.cs @@ -7,13 +7,13 @@ namespace Barotrauma { class GodModeAction : EventAction { - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool Enabled { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public GodModeAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public GodModeAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs index 3c81f85d3..a95a7dea7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/Label.cs @@ -4,10 +4,10 @@ namespace Barotrauma { class Label : EventAction { - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Name { get; set; } - public Label(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public Label(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs index 301ca0f74..da7661209 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs @@ -8,33 +8,33 @@ namespace Barotrauma { class MissionAction : EventAction { - [Serialize("", true)] - public string MissionIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionIdentifier { get; set; } - [Serialize("", true)] - public string MissionTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier MissionTag { get; set; } - [Serialize("", true, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] + [Serialize("", IsPropertySaveable.Yes, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")] public string LocationType { get; set; } - [Serialize(0, true, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] + [Serialize(0, IsPropertySaveable.Yes, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")] public int MinLocationDistance { get; set; } - [Serialize(true, true, description: "If true, the mission has to be unlocked in a location further on the campaign map.")] + [Serialize(true, IsPropertySaveable.Yes, description: "If true, the mission has to be unlocked in a location further on the campaign map.")] public bool UnlockFurtherOnMap { get; set; } - [Serialize(false, true, description: "If true, a suitable location is forced on the map if one isn't found.")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, a suitable location is forced on the map if one isn't found.")] public bool CreateLocationIfNotFound { get; set; } private bool isFinished; - public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public MissionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrEmpty(MissionIdentifier) && string.IsNullOrEmpty(MissionTag)) + if (MissionIdentifier.IsEmpty && MissionTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured."); } - if (!string.IsNullOrEmpty(MissionIdentifier) && !string.IsNullOrEmpty(MissionTag)) + if (!MissionIdentifier.IsEmpty && !MissionTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored."); } @@ -63,18 +63,18 @@ namespace Barotrauma var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet()); if (emptyLocation != null) { - emptyLocation.ChangeType(Barotrauma.LocationType.List.Find(lt => lt.Identifier.Equals(LocationType, StringComparison.OrdinalIgnoreCase))); + emptyLocation.ChangeType(Barotrauma.LocationType.Prefabs[LocationType]); unlockLocation = emptyLocation; } } if (unlockLocation != null) { - if (!string.IsNullOrEmpty(MissionIdentifier)) + if (!MissionIdentifier.IsEmpty) { prefab = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier); } - else if (!string.IsNullOrEmpty(MissionTag)) + else if (!MissionTag.IsEmpty) { prefab = unlockLocation.UnlockMissionByTag(MissionTag); } @@ -87,7 +87,7 @@ namespace Barotrauma DebugConsole.NewMessage($"Unlocked mission \"{prefab.Name}\" in the location \"{unlockLocation.Name}\"."); #if CLIENT new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name), - new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) + Array.Empty(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) { IconColor = prefab.IconColor }; @@ -118,7 +118,7 @@ namespace Barotrauma private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet checkedLocations) { var campaign = GameMain.GameSession.GameMode as CampaignMode; - if (currLocation.Type.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase) && currDistance >= MinLocationDistance && + if (currLocation.Type.Identifier == locationType && currDistance >= MinLocationDistance && (!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X)) { return currLocation; @@ -136,7 +136,7 @@ namespace Barotrauma public override string ToDebugString() { - return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(string.IsNullOrEmpty(MissionIdentifier) ? MissionTag : MissionIdentifier)})"; + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MissionAction)} -> ({(MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier)})"; } #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index 5dfe8e9f0..ae7273383 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -5,9 +5,9 @@ namespace Barotrauma { class MoneyAction : EventAction { - public MoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public MoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } private bool isFinished; @@ -28,7 +28,7 @@ namespace Barotrauma if (GameMain.GameSession?.GameMode is CampaignMode campaign) { campaign.Money += Amount; - GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value); #if SERVER (campaign as MultiPlayerCampaign).LastUpdateID++; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index b7c31e334..d4a5f8ed7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -7,18 +7,18 @@ namespace Barotrauma { class NPCChangeTeamAction : EventAction { - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize(CharacterTeamType.None, true)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes)] public CharacterTeamType TeamTag { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool AddToCrew { get; set; } private bool isFinished = false; - public NPCChangeTeamAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private List affectedNpcs = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index eaa494848..7cc2f28ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -8,18 +8,18 @@ namespace Barotrauma { class NPCFollowAction : EventAction { - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool Follow { get; set; } private bool isFinished = false; - public NPCFollowAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public NPCFollowAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private List affectedNpcs = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs index 751731fbc..aa2bee231 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs @@ -6,16 +6,16 @@ namespace Barotrauma { class NPCWaitAction : EventAction { - [Serialize("", true)] - public string NPCTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier NPCTag { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool Wait { get; set; } private bool isFinished = false; - public NPCWaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public NPCWaitAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private IEnumerable affectedNpcs; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index f08ec8423..3ab365409 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -6,10 +6,10 @@ namespace Barotrauma { class RNGAction : BinaryOptionAction { - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Chance { get; set; } - public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public RNGAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { if (Chance >= 1.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 2d1e1e4d7..d39a5e666 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -7,20 +7,20 @@ namespace Barotrauma { class RemoveItemAction : EventAction { - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - [Serialize("", true)] - public string ItemIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier ItemIdentifier { get; set; } - [Serialize(1, true)] + [Serialize(1, IsPropertySaveable.Yes)] public int Amount { get; set; } - public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public RemoveItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrWhiteSpace(ItemIdentifier)) + if (ItemIdentifier.IsEmpty) { - ItemIdentifier = element.GetAttributeString("itemidentifiers", null) ?? element.GetAttributeString("identifier", ""); + ItemIdentifier = element.GetAttributeIdentifier("itemidentifiers", element.GetAttributeIdentifier("identifier", Identifier.Empty)); } } @@ -62,17 +62,17 @@ namespace Barotrauma var item = inventory.FindItem(it => it != null && !removedItems.Contains(it) && - (string.IsNullOrEmpty(ItemIdentifier) || it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)), recursive: true); + (ItemIdentifier.IsEmpty || it.Prefab.Identifier == ItemIdentifier), recursive: true); if (item == null) { break; } - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); } } else if (target is Item item) { - if (string.IsNullOrEmpty(ItemIdentifier) || item.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)) + if (ItemIdentifier.IsEmpty || item.Prefab.Identifier == ItemIdentifier) { - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); removedItems.Add(item); if (removedItems.Count >= Amount) { break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs index d4f86a1f0..14be7bdba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ReputationAction.cs @@ -15,15 +15,15 @@ namespace Barotrauma Faction } - public ReputationAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public ReputationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Increase { get; set; } - [Serialize("", true)] - public string Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } - [Serialize(ReputationType.None, true)] + [Serialize(ReputationType.None, IsPropertySaveable.Yes)] public ReputationType TargetType { get; set; } private bool isFinished; @@ -47,7 +47,7 @@ namespace Barotrauma { case ReputationType.Faction: { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == Identifier); if (faction != null) { faction.Reputation.AddReputation(Increase); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs index d9e180e3a..7e33a2373 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetDataAction.cs @@ -12,16 +12,16 @@ namespace Barotrauma Add } - public SetDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public SetDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } - [Serialize(OperationType.Set, true)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] public OperationType Operation { get; set; } - [Serialize(null, true)] + [Serialize(null, IsPropertySaveable.Yes)] public string Value { get; set; } = null!; - [Serialize("", true)] - public string Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } private bool isFinished; @@ -47,7 +47,7 @@ namespace Barotrauma isFinished = true; } - public static void PerformOperation(CampaignMetadata metadata, string identifier, object value, OperationType operation) + public static void PerformOperation(CampaignMetadata metadata, Identifier identifier, object value, OperationType operation) { if (metadata == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs index 6c0ac0e1f..bdded765e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SetPriceMultiplierAction.cs @@ -19,16 +19,16 @@ namespace Barotrauma Mechanical } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float Multiplier { get; set; } - [Serialize(OperationType.Set, true)] + [Serialize(OperationType.Set, IsPropertySaveable.Yes)] public OperationType Operation { get; set; } - [Serialize(PriceMultiplierType.Store, true)] + [Serialize(PriceMultiplierType.Store, IsPropertySaveable.Yes)] public PriceMultiplierType TargetMultiplier { get; set; } - public SetPriceMultiplierAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public SetPriceMultiplierAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index d63601890..c74ddea95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -7,21 +7,21 @@ namespace Barotrauma { class SkillCheckAction : BinaryOptionAction { - [Serialize("", true)] - public string RequiredSkill { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier RequiredSkill { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float RequiredLevel { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool ProbabilityBased { get; set; } - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public SkillCheckAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public SkillCheckAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { - if (string.IsNullOrEmpty(TargetTag)) + if (TargetTag.IsEmpty) { DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": SkillCheckAction without a target tag (the action needs to know whose skill to check)."); } @@ -33,11 +33,11 @@ namespace Barotrauma if (ProbabilityBased) { - return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) / RequiredLevel > Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); + return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill) / RequiredLevel > Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); } else { - return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) >= RequiredLevel); + return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill) >= RequiredLevel); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 8293cc1e5..a05b0a183 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -19,39 +19,39 @@ namespace Barotrauma BeaconStation } - [Serialize("", true, description: "Species name of the character to spawn.")] - public string SpeciesName { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Species name of the character to spawn.")] + public Identifier SpeciesName { get; set; } - [Serialize("", true, description: "Identifier of the NPC set to choose from.")] - public string NPCSetIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the NPC set to choose from.")] + public Identifier NPCSetIdentifier { get; set; } - [Serialize("", true, description: "Identifier of the NPC.")] - public string NPCIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the NPC.")] + public Identifier NPCIdentifier { get; set; } - [Serialize(true, true, description: "Should taking the items of this npc be considered as stealing?")] + [Serialize(true, IsPropertySaveable.Yes, description: "Should taking the items of this npc be considered as stealing?")] public bool LootingIsStealing { get; set; } - [Serialize("", true, description: "Identifier of the item to spawn.")] - public string ItemIdentifier { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item to spawn.")] + public Identifier ItemIdentifier { get; set; } - [Serialize("", true, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")] + public Identifier TargetTag { get; set; } - [Serialize("", true, description: "Tag of an entity with an inventory to spawn the item into.")] - public string TargetInventory { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of an entity with an inventory to spawn the item into.")] + public Identifier TargetInventory { get; set; } - [Serialize(SpawnLocationType.MainSub, true)] + [Serialize(SpawnLocationType.MainSub, IsPropertySaveable.Yes)] public SpawnLocationType SpawnLocation { get; set; } - [Serialize(SpawnType.Human, true)] + [Serialize(SpawnType.Human, IsPropertySaveable.Yes)] public SpawnType SpawnPointType { get; set; } - [Serialize("", true)] - public string SpawnPointTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SpawnPointTag { get; set; } - private readonly HashSet targetModuleTags = new HashSet(); + private readonly HashSet targetModuleTags = new HashSet(); - [Serialize("", true, "What outpost module tags does the entity prefer to spawn in.")] + [Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")] public string TargetModuleTags { get => string.Join(",", targetModuleTags); @@ -63,13 +63,13 @@ namespace Barotrauma string[] splitTags = value.Split(','); foreach (var s in splitTags) { - targetModuleTags.Add(s); + targetModuleTags.Add(s.ToIdentifier()); } } } } - [Serialize(false, true, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] public bool IgnoreByAI { get; set; } private bool spawned; @@ -77,7 +77,7 @@ namespace Barotrauma private readonly bool ignoreSpawnPointType; - public SpawnAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase)); } @@ -104,16 +104,16 @@ namespace Barotrauma { if (spawned) { return; } - if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier)) + if (!NPCSetIdentifier.IsEmpty && !NPCIdentifier.IsEmpty) { HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); if (humanPrefab != null) { ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { if (newCharacter == null) { return; } - newCharacter.Prefab = humanPrefab; + newCharacter.HumanPrefab = humanPrefab; newCharacter.TeamID = CharacterTeamType.FriendlyNPC; newCharacter.EnableDespawn = false; humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); @@ -126,7 +126,7 @@ namespace Barotrauma } } humanPrefab.InitializeCharacter(newCharacter, spawnPos); - if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) + if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); } @@ -134,18 +134,18 @@ namespace Barotrauma }); } } - else if (!string.IsNullOrEmpty(SpeciesName)) + else if (!SpeciesName.IsEmpty) { - Entity.Spawner.AddToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => + Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => { - if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) + if (!TargetTag.IsEmpty && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); } spawnedEntity = newCharacter; }); } - else if (!string.IsNullOrEmpty(ItemIdentifier)) + else if (!ItemIdentifier.IsEmpty) { if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab)) { @@ -154,7 +154,7 @@ namespace Barotrauma else { Inventory spawnInventory = null; - if (!string.IsNullOrEmpty(TargetInventory)) + if (!TargetInventory.IsEmpty) { var targets = ParentEvent.GetTargets(TargetInventory); if (targets.Any()) @@ -178,17 +178,17 @@ namespace Barotrauma if (spawnInventory == null) { - Entity.Spawner.AddToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned); } else { - Entity.Spawner.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned); } void onSpawned(Item newItem) { if (newItem != null) { - if (!string.IsNullOrEmpty(TargetTag)) + if (!TargetTag.IsEmpty) { ParentEvent.AddTarget(TargetTag, newItem); } @@ -221,7 +221,7 @@ namespace Barotrauma private ISpatialEntity GetSpawnPos() { - if (!string.IsNullOrWhiteSpace(SpawnPointTag)) + if (!SpawnPointTag.IsEmpty) { List potentialItems = SpawnLocation switch { @@ -234,10 +234,10 @@ namespace Barotrauma _ => throw new NotImplementedException() }; - var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandom(); + var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).GetRandom(); + var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced(); if (target != null) { return target; } } @@ -247,7 +247,7 @@ namespace Barotrauma return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable()); } - 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) { List potentialSpawnPoints = spawnLocation switch { @@ -301,7 +301,7 @@ namespace Barotrauma } //don't spawn in an airlock module if there are other options - var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false); + var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false); if (airlockSpawnPoints.Count() < validSpawnPoints.Count()) { validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints); @@ -310,7 +310,7 @@ namespace Barotrauma if (!validSpawnPoints.Any()) { 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.GetRandom(); + return potentialSpawnPoints.GetRandomUnsynced(); } //avoid using waypoints if there's any actual spawnpoints available @@ -346,7 +346,7 @@ namespace Barotrauma } else { - return validSpawnPoints.GetRandom(); + return validSpawnPoints.GetRandomUnsynced(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs index 8a3630247..4dc8d6adc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/StatusEffectAction.cs @@ -9,19 +9,19 @@ namespace Barotrauma private int actionIndex; - [Serialize("", true)] - public string TargetTag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } - public StatusEffectAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public StatusEffectAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { actionIndex = 0; - foreach (XElement subElement in parentEvent.Prefab.ConfigElement.Descendants()) + foreach (var subElement in parentEvent.Prefab.ConfigElement.Descendants()) { if (subElement == element) { break; } actionIndex++; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index a1a054a1c..377fa8eb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -1,5 +1,9 @@ using Barotrauma.Extensions; using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -8,21 +12,35 @@ namespace Barotrauma { public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Criteria { get; set; } - [Serialize("", true)] - public string Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Tag { get; set; } - [Serialize(SubType.Any, true)] + [Serialize(SubType.Any, IsPropertySaveable.Yes)] public SubType SubmarineType { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool IgnoreIncapacitatedCharacters { get; set; } private bool isFinished = false; - public TagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public TagAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + Taggers = new (string k, Action v)[] + { + ("players", v => TagPlayers()), + ("player", v => TagPlayers()), + ("bot", v => TagBots(playerCrewOnly: false)), + ("crew", v => TagCrew()), + ("humanprefabidentifier", TagHumansByIdentifier), + ("structureidentifier", TagStructuresByIdentifier), + ("itemidentifier", TagItemsByIdentifier), + ("itemtag", TagItemsByTag), + ("hullname", TagHullsByName) + }.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary(); + } public override bool IsFinished(ref string goTo) { @@ -67,34 +85,34 @@ namespace Barotrauma #endif } - private void TagHumansByIdentifier(string identifier) + private void TagHumansByIdentifier(Identifier identifier) { foreach (Character c in Character.CharacterList) { - if (c.Prefab?.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase) ?? false) + if (c.HumanPrefab?.Identifier == identifier) { ParentEvent.AddTarget(Tag, c); } } } - private void TagStructuresByIdentifier(string identifier) + private void TagStructuresByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier); } - private void TagItemsByIdentifier(string identifier) + private void TagItemsByIdentifier(Identifier identifier) { - ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier == identifier); } - private void TagItemsByTag(string tag) + private void TagItemsByTag(Identifier tag) { ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag)); } - private void TagHullsByName(string name) + private void TagHullsByName(Identifier name) { - ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name, StringComparison.OrdinalIgnoreCase)); + ParentEvent.AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase)); } private bool SubmarineTypeMatches(Submarine sub) @@ -117,6 +135,8 @@ namespace Barotrauma } } + private readonly ImmutableDictionary> Taggers; + public override void Update(float deltaTime) { if (isFinished) { return; } @@ -126,32 +146,17 @@ namespace Barotrauma foreach (string entry in criteriaSplit) { string[] kvp = entry.Split(':'); - switch (kvp[0].Trim().ToLowerInvariant()) + Identifier key = kvp[0].Trim().ToIdentifier(); + Identifier value = kvp.Length > 1 ? kvp[1].Trim().ToIdentifier() : Identifier.Empty; + if (Taggers.TryGetValue(key, out Action tagger)) { - case "player": - TagPlayers(); - break; - case "bot": - TagBots(playerCrewOnly: false); - break; - case "crew": - TagCrew(); - break; - case "humanprefabidentifier": - if (kvp.Length > 1) { TagHumansByIdentifier(kvp[1].Trim()); } - break; - case "structureidentifier": - if (kvp.Length > 1) { TagStructuresByIdentifier(kvp[1].Trim()); } - break; - case "itemidentifier": - if (kvp.Length > 1) { TagItemsByIdentifier(kvp[1].Trim()); } - break; - case "itemtag": - if (kvp.Length > 1) { TagItemsByTag(kvp[1].Trim()); } - break; - case "hullname": - if (kvp.Length > 1) { TagHullsByName(kvp[1].Trim()); } - break; + tagger(value); + } + else + { + string errorMessage = $"Error in TagAction (event \"{ParentEvent.Prefab.Identifier}\") - unrecognized target criteria \"{key}\"."; + DebugConsole.ThrowError(errorMessage); + GameAnalyticsManager.AddErrorEventOnce($"TagAction.Update:InvalidCriteria_{ParentEvent.Prefab.Identifier}_{key}", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index c75d1ea35..470d6a129 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -8,42 +8,39 @@ namespace Barotrauma { class TriggerAction : EventAction { - [Serialize("", true, description: "Tag of the first entity that will be used for trigger checks.")] - public string Target1Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the first entity that will be used for trigger checks.")] + public Identifier Target1Tag { get; set; } - [Serialize("", true, description: "Tag of the second entity that will be used for trigger checks.")] - public string Target2Tag { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag of the second entity that will be used for trigger checks.")] + public Identifier Target2Tag { get; set; } - [Serialize("", true, description: "If set, the first target has to be within an outpost module of this type.")] - public string TargetModuleType { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "If set, the first target has to be within an outpost module of this type.")] + public Identifier TargetModuleType { get; set; } - [Serialize("", true, description: "Tag to apply to the first entity when the trigger check succeeds.")] - public string ApplyToTarget1 { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first entity when the trigger check succeeds.")] + public Identifier ApplyToTarget1 { get; set; } - [Serialize("", true, description: "Tag to apply to the second entity when the trigger check succeeds.")] - public string ApplyToTarget2 { get; set; } + [Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the second entity when the trigger check succeeds.")] + public Identifier ApplyToTarget2 { get; set; } - [Serialize(0.0f, true, description: "Range both entities must be within to activate the trigger.")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Range both entities must be within to activate the trigger.")] public float Radius { get; set; } - [Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] + [Serialize(true, IsPropertySaveable.Yes, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] public bool DisableInCombat { get; set; } - [Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")] + [Serialize(true, IsPropertySaveable.Yes, description: "If true, dead/unconscious characters cannot trigger the action.")] public bool DisableIfTargetIncapacitated { get; set; } - [Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, one target must interact with the other to trigger the action.")] public bool WaitForInteraction { get; set; } - [Serialize(false, true, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, the action can be triggered by interacting with any matching target (not just the 1st one).")] public bool AllowMultipleTargets { get; set; } private float distance; - - public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) - { - TargetModuleType = TargetModuleType?.ToLowerInvariant(); - } + + public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; public override bool IsFinished(ref string goTo) @@ -74,7 +71,7 @@ namespace Barotrauma { if (DisableInCombat && IsInCombat(e1)) { continue; } if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; } - if (!string.IsNullOrEmpty(TargetModuleType)) + if (!TargetModuleType.IsEmpty) { if (IsCloseEnoughToHull(e1, out Hull hull)) { @@ -143,7 +140,7 @@ namespace Barotrauma #if CLIENT npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, - TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use))); + TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, @@ -226,7 +223,7 @@ namespace Barotrauma } else { - foreach (Hull potentialHull in Hull.hullList) + foreach (Hull potentialHull in Hull.HullList) { if (!potentialHull.OutpostModuleTags.Contains(TargetModuleType)) { continue; } @@ -270,11 +267,11 @@ namespace Barotrauma private void Trigger(Entity entity1, Entity entity2) { ResetTargetIcons(); - if (!string.IsNullOrEmpty(ApplyToTarget1)) + if (!ApplyToTarget1.IsEmpty) { ParentEvent.AddTarget(ApplyToTarget1, entity1); } - if (!string.IsNullOrEmpty(ApplyToTarget2)) + if (!ApplyToTarget2.IsEmpty) { ParentEvent.AddTarget(ApplyToTarget2, entity2); } @@ -285,7 +282,7 @@ namespace Barotrauma public override string ToDebugString() { - if (string.IsNullOrEmpty(TargetModuleType)) + if (TargetModuleType.IsEmpty) { return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index 9a6aabf58..c7253cc72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -4,12 +4,12 @@ namespace Barotrauma { class TriggerEventAction : EventAction { - [Serialize("", true)] - public string Identifier { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier Identifier { get; set; } private bool isFinished; - public TriggerEventAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public override bool IsFinished(ref string goTo) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs index 51ef8e0cf..ac2b2fe9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/UnlockPathAction.cs @@ -9,7 +9,7 @@ namespace Barotrauma { class UnlockPathAction : EventAction { - public UnlockPathAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } private bool isFinished = false; @@ -36,7 +36,7 @@ namespace Barotrauma NotifyUnlock(connection); #else new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"), - new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); + Array.Empty(), type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs index a413f9668..6380f8f0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/WaitAction.cs @@ -5,12 +5,12 @@ namespace Barotrauma { class WaitAction : EventAction { - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Time { get; set; } private float timeRemaining; - public WaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + public WaitAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { timeRemaining = Time; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 039131b10..d979fc275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -144,17 +144,17 @@ namespace Barotrauma seed = ToolBox.StringToInt(level.Seed); foreach (var previousEvent in level.LevelData.EventHistory) { - seed ^= ToolBox.StringToInt(previousEvent.Identifier); + seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier); } } MTRandom rand = new MTRandom(seed); - EventSet initialEventSet = SelectRandomEvents(EventSet.List, requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); EventSet additiveSet = null; if (initialEventSet != null && initialEventSet.Additive) { additiveSet = initialEventSet; - initialEventSet = SelectRandomEvents(EventSet.List.FindAll(e => !e.Additive), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); + initialEventSet = SelectRandomEvents(EventSet.Prefabs.Where(e => !e.Additive).ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand); } if (initialEventSet != null) { @@ -172,14 +172,14 @@ namespace Barotrauma //if the outpost is connected to a locked connection, create an event to unlock it if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false) { - var unlockPathPrefabs = EventSet.PrefabList.FindAll(e => e.UnlockPathEvent); - var unlockPathPrefabsForBiome = unlockPathPrefabs.FindAll(e => - string.IsNullOrEmpty(e.BiomeIdentifier) || - e.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase)); + var unlockPathPrefabs = EventPrefab.Prefabs.Where(e => e.UnlockPathEvent); + var unlockPathPrefabsForBiome = unlockPathPrefabs.Where(e => + e.BiomeIdentifier.IsEmpty || + e.BiomeIdentifier == level.LevelData.Biome.Identifier); var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ? - ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, unlockPathPrefabsForBiome.Select(b => b.Commonness).ToList(), rand) : - ToolBox.SelectWeightedRandom(unlockPathPrefabs, unlockPathPrefabs.Select(b => b.Commonness).ToList(), rand); + ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, b => b.Commonness, rand) : + ToolBox.SelectWeightedRandom(unlockPathPrefabs, b => b.Commonness, rand); if (unlockPathEventPrefab != null) { var newEvent = unlockPathEventPrefab.CreateInstance(); @@ -204,7 +204,7 @@ namespace Barotrauma if (eventSet == null) { return; } if (eventSet.OncePerOutpost) { - foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.Prefabs)) + foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) { if (!level.LevelData.NonRepeatableEvents.Contains(ep)) { @@ -237,16 +237,17 @@ namespace Barotrauma private void SelectSettings() { - if (EventManagerSettings.List.Count == 0) + if (!EventManagerSettings.Prefabs.Any()) { throw new InvalidOperationException("Could not select EventManager settings (no settings loaded)."); } + var orderedByDifficulty = EventManagerSettings.OrderedByDifficulty.ToArray(); if (level == null) { #if CLIENT if (GameMain.GameSession.GameMode is TestGameMode) { - settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)]; + settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient); if (settings != null) { eventThreshold = settings.DefaultEventThreshold; @@ -257,18 +258,18 @@ namespace Barotrauma throw new InvalidOperationException("Could not select EventManager settings (level not set)."); } - var suitableSettings = EventManagerSettings.List.FindAll(s => + var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s => level.Difficulty >= s.MinLevelDifficulty && - level.Difficulty <= s.MaxLevelDifficulty); + level.Difficulty <= s.MaxLevelDifficulty).ToArray(); - if (suitableSettings.Count == 0) + if (suitableSettings.Length == 0) { DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")"); - settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)]; + settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient); } else { - settings = suitableSettings[Rand.Int(suitableSettings.Count, Rand.RandSync.Server)]; + settings = suitableSettings.GetRandom(Rand.RandSync.ServerAndClient); } if (settings != null) { @@ -292,17 +293,17 @@ namespace Barotrauma public void PreloadContent(IEnumerable contentFiles) { - var filesToPreload = new List(contentFiles); + var filesToPreload = contentFiles.ToList(); foreach (Submarine sub in Submarine.Loaded) { if (sub.WreckAI == null) { continue; } - if (!string.IsNullOrEmpty(sub.WreckAI.Config.DefensiveAgent)) + if (!sub.WreckAI.Config.DefensiveAgent.IsEmpty) { var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent); if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) { - filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + filesToPreload.Add(prefab.ContentFile); } } foreach (Item item in Item.ItemList) @@ -318,9 +319,9 @@ namespace Barotrauma foreach (var spawnInfo in statusEffect.SpawnCharacters) { var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName); - if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) + if (prefab != null && !filesToPreload.Contains(prefab.ContentFile)) { - filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + filesToPreload.Add(prefab.ContentFile); } } } @@ -331,77 +332,7 @@ namespace Barotrauma foreach (ContentFile file in filesToPreload) { - switch (file.Type) - { - case ContentType.Character: -#if CLIENT - CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(file.Path); - if (characterPrefab?.XDocument == null) - { - throw new Exception($"Failed to load the character config file from {file.Path}!"); - } - var doc = characterPrefab.XDocument; - var rootElement = doc.Root; - var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; - mainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e)); - if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string speciesName)) { continue; } - bool humanoid = mainElement.GetAttributeBool("humanoid", false); - CharacterPrefab originalCharacter; - if (characterPrefab.VariantOf != null) - { - originalCharacter = CharacterPrefab.FindBySpeciesName(characterPrefab.VariantOf); - var originalRoot = originalCharacter.XDocument.Root; - var originalMainElement = originalRoot.IsOverride() ? originalRoot.FirstElement() : originalRoot; - originalMainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e)); - if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string name)) { continue; } - speciesName = name; - if (mainElement.Attribute("humanoid") == null) - { - humanoid = originalMainElement.GetAttributeBool("humanoid", false); - } - } - RagdollParams ragdollParams; - try - { - if (humanoid) - { - ragdollParams = RagdollParams.GetRagdollParams(characterPrefab.VariantOf ?? speciesName); - } - else - { - ragdollParams = RagdollParams.GetRagdollParams(characterPrefab.VariantOf ?? speciesName); - } - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e); - continue; - } - - if (ragdollParams != null) - { - HashSet texturePaths = new HashSet - { - ragdollParams.Texture - }; - foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs) - { - if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); } - if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); } - if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); } - foreach (var decorativeSprite in limb.decorativeSpriteParams) - { - if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); } - } - } - foreach (string texturePath in texturePaths) - { - preloadedSprites.Add(new Sprite(texturePath, Vector2.Zero)); - } - } -#endif - break; - } + file.Preload(preloadedSprites.Add); } } @@ -435,7 +366,8 @@ namespace Barotrauma { if (level == null) { return; } if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; } - DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue, debugOnly: true); + DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); + int applyCount = 1; List> spawnPosFilter = new List>(); if (eventSet.PerRuin) @@ -464,31 +396,29 @@ namespace Barotrauma } } - bool isPrefabSuitable(EventPrefab p) - => string.IsNullOrEmpty(p.BiomeIdentifier) || - p.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase); - - var suitablePrefabSubsets = eventSet.EventPrefabs - .FindAll(p => p.Prefabs.Any(isPrefabSuitable)); + bool isPrefabSuitable(EventPrefab e) + => e.BiomeIdentifier.IsEmpty || + e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; + + var suitablePrefabSubsets = eventSet.EventPrefabs.Where( + e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray(); for (int i = 0; i < applyCount; i++) { if (eventSet.ChooseRandom) { - if (suitablePrefabSubsets.Count > 0) + if (suitablePrefabSubsets.Any()) { var unusedEvents = suitablePrefabSubsets.ToList(); for (int j = 0; j < eventSet.EventCount; j++) { - if (unusedEvents.All(e => e.Prefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } - EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Prefabs.Max(p => CalculateCommonness(p, e.Commonness))).ToList(), rand); + if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; } + EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), rand); (IEnumerable eventPrefabs, float commonness, float probability) = subEventPrefab; if (eventPrefabs != null && rand.NextDouble() <= probability) { - var finalPrefabs = eventPrefabs.Where(isPrefabSuitable).ToArray(); - var finalPrefabCommonnesses = finalPrefabs.Select(p => p.Commonness).ToArray(); - var eventPrefab = ToolBox.SelectWeightedRandom(finalPrefabs, finalPrefabCommonnesses, rand); - + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); @@ -503,7 +433,7 @@ namespace Barotrauma } } } - if (eventSet.ChildSets.Count > 0) + if (eventSet.ChildSets.Any()) { var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand); if (newEventSet != null) @@ -517,10 +447,8 @@ namespace Barotrauma foreach ((IEnumerable eventPrefabs, float commonness, float probability) in suitablePrefabSubsets) { if (rand.NextDouble() > probability) { continue; } - - var finalPrefabs = eventPrefabs.Where(isPrefabSuitable).ToArray(); - var finalPrefabCommonnesses = finalPrefabs.Select(p => p.Commonness).ToArray(); - var eventPrefab = ToolBox.SelectWeightedRandom(finalPrefabs, finalPrefabCommonnesses, rand); + + var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand); var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); @@ -540,7 +468,7 @@ namespace Barotrauma } } - private EventSet SelectRandomEvents(List eventSets, bool? requireCampaignSet = null, Random random = null) + private EventSet SelectRandomEvents(IReadOnlyList eventSets, bool? requireCampaignSet = null, Random random = null) { if (level == null) { return null; } Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); @@ -549,7 +477,7 @@ namespace Barotrauma eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType && - (string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase))); + (es.BiomeIdentifier.IsEmpty || es.BiomeIdentifier == level.LevelData.Biome.Identifier)); if (requireCampaignSet.HasValue) { @@ -579,7 +507,7 @@ namespace Barotrauma { allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || - set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase))); + set.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier)); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -786,7 +714,7 @@ namespace Barotrauma monsterStrength = 0; foreach (Character character in Character.CharacterList) { - if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } + if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup(CharacterPrefab.HumanSpeciesName)) { continue; } if (!(character.AIController is EnemyAIController enemyAI)) { continue; } @@ -836,7 +764,7 @@ namespace Barotrauma float holeCount = 0.0f; float waterAmount = 0.0f; float dryHullVolume = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; } if (GameMain.GameSession?.GameMode is PvPMode) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs index 3572f831d..7d5fece59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs @@ -6,12 +6,25 @@ using Microsoft.Xna.Framework; namespace Barotrauma { - class EventManagerSettings + class EventManagerSettings : PrefabWithUintIdentifier { - public static readonly List List = new List(); + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public static IOrderedEnumerable OrderedByDifficulty + { + get + { + return Prefabs.OrderBy(p => (p.MinLevelDifficulty + p.MaxLevelDifficulty) * 0.5f) + .ThenBy(p => p.UintIdentifier); + } + } - public readonly string Identifier; - public readonly string Name; + public static EventManagerSettings GetByDifficultyPercentile(float p) + { + EventManagerSettings[] settings = OrderedByDifficulty.ToArray(); + return settings[Math.Clamp((int)(settings.Length * p), 0, settings.Length - 1)]; + } + + public readonly LocalizedString Name; //How much the event threshold increases per second. 0.0005f = 0.03f per minute public readonly float EventThresholdIncrease = 0.0005f; @@ -26,61 +39,19 @@ namespace Barotrauma public readonly float FreezeDurationWhenCrewAway = 60.0f * 10.0f; - public static void Init() + public override void Dispose() { } + + public EventManagerSettings(XElement element, EventManagerSettingsFile file) : base(file, element.NameAsIdentifier()) { - List.Clear(); - foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings)) - { - Load(file); - } - } + Name = TextManager.Get("difficulty." + Identifier).Fallback(Identifier.Value); + EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", EventThresholdIncrease); + DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", DefaultEventThreshold); + EventCooldown = element.GetAttributeFloat("EventCooldown", EventCooldown); - private static void Load(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var mainElement = doc.Root; - bool allowOverriding = false; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - foreach (XElement subElement in mainElement.Elements()) - { - var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; - string identifier = element.Name.ToString(); - var duplicate = List.FirstOrDefault(e => e.Identifier.ToString().Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (duplicate != null) - { - if (allowOverriding || subElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file.Path}'", Color.Yellow); - List.Remove(duplicate); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': Another element with the name '{identifier}' found! Each element must have a unique name. Use tags if you want to override an existing preset."); - continue; - } - } - List.Add(new EventManagerSettings(element)); - } - List.Sort((x, y) => { return Math.Sign((x.MinLevelDifficulty + x.MaxLevelDifficulty) / 2.0f - (y.MinLevelDifficulty + y.MaxLevelDifficulty) / 2.0f); }); - } + MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", MinLevelDifficulty); + MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", MaxLevelDifficulty); - public EventManagerSettings(XElement element) - { - Identifier = element.Name.ToString(); - Name = TextManager.Get("difficulty." + Identifier, returnNull: true) ?? Identifier; - EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f); - DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f); - EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f); - - MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f); - MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f); - - FreezeDurationWhenCrewAway = element.GetAttributeFloat("FreezeDurationWhenCrewAway", 10.0f * 60.0f); + FreezeDurationWhenCrewAway = element.GetAttributeFloat("FreezeDurationWhenCrewAway", FreezeDurationWhenCrewAway); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 856f99d79..db5fe38b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -4,23 +4,25 @@ using System.Xml.Linq; namespace Barotrauma { - class EventPrefab + class EventPrefab : Prefab { - public readonly XElement ConfigElement; + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + public readonly ContentXElement ConfigElement; public readonly Type EventType; public readonly float Probability; public readonly bool TriggerEventCooldown; - public float Commonness; - public string Identifier; - public string BiomeIdentifier; - public float SpawnDistance; + public readonly float Commonness; + public readonly Identifier BiomeIdentifier; + public readonly float SpawnDistance; - public bool UnlockPathEvent; - public string UnlockPathTooltip; - public int UnlockPathReputation; - public string UnlockPathFaction; + public readonly bool UnlockPathEvent; + public readonly string UnlockPathTooltip; + public readonly int UnlockPathReputation; + public readonly string UnlockPathFaction; - public EventPrefab(XElement element) + public EventPrefab(ContentXElement element, RandomEventsFile file, Identifier fallbackIdentifier = default) + : base(file, element.GetAttributeIdentifier("identifier", fallbackIdentifier)) { ConfigElement = element; @@ -37,8 +39,7 @@ namespace Barotrauma DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } - Identifier = ConfigElement.GetAttributeString("identifier", string.Empty); - BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty); + BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent)); @@ -73,6 +74,8 @@ namespace Barotrauma return instance; } + public override void Dispose() { } + public override string ToString() { return $"EventPrefab ({Identifier})"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 227528cee..2d91d363c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -7,13 +7,29 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma -{ - class EventSet +{ +#if CLIENT + class EventSprite : Prefab + { + public readonly static PrefabCollection Prefabs = new PrefabCollection(); + + public readonly Sprite Sprite; + + public EventSprite(ContentXElement element, RandomEventsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) + { + Sprite = new Sprite(element); + } + + public override void Dispose() { Sprite?.Remove(); } + } +#endif + + class EventSet : Prefab { internal class EventDebugStats { public readonly EventSet RootSet; - public readonly Dictionary MonsterCounts = new Dictionary(); + public readonly Dictionary MonsterCounts = new Dictionary(); public float MonsterStrength; public EventDebugStats(EventSet rootSet) @@ -22,13 +38,7 @@ namespace Barotrauma } } - public static List List - { - get; - private set; - } - - public static readonly List PrefabList = new List(); + public readonly static PrefabCollection Prefabs = new PrefabCollection(); #if CLIENT private static readonly Dictionary EventSprites = new Dictionary(); @@ -47,21 +57,23 @@ namespace Barotrauma public static List GetAllEventPrefabs() { - List eventPrefabs = new List(PrefabList); - foreach (var eventSet in List) + List eventPrefabs = EventPrefab.Prefabs.ToList(); + foreach (var eventSet in Prefabs) { - eventPrefabs.AddRange(eventSet.EventPrefabs.SelectMany(ep => ep.Prefabs)); - foreach (var childSet in eventSet.ChildSets) - { - eventPrefabs.AddRange(childSet.EventPrefabs.SelectMany(ep => ep.Prefabs)); - } + AddSetEventPrefabsToList(eventPrefabs, eventSet); } return eventPrefabs; } - public static EventPrefab GetEventPrefab(string identifer) + public static void AddSetEventPrefabsToList(List list, EventSet set) { - return GetAllEventPrefabs().Find(prefab => string.Equals(prefab.Identifier, identifer, StringComparison.Ordinal)); + list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs)); + foreach (var childSet in set.ChildSets) { AddSetEventPrefabsToList(list, childSet); } + } + + public static EventPrefab GetEventPrefab(Identifier identifier) + { + return GetAllEventPrefabs().Find(prefab => prefab.Identifier == identifier); } public readonly bool IsCampaignSet; @@ -69,11 +81,11 @@ namespace Barotrauma //0-100 public readonly float MinLevelDifficulty, MaxLevelDifficulty; - public readonly string BiomeIdentifier; + public readonly Identifier BiomeIdentifier; public readonly LevelData.LevelType LevelType; - public readonly string[] LocationTypeIdentifiers; + public readonly ImmutableArray LocationTypeIdentifiers; public readonly bool ChooseRandom; @@ -99,68 +111,100 @@ namespace Barotrauma public readonly bool TriggerEventCooldown; public readonly bool Additive; + + public readonly float DefaultCommonness; + public readonly ImmutableDictionary OverrideCommonness; - public readonly Dictionary Commonness; - - public struct SubEventPrefab + public readonly struct SubEventPrefab { - public SubEventPrefab(string debugIdentifier, string[] prefabIdentifiers, float? commonness, float? probability) + public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) { - EventPrefab tryFindPrefab(string id) + PrefabOrIdentifier = prefabOrIdentifiers; + SelfCommonness = commonness; + SelfProbability = probability; + } + + public readonly Either PrefabOrIdentifier; + public IEnumerable EventPrefabs + { + get { - var prefab = PrefabList.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase)); - if (prefab is null) + if (PrefabOrIdentifier.TryGet(out EventPrefab p)) { - DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\" - could not find the event prefab \"{id}\"."); + yield return p; + } + else + { + foreach (var id in (Identifier[])PrefabOrIdentifier) + { + yield return EventPrefab.Prefabs[id]; + } } - return prefab; } - - this.Prefabs = prefabIdentifiers - .Select(tryFindPrefab) - .Where(p => p != null) - .ToImmutableArray(); - this.Commonness = commonness ?? this.Prefabs.Select(p => p.Commonness).MaxOrNull() ?? 0.0f; - this.Probability = probability ?? this.Prefabs.Select(p => p.Probability).MaxOrNull() ?? 0.0f; } + public readonly float? SelfCommonness; + public float Commonness => SelfCommonness ?? EventPrefabs.MaxOrNull(p => p.Commonness) ?? 0.0f; - public SubEventPrefab(EventPrefab prefab, float commonness, float probability) + public readonly float? SelfProbability; + public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f; + + public void Deconstruct(out IEnumerable eventPrefabs, out float commonness, out float probability) { - Prefabs = prefab.ToEnumerable().ToImmutableArray(); - Commonness = commonness; - Probability = probability; - } - - public readonly ImmutableArray Prefabs; - public readonly float Commonness; - public readonly float Probability; - - public void Deconstruct(out IEnumerable prefabs, out float commonness, out float probability) - { - prefabs = Prefabs; + eventPrefabs = EventPrefabs; commonness = Commonness; probability = Probability; } } - - public readonly List EventPrefabs; + public readonly ImmutableArray EventPrefabs; - public readonly List ChildSets; + public readonly ImmutableArray ChildSets; - public string DebugIdentifier + private static Identifier DetermineIdentifier(EventSet parent, XElement element, RandomEventsFile file) { - get; - private set; - } = ""; + Identifier retVal = element.GetAttributeIdentifier("identifier", Identifier.Empty); - private EventSet(XElement element, string debugIdentifier, EventSet parentSet = null) + if (retVal.IsEmpty) + { + if (parent is null) + { + if (file.ContentPackage is CorePackage) + { + throw new Exception($"Error in {file.Path}: All root EventSets in a core package must have identifiers"); + } + else + { + DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier"); + } + } + + XElement currElement = element; + string siblingIndices = ""; + while (currElement.Parent != null) + { + int siblingIndex = currElement.ElementsBeforeSelf().Count(); + siblingIndices = $"-{siblingIndex}{siblingIndices}"; + if (parent != null) { break; } + currElement = currElement.Parent; + } + + retVal = + ((parent != null + ? parent.Identifier.Value + : $"{file.ContentPackage.Name}-{file.Path}") + + siblingIndices) + .ToIdentifier(); + } + return retVal; + } + + public EventSet(ContentXElement element, RandomEventsFile file, EventSet parentSet = null) + : base(file, DetermineIdentifier(parentSet, element, file)) { - DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier; - Commonness = new Dictionary(); - EventPrefabs = new List(); - ChildSets = new List(); + var eventPrefabs = new List(); + var childSets = new List(); + var overrideCommonness = new Dictionary(); - BiomeIdentifier = element.GetAttributeString("biome", string.Empty); + BiomeIdentifier = element.GetAttributeIdentifier("biome", Barotrauma.Identifier.Empty); MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0); MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty); @@ -169,14 +213,14 @@ namespace Barotrauma string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { - DebugConsole.ThrowError($"Error in event set \"{debugIdentifier}\". \"{levelTypeStr}\" is not a valid level type."); + DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); } - string[] locationTypeStr = element.GetAttributeStringArray("locationtype", null); + Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null); if (locationTypeStr != null) { - LocationTypeIdentifiers = locationTypeStr; - if (LocationType.List.Any()) { CheckLocationTypeErrors(); } + LocationTypeIdentifiers = locationTypeStr.ToImmutableArray(); + //if (LocationType.List.Any()) { CheckLocationTypeErrors(); } //TODO: perform validation elsewhere } MinIntensity = element.GetAttributeFloat("minintensity", 0.0f); @@ -198,164 +242,77 @@ namespace Barotrauma TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); - Commonness[""] = element.GetAttributeFloat("commonness", 1.0f); - foreach (XElement subElement in element.Elements()) + DefaultCommonness = 1.0f; + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "commonness": - Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f); + DefaultCommonness = subElement.GetAttributeFloat("commonness", 0.0f); foreach (XElement overrideElement in subElement.Elements()) { - if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) + if (overrideElement.NameAsIdentifier() == "override") { - string levelType = overrideElement.GetAttributeString("leveltype", "").ToLowerInvariant(); - if (!Commonness.ContainsKey(levelType)) + Identifier levelType = overrideElement.GetAttributeIdentifier("leveltype", ""); + if (!overrideCommonness.ContainsKey(levelType)) { - Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f)); + overrideCommonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f)); } } } break; case "eventset": - ChildSets.Add(new EventSet(subElement, this.DebugIdentifier + "-" + ChildSets.Count, this)); + childSets.Add(new EventSet(subElement, file, this)); break; default: //an element with just an identifier = reference to an event prefab if (!subElement.HasElements && subElement.Attributes().First().Name.ToString().Equals("identifier", StringComparison.OrdinalIgnoreCase)) { - string[] identifiers = subElement.GetAttributeStringArray("identifier", Array.Empty()); - + Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); float commonness = subElement.GetAttributeFloat("commonness", -1f); float probability = subElement.GetAttributeFloat("probability", -1f); - EventPrefabs.Add(new SubEventPrefab( - debugIdentifier, + eventPrefabs.Add(new SubEventPrefab( identifiers, commonness >= 0f ? commonness : (float?)null, probability >= 0f ? probability : (float?)null)); } else { - var prefab = new EventPrefab(subElement); - EventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); + var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier()); + eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); } break; } } + + EventPrefabs = eventPrefabs.ToImmutableArray(); + ChildSets = childSets.ToImmutableArray(); + OverrideCommonness = overrideCommonness.ToImmutableDictionary(); } public void CheckLocationTypeErrors() { if (LocationTypeIdentifiers == null) { return; } - foreach (string locationTypeId in LocationTypeIdentifiers) + foreach (Identifier locationTypeId in LocationTypeIdentifiers) { - if (!LocationType.List.Any(lt => lt.Identifier.Equals(locationTypeId, StringComparison.OrdinalIgnoreCase))) + if (!LocationType.Prefabs.ContainsKey(locationTypeId)) { - DebugConsole.ThrowError($"Error in event set \"{DebugIdentifier}\". Location type \"{locationTypeId}\" not found."); + DebugConsole.ThrowError($"Error in event set \"{Identifier}\". Location type \"{locationTypeId}\" not found."); } } } public float GetCommonness(Level level) { - string key = level.GenerationParams?.Identifier ?? ""; - return Commonness.ContainsKey(key) ? Commonness[key] : Commonness[""]; - } - - public static void LoadPrefabs() - { -#if CLIENT - EventSprites.ForEach(pair => pair.Value?.Remove()); - EventSprites.Clear(); -#endif - List = new List(); - var configFiles = GameMain.Instance.GetFilesOfType(ContentType.RandomEvents); - - if (!configFiles.Any()) - { - DebugConsole.ThrowError("No config files for random events found in the selected content package"); - return; - } - - List configElements = new List(); - Dictionary filePaths = new Dictionary(); - - foreach (ContentFile configFile in configFiles) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { continue; } - - var mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - if (doc.Root.IsOverride()) - { - DebugConsole.NewMessage($"Overriding all random events using the file {configFile.Path}", Color.Yellow); - List.Clear(); - } - - foreach (XElement element in doc.Root.Elements()) - { - configElements.Add(element); - filePaths[element] = configFile.Path; - } - } - - //load event prefabs first so we can link to them when loading event sets - foreach (XElement element in configElements) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "eventprefabs": - foreach (var subElement in element.Elements()) - { - // Warn if an event prefab has no identifier as this would make it impossible to refer to - if (!element.GetAttributeBool("suppresswarnings", false) && string.IsNullOrWhiteSpace(subElement.GetAttributeString("identifier", string.Empty))) - { - DebugConsole.AddWarning($"An event prefab {subElement.Name} in {filePaths[element]} is missing an identifier."); - } - - PrefabList.Add(new EventPrefab(subElement)); - } - break; - case "eventsprites": -#if CLIENT - foreach (var subElement in element.Elements()) - { - string identifier = subElement.GetAttributeString("identifier", string.Empty); - - if (EventSprites.ContainsKey(identifier)) - { - EventSprites[identifier]?.Remove(); - EventSprites[identifier] = new Sprite(subElement); - continue; - } - else - { - EventSprites.Add(identifier, new Sprite(subElement)); - } - } -#endif - break; - } - } - - int i = 0; - foreach (XElement element in configElements) - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "eventset": - List.Add(new EventSet(element, i.ToString())); - i++; - break; - } - } + Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty; + return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; } public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) { List debugLines = new List(); - foreach (var eventSet in List) + foreach (var eventSet in Prefabs) { List stats = new List(); for (int i = 0; i < simulatedRoundCount; i++) @@ -364,7 +321,7 @@ namespace Barotrauma CheckEventSet(newStats, eventSet, filter); stats.Add(newStats); } - debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): "); + debugLines.Add($"Event stats ({eventSet.Identifier}): "); LogEventStats(stats, debugLines, fullLog); } @@ -379,16 +336,18 @@ namespace Barotrauma { for (int i = 0; i < thisSet.EventCount; i++) { - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList()); - if (eventPrefab.Prefabs.Any(p => p != null)) + var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); + if (eventPrefab.EventPrefabs.Any(p => p != null)) { - AddEvents(stats, eventPrefab.Prefabs, filter); + AddEvents(stats, eventPrefab.EventPrefabs, filter); unusedEvents.Remove(eventPrefab); } } } - List values = thisSet.ChildSets.SelectMany(s => s.Commonness.Values).ToList(); - EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values); + List values = thisSet.ChildSets + .SelectMany(s => s.DefaultCommonness.ToEnumerable().Concat(s.OverrideCommonness.Values)) + .ToList(); + EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values, Rand.RandSync.Unsynced); if (childSet != null) { CheckEventSet(stats, childSet, filter); @@ -398,7 +357,7 @@ namespace Barotrauma { foreach (var eventPrefab in thisSet.EventPrefabs) { - AddEvents(stats, eventPrefab.Prefabs, filter); + AddEvents(stats, eventPrefab.EventPrefabs, filter); } foreach (var childSet in thisSet.ChildSets) { @@ -419,7 +378,7 @@ namespace Barotrauma if (Rand.Value() > spawnProbability) { return; } int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } - string character = monsterEvent.speciesName; + Identifier character = monsterEvent.SpeciesName; if (stats.MonsterCounts.TryGetValue(character, out int currentCount)) { if (currentCount >= monsterEvent.MaxAmountPerLevel) { return; } @@ -430,7 +389,7 @@ namespace Barotrauma } stats.MonsterCounts[character] += count; - var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai"); + var aiElement = CharacterPrefab.FindBySpeciesName(character)?.ConfigElement?.GetChildElement("ai"); if (aiElement != null) { stats.MonsterStrength += aiElement.GetAttributeFloat("combatstrength", 0) * count; @@ -447,7 +406,7 @@ namespace Barotrauma } else { - var allMonsters = new Dictionary(); + var allMonsters = new Dictionary(); foreach (var stat in stats) { foreach (var monster in stat.MonsterCounts) @@ -473,7 +432,7 @@ namespace Barotrauma } } - static string LogMonsterCounts(Dictionary stats, float divider = 0) + static string LogMonsterCounts(Dictionary stats, float divider = 0) { if (divider > 0) { @@ -485,5 +444,14 @@ namespace Barotrauma } } } + + public override void Dispose() + { + foreach (var childSet in ChildSets) + { + childSet.Dispose(); + } + + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs index 74f1b1e2a..f716173e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MalfunctionEvent.cs @@ -8,7 +8,7 @@ namespace Barotrauma { class MalfunctionEvent : Event { - private string[] targetItemIdentifiers; + private Identifier[] targetItemIdentifiers; private List targetItems; @@ -36,7 +36,7 @@ namespace Barotrauma decreaseConditionAmount = prefab.ConfigElement.GetAttributeFloat("decreaseconditionamount", 0.0f); duration = prefab.ConfigElement.GetAttributeFloat("duration", 0.0f); - targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("itemidentifiers", new string[0]); + targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty()); } public override bool CanAffectSubImmediately(Level level) @@ -47,11 +47,11 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)); - int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.Server); + int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient); for (int i = 0; i < itemAmount; i++) { if (matchingItems.Count == 0) break; - targetItems.Add(matchingItems[Rand.Int(matchingItems.Count, Rand.RandSync.Server)]); + targetItems.Add(matchingItems[Rand.Int(matchingItems.Count, Rand.RandSync.ServerAndClient)]); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs index 32a2d3c6c..9665b6b90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -23,7 +23,7 @@ namespace Barotrauma protected const int HostagesKilledState = 5; - private readonly string hostagesKilledMessage; + private readonly LocalizedString hostagesKilledMessage; private const float EndDelay = 5.0f; private float endTimer; @@ -81,12 +81,12 @@ namespace Barotrauma public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - characterConfig = prefab.ConfigElement.Element("Characters"); + characterConfig = prefab.ConfigElement.GetChildElement("Characters"); string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", ""); - hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag; + hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag); - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); } @@ -139,14 +139,14 @@ namespace Barotrauma continue; } - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); + Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); if (spawnPoint == null) { - spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); } Vector2 spawnPos = spawnPoint.WorldPosition; if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100) @@ -194,7 +194,7 @@ namespace Barotrauma } else { - string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", "")); + Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty)); var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab == null) { @@ -212,8 +212,8 @@ namespace Barotrauma private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine) { - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); + Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, moduleFlags ?? humanPrefab.GetModuleFlags(), @@ -221,7 +221,7 @@ namespace Barotrauma element.GetAttributeBool("asfaraspossible", false)); if (spawnPos == null) { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); } bool requiresRescue = element.GetAttributeBool("requirerescue", false); @@ -249,12 +249,12 @@ namespace Barotrauma private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine) { - string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); - string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null); + Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null); ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false)); if (spawnPos == null) { - spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced(); } Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false); characters.Add(spawnedCharacter); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs index 993b0cd02..78ddfaed8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -1,3 +1,4 @@ +using System; using Barotrauma.Extensions; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; @@ -8,8 +9,8 @@ namespace Barotrauma { partial class AlienRuinMission : Mission { - private readonly string[] targetItemIdentifiers; - private readonly string[] targetEnemyIdentifiers; + private readonly Identifier[] targetItemIdentifiers; + private readonly Identifier[] targetEnemyIdentifiers; private readonly int minEnemyCount; private readonly HashSet existingTargets = new HashSet(); private readonly HashSet spawnedTargets = new HashSet(); @@ -34,8 +35,8 @@ namespace Barotrauma public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetitems", new string[0], convertToLowerInvariant: true); - targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetenemies", new string[0], convertToLowerInvariant: true); + targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetitems", Array.Empty()); + targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetenemies", Array.Empty()); minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0); } @@ -45,7 +46,7 @@ namespace Barotrauma spawnedTargets.Clear(); allTargets.Clear(); if (IsClient) { return; } - TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins"); @@ -66,8 +67,8 @@ namespace Barotrauma int existingEnemyCount = 0; foreach (var character in Character.CharacterList) { - if (string.IsNullOrEmpty(character.SpeciesName)) { continue; } - if (!targetEnemyIdentifiers.Contains(character.SpeciesName.ToLowerInvariant())) { continue; } + if (character.SpeciesName.IsEmpty) { continue; } + if (!targetEnemyIdentifiers.Contains(character.SpeciesName)) { continue; } if (character.Submarine != TargetRuin.Submarine) { continue; } existingTargets.Add(character); allTargets.Add(character); @@ -76,7 +77,7 @@ namespace Barotrauma if (existingEnemyCount < minEnemyCount) { var enemyPrefabs = new HashSet(); - foreach (string identifier in targetEnemyIdentifiers) + foreach (Identifier identifier in targetEnemyIdentifiers) { var prefab = CharacterPrefab.FindBySpeciesName(identifier); if (prefab != null) @@ -95,8 +96,8 @@ namespace Barotrauma } for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) { - var prefab = enemyPrefabs.GetRandom(); - var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandom(w => w.CurrentHull != null)?.WorldPosition; + var prefab = enemyPrefabs.GetRandomUnsynced(); + var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition; if (!spawnPos.HasValue) { DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index d5ee46335..6b09ce371 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -9,18 +9,48 @@ namespace Barotrauma { partial class BeaconMission : Mission { + private class MonsterSet + { + public readonly HashSet<(CharacterPrefab character, Point amountRange)> MonsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>(); + public float Commonness; + + public MonsterSet(XElement element) + { + Commonness = element.GetAttributeFloat("commonness", 100.0f); + } + } + private bool swarmSpawned; - private readonly string monsterSpeciesName; - private Point monsterCountRange; - private readonly string sonarLabel; + private readonly List monsterSets = new List(); + private readonly LocalizedString sonarLabel; public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { swarmSpawned = false; - XElement monsterElement = prefab.ConfigElement.Element("monster"); + foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) + { + if (!monsterSets.Any()) + { + monsterSets.Add(new MonsterSet(monsterElement)); + } + LoadMonsters(monsterElement, monsterSets[0]); + } + foreach (var monsterSetElement in prefab.ConfigElement.GetChildElements("monsters")) + { + monsterSets.Add(new MonsterSet(monsterSetElement)); + foreach (var monsterElement in monsterSetElement.GetChildElements("monster")) + { + LoadMonsters(monsterElement, monsterSets.Last()); + } + } - monsterSpeciesName = monsterElement.GetAttributeString("character", string.Empty); + sonarLabel = TextManager.Get("beaconstationsonarlabel"); + } + + private void LoadMonsters(XElement monsterElement, MonsterSet set) + { + Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { @@ -28,17 +58,22 @@ namespace Barotrauma } int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255); int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255); - - monsterCountRange = new Point(min, max); - - sonarLabel = TextManager.Get("beaconstationsonarlabel"); + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (characterPrefab != null) + { + set.MonsterPrefabs.Add((characterPrefab, new Point(min, max))); + } + else + { + DebugConsole.ThrowError($"Error in beacon mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } } - public override string SonarLabel + public override LocalizedString SonarLabel { get { - return string.IsNullOrEmpty(base.SonarLabel) ? sonarLabel : base.SonarLabel; + return base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel; } } @@ -101,16 +136,21 @@ namespace Barotrauma } } - int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); - for (int i = 0; i < amount; i++) + var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced); + foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs) { - CoroutineManager.Invoke(() => + int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); + for (int i = 0; i < amount; i++) { - //round ended before the coroutine finished - if (GameMain.GameSession == null || Level.Loaded == null) { return; } - Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos); - }, Rand.Range(0f, amount)); + CoroutineManager.Invoke(() => + { + //round ended before the coroutine finished + if (GameMain.GameSession == null || Level.Loaded == null) { return; } + Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos); + }, Rand.Range(0f, amount)); + } } + swarmSpawned = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index ce5311135..934b9df66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -9,24 +9,25 @@ namespace Barotrauma { partial class CargoMission : Mission { - private readonly XElement itemConfig; + private readonly ContentXElement itemConfig; private readonly List items = new List(); private readonly Dictionary parentInventoryIDs = new Dictionary(); + private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); private float requiredDeliveryAmount; - private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>(); + private readonly List<(ContentXElement element, ItemContainer container)> itemsToSpawn = new List<(ContentXElement element, ItemContainer container)>(); private int? rewardPerCrate; private int calculatedReward; private int maxItemCount; private Submarine sub; - + private readonly List previouslySelectedMissions = new List(); - public override string Description + public override LocalizedString Description { get { @@ -43,7 +44,7 @@ namespace Barotrauma : base(prefab, locations, sub) { this.sub = sub; - itemConfig = prefab.ConfigElement.Element("Items"); + 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 //don't attempt to determine cargo if the sub hasn't been fully loaded @@ -91,7 +92,7 @@ namespace Barotrauma } maxItemCount = 0; - foreach (XElement subElement in itemConfig.Elements()) + foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); maxItemCount += maxCount; @@ -99,7 +100,7 @@ namespace Barotrauma for (int i = 0; i < containers.Count; i++) { - foreach (XElement subElement in itemConfig.Elements()) + foreach (var subElement in itemConfig.Elements()) { int maxCount = subElement.GetAttributeInt("maxcount", 10); if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; } @@ -183,6 +184,7 @@ namespace Barotrauma items.Clear(); parentInventoryIDs.Clear(); parentItemContainerIndices.Clear(); + inventorySlotIndices.Clear(); if (itemConfig == null) { @@ -198,7 +200,7 @@ namespace Barotrauma if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; } } - private void LoadItemAsChild(XElement element, Item parent) + private void LoadItemAsChild(ContentXElement element, Item parent) { ItemPrefab itemPrefab = FindItemPrefab(element); @@ -218,9 +220,10 @@ namespace Barotrauma parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(parent.GetComponent())); parent.Combine(item, user: null); + inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { int amount = subElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 30a7c608c..9e2e4c921 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -6,8 +6,8 @@ namespace Barotrauma { private Submarine[] subs; - private readonly string[] descriptions; - private static string[] teamNames = { "Team A", "Team B" }; + private readonly LocalizedString[] descriptions; + private static LocalizedString[] teamNames = { "Team A", "Team B" }; public override bool AllowRespawn { @@ -23,11 +23,11 @@ namespace Barotrauma } } - public override string SuccessMessage + public override LocalizedString SuccessMessage { get { - if (Winner == CharacterTeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; } + if (Winner == CharacterTeamType.None || base.SuccessMessage.IsNullOrEmpty()) { return ""; } //disable success message for now if it hasn't been translated if (!TextManager.ContainsTag("MissionSuccess." + Prefab.TextIdentifier)) { return ""; } @@ -45,11 +45,11 @@ namespace Barotrauma public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - descriptions = new string[] + descriptions = new LocalizedString[] { - TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("descriptionneutral", ""), - TextManager.Get("MissionDescription1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("description1", ""), - TextManager.Get("MissionDescription2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("description2", "") + TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("descriptionneutral", "")), + TextManager.Get("MissionDescription1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("description1", "")), + TextManager.Get("MissionDescription2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("description2", "")) }; for (int i = 0; i < descriptions.Length; i++) @@ -60,14 +60,14 @@ namespace Barotrauma } } - teamNames = new string[] + teamNames = new LocalizedString[] { - TextManager.Get("MissionTeam1." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname1", "Team A"), - TextManager.Get("MissionTeam2." + prefab.TextIdentifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname2", "Team B") + TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname1", "Team A")), + TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname2", "Team B")) }; } - public static string GetTeamName(CharacterTeamType teamID) + public static LocalizedString GetTeamName(CharacterTeamType teamID) { if (teamID == CharacterTeamType.Team1) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 99bdf971e..a1434ba45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -34,11 +34,11 @@ namespace Barotrauma : base(prefab, locations, sub) { missionSub = sub; - characterConfig = prefab.ConfigElement.Element("Characters"); + characterConfig = prefab.ConfigElement.GetChildElement("Characters"); baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1); scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); - itemConfig = prefab.ConfigElement.Element("TerroristItems"); + itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems"); CalculateReward(); } @@ -87,7 +87,7 @@ namespace Barotrauma characterItems.Clear(); WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); - Rand.RandSync randSync = Rand.RandSync.Server; + Rand.RandSync randSync = Rand.RandSync.ServerAndClient; if (terroristChance > 0f) { @@ -226,8 +226,8 @@ namespace Barotrauma if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands) { character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true)); - character.Speak(TextManager.Get("dialogterroristannounce"), null, Rand.Range(0.5f, 3f)); - XElement randomElement = itemConfig.Elements().GetRandom(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); + character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f)); + XElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty); if (randomElement != null) { HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true); @@ -286,7 +286,8 @@ namespace Barotrauma private bool Survived(Character character) { - return IsAlive(character) && character.CurrentHull != null && character.CurrentHull.Submarine == Submarine.MainSub; + return IsAlive(character) && character.CurrentHull?.Submarine != null && + (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)); } private bool IsAlive(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index e407ba68d..c4a787a40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -9,10 +9,23 @@ namespace Barotrauma { partial class MineralMission : Mission { - private readonly Dictionary resourceClusters = new Dictionary(); - private readonly Dictionary> spawnedResources = new Dictionary>(); - private readonly Dictionary relevantLevelResources = new Dictionary(); - private readonly List> missionClusterPositions = new List>(); + private struct ResourceCluster + { + public int Amount; + public float Rotation; + + public ResourceCluster(int amount, float rotation) + { + Amount = amount; + Rotation = rotation; + } + + public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation); + } + private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary> spawnedResources = new Dictionary>(); + private readonly Dictionary relevantLevelResources = new Dictionary(); + private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>(); private readonly HashSet caves = new HashSet(); @@ -28,14 +41,14 @@ namespace Barotrauma public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - var configElement = prefab.ConfigElement.Element("Items"); + var configElement = prefab.ConfigElement.GetChildElement("Items"); foreach (var c in configElement.GetChildElements("Item")) { - var identifier = c.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) { continue; } + var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { continue; } if (resourceClusters.ContainsKey(identifier)) { - resourceClusters[identifier] = (resourceClusters[identifier].amount + 1, resourceClusters[identifier].rotation); + resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation); } else { @@ -88,11 +101,11 @@ namespace Barotrauma "couldn't find an item prefab with the identifier " + kvp.Key); continue; } - var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.amount, out float rotation); - if (spawnedResources.Count < kvp.Value.amount) + var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.Amount, out float rotation); + if (spawnedResources.Count < kvp.Value.Amount) { DebugConsole.ThrowError("Error in MineralMission - " + - "spawned " + spawnedResources.Count + "/" + kvp.Value.amount + " of " + prefab.Name); + "spawned " + spawnedResources.Count + "/" + kvp.Value.Amount + " of " + prefab.Name); } if (spawnedResources.None()) { continue; } this.spawnedResources.Add(kvp.Key, spawnedResources); @@ -176,8 +189,8 @@ namespace Barotrauma { if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { - var collected = availableResources.Count(r => HasBeenCollected(r)); - var needed = kvp.Value.amount; + var collected = availableResources.Count(HasBeenCollected); + var needed = kvp.Value.Amount; if (collected < needed) { return false; } } else @@ -221,7 +234,7 @@ namespace Barotrauma itemCount++; } pos /= itemCount; - missionClusterPositions.Add(new Tuple(kvp.Key, pos)); + missionClusterPositions.Add((kvp.Key, pos)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 097d2ca16..0bccbf953 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -3,6 +3,7 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -37,36 +38,33 @@ namespace Barotrauma protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - public readonly List Headers; - public readonly List Messages; - - public string Name - { - get { return Prefab.Name; } - } + public readonly ImmutableArray Headers; + public readonly ImmutableArray Messages; - private readonly string successMessage; - public virtual string SuccessMessage + public LocalizedString Name => Prefab.Name; + + private readonly LocalizedString successMessage; + public virtual LocalizedString SuccessMessage { get { return successMessage; } //private set { successMessage = value; } } - private readonly string failureMessage; - public virtual string FailureMessage + private readonly LocalizedString failureMessage; + public virtual LocalizedString FailureMessage { get { return failureMessage; } //private set { failureMessage = value; } } - protected string description; - public virtual string Description + protected LocalizedString description; + public virtual LocalizedString Description { get { return description; } //private set { description = value; } } - protected string descriptionWithoutReward; + protected LocalizedString descriptionWithoutReward; public virtual bool AllowUndocking { @@ -81,7 +79,7 @@ namespace Barotrauma } } - public Dictionary ReputationRewards + public Dictionary ReputationRewards { get { return Prefab.ReputationRewards; } } @@ -116,15 +114,10 @@ namespace Barotrauma { get { return Enumerable.Empty(); } } - - public virtual string SonarLabel - { - get { return Prefab.SonarLabel; } - } - public string SonarIconIdentifier - { - get { return Prefab.SonarIconIdentifier; } - } + + public virtual LocalizedString SonarLabel => Prefab.SonarLabel; + + public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier; public readonly Location[] Locations; @@ -155,11 +148,11 @@ namespace Barotrauma Prefab = prefab; - description = prefab.Description; - successMessage = prefab.SuccessMessage; - failureMessage = prefab.FailureMessage; - Headers = new List(prefab.Headers); - Messages = new List(prefab.Messages); + description = prefab.Description.Value; + successMessage = prefab.SuccessMessage.Value; + failureMessage = prefab.FailureMessage.Value; + Headers = prefab.Headers; + var messages = prefab.Messages.ToArray(); Locations = locations; @@ -169,9 +162,9 @@ namespace Barotrauma if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); } if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); } - for (int m = 0; m < Messages.Count; m++) + for (int m = 0; m < messages.Length; m++) { - Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName); + messages[m] = messages[m].Replace("[location" + (n + 1) + "]", locationName); } } string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖"; @@ -182,10 +175,12 @@ namespace Barotrauma } if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); } if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); } - for (int m = 0; m < Messages.Count; m++) + for (int m = 0; m < messages.Length; m++) { - Messages[m] = Messages[m].Replace("[reward]", rewardText); + messages[m] = messages[m].Replace("[reward]", rewardText); } + + Messages = messages.ToImmutableArray(); } public virtual void SetLevel(LevelData level) { } @@ -204,7 +199,7 @@ namespace Barotrauma } else { - allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.Type)) != 0)); + allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => ((int)(missionType & m.Type)) != 0)); } allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); @@ -246,7 +241,7 @@ namespace Barotrauma delayedTriggerEvents.Clear(); foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) { - foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false)) + foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false)) { entityToShow.HiddenInGame = false; } @@ -318,7 +313,7 @@ namespace Barotrauma private void TriggerEvent(MissionPrefab.TriggerEvent trigger) { if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; } - var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier.Equals(trigger.EventIdentifier, StringComparison.OrdinalIgnoreCase)); + var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier); if (eventPrefab == null) { DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\")."); @@ -384,23 +379,23 @@ namespace Barotrauma int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); campaign.Money += totalReward; - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier); + GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); foreach (Character character in crewCharacters) { character.Info.MissionsCompletedSinceDeath++; } - foreach (KeyValuePair reputationReward in ReputationRewards) + foreach (KeyValuePair reputationReward in ReputationRewards) { - if (reputationReward.Key.Equals("location", StringComparison.OrdinalIgnoreCase)) + if (reputationReward.Key == "location") { Locations[0].Reputation.AddReputation(reputationReward.Value); Locations[1].Reputation.AddReputation(reputationReward.Value); } else { - Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier.Equals(reputationReward.Key, StringComparison.OrdinalIgnoreCase)); + Faction faction = campaign.Factions.Find(faction1 => faction1.Prefab.Identifier == reputationReward.Key); if (faction != null) { faction.Reputation.AddReputation(reputationReward.Value); } } } @@ -422,7 +417,7 @@ namespace Barotrauma int srcIndex = -1; for (int i = 0; i < Locations.Length; i++) { - if (Locations[i].Type.Identifier.Equals(change.CurrentType, StringComparison.OrdinalIgnoreCase)) + if (Locations[i].Type.Identifier == change.CurrentType) { srcIndex = i; break; @@ -437,7 +432,7 @@ namespace Barotrauma } else { - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase))); + location.ChangeType(LocationType.Prefabs[change.ChangeToType]); location.LocationTypeChangeCooldown = change.CooldownAfterChange; } } @@ -455,8 +450,8 @@ namespace Barotrauma return null; } - string characterIdentifier = element.GetAttributeString("identifier", ""); - string characterFrom = element.GetAttributeString("from", ""); + Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty); HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { @@ -467,9 +462,9 @@ namespace Barotrauma return humanPrefab; } - protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true) + protected Character CreateHuman(HumanPrefab humanPrefab, List characters, Dictionary> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true) { - var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); + var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.ServerAndClient) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobOrJobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); characterInfo.TeamID = teamType; if (positionToStayIn == null) @@ -480,9 +475,9 @@ namespace Barotrauma } Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); - spawnedCharacter.Prefab = humanPrefab; + spawnedCharacter.HumanPrefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); - humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false); + humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.ServerAndClient, createNetworkEvents: false); characters.Add(spawnedCharacter); characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); @@ -536,7 +531,7 @@ namespace Barotrauma cargoRoomSub = cargoRoom.Submarine; return new Vector2( - cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), + cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.ServerAndClient), cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index c6b93aab0..8b6c7e1a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Xml.Linq; @@ -27,9 +28,9 @@ namespace Barotrauma All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins } - partial class MissionPrefab + partial class MissionPrefab : PrefabWithUintIdentifier { - public static readonly List List = new List(); + public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static readonly Dictionary CoOpMissionClasses = new Dictionary() { @@ -52,15 +53,14 @@ namespace Barotrauma }; public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; - + private readonly ConstructorInfo constructor; public readonly MissionType Type; public readonly bool MultiplayerOnly, SingleplayerOnly; - public readonly string Identifier; - public readonly string TextIdentifier; + public readonly Identifier TextIdentifier; private readonly string[] tags; public IEnumerable Tags @@ -68,17 +68,19 @@ namespace Barotrauma get { return tags; } } - public readonly string Name; - public readonly string Description; - public readonly string SuccessMessage; - public readonly string FailureMessage; - public readonly string SonarLabel; - public readonly string SonarIconIdentifier; + public readonly LocalizedString Name; + public readonly LocalizedString Description; + public readonly LocalizedString SuccessMessage; + public readonly LocalizedString FailureMessage; + public readonly LocalizedString SonarLabel; + public readonly Identifier SonarIconIdentifier; - public readonly string AchievementIdentifier; + public readonly Identifier AchievementIdentifier; - public readonly Dictionary ReputationRewards = new Dictionary(); - public readonly List> DataRewards = new List>(); + public readonly Dictionary ReputationRewards = new Dictionary(); + + public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> + DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); public readonly int Commonness; public readonly int? Difficulty; @@ -86,8 +88,8 @@ namespace Barotrauma public readonly int Reward; - public readonly List Headers; - public readonly List Messages; + public readonly ImmutableArray Headers; + public readonly ImmutableArray Messages; public readonly bool AllowRetry; @@ -98,12 +100,12 @@ namespace Barotrauma /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// - public readonly List<(string from, string to)> AllowedConnectionTypes; + public readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes; /// /// The mission can only be received in these location types /// - public readonly List AllowedLocationTypes = new List(); + public readonly List AllowedLocationTypes = new List(); /// /// Show entities belonging to these sub categories when the mission starts @@ -112,16 +114,16 @@ namespace Barotrauma public class TriggerEvent { - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string EventIdentifier { get; private set; } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int State { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float Delay { get; private set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool CampaignOnly { get; private set; } public TriggerEvent(XElement element) @@ -134,66 +136,18 @@ namespace Barotrauma public LocationTypeChange LocationTypeChangeOnCompleted; - public readonly XElement ConfigElement; + public readonly ContentXElement ConfigElement; - public static void Init() - { - List.Clear(); - var files = GameMain.Instance.GetFilesOfType(ContentType.Missions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - bool allowOverride = false; - var mainElement = doc.Root; - if (mainElement.IsOverride()) - { - allowOverride = true; - mainElement = mainElement.FirstElement(); - } - - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - var identifier = element.GetAttributeString("identifier", string.Empty); - var duplicate = List.Find(m => m.Identifier == identifier); - if (duplicate != null) - { - if (allowOverride || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding a mission with the identifier '{identifier}' using the file '{file.Path}'", Color.Yellow); - List.Remove(duplicate); - } - else - { - DebugConsole.ThrowError($"Duplicate mission found with the identifier '{identifier}' in file '{file.Path}'! Add tags as the parent of the mission definition to allow overriding."); - // TODO: Don't allow adding duplicates when the issue with multiple missions is solved. - //continue; - } - } - List.Add(new MissionPrefab(element)); - } - } - } - - public MissionPrefab(XElement element) + public MissionPrefab(ContentXElement element, MissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { ConfigElement = element; - Identifier = element.GetAttributeString("identifier", ""); - TextIdentifier = element.GetAttributeString("textidentifier", null) ?? Identifier; + TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); - tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true); + tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); - Name = TextManager.Get("MissionName." + TextIdentifier, true); - if (Name == null) - { -#if DEBUG - DebugConsole.ThrowError($"Error in mission \"{Identifier}\" - could not find a name in localization files. Make sure the texts are present in the loca file or that the mission is set to share texts with another mission using the TextIdentifier attribute."); -#endif - Name = element.GetAttributeString("name", ""); - } - Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", ""); + Name = TextManager.Get($"MissionName.{TextIdentifier}").Fallback(element.GetAttributeString("name", "")); + Description = TextManager.Get($"MissionDescription.{TextIdentifier}").Fallback(element.GetAttributeString("description", "")); Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); @@ -205,87 +159,73 @@ namespace Barotrauma Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } - SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully"); - FailureMessage = TextManager.Get("MissionFailure." + TextIdentifier, true) ?? ""; - if (string.IsNullOrEmpty(FailureMessage) && TextManager.ContainsTag("missionfailed")) - { - FailureMessage = TextManager.Get("missionfailed", returnNull: true) ?? ""; - } - if (string.IsNullOrEmpty(FailureMessage) && GameMain.Config.Language == "English") - { - FailureMessage = element.GetAttributeString("failuremessage", ""); - } + 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", "") : ""); - if (element.Attribute("sonarlabel") == null) - { - SonarLabel = - TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? - TextManager.Get("missionsonarlabel.target"); - } - else - { - SonarLabel = - TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ?? - TextManager.Get(element.GetAttributeString("sonarlabel", ""), true) ?? - element.GetAttributeString("sonarlabel", ""); - } - - SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); + SonarLabel = + TextManager.Get($"MissionSonarLabel.{TextIdentifier}").Fallback( + TextManager.Get($"MissionSonarLabel.{element.GetAttributeString("sonarlabel", "")}")).Fallback( + element.GetAttributeString("sonarlabel", "")); + SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", ""); MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false); - AchievementIdentifier = element.GetAttributeString("achievementidentifier", ""); + AchievementIdentifier = element.GetAttributeIdentifier("achievementidentifier", ""); - UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", new string[0]).ToList(); + UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", Array.Empty()).ToList(); - Headers = new List(); - Messages = new List(); - AllowedConnectionTypes = new List<(string from, string to)>(); + var headers = new List(); + var messages = new List(); + AllowedConnectionTypes = new List<(Identifier from, Identifier to)>(); for (int i = 0; i < 100; i++) { - string header = TextManager.Get("MissionHeader" + i + "." + TextIdentifier, true); - string message = TextManager.Get("MissionMessage" + i + "." + TextIdentifier, true); - if (!string.IsNullOrEmpty(message)) + LocalizedString header = TextManager.Get($"MissionHeader{i}.{TextIdentifier}"); + LocalizedString message = TextManager.Get($"MissionMessage{i}.{TextIdentifier}"); + if (!message.IsNullOrEmpty()) { - Headers.Add(header); - Messages.Add(message); + headers.Add(header); + messages.Add(message); } } - + int messageIndex = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "message": - if (messageIndex > Headers.Count - 1) + if (messageIndex > headers.Count - 1) { - Headers.Add(string.Empty); - Messages.Add(string.Empty); + headers.Add(string.Empty); + messages.Add(string.Empty); } - Headers[messageIndex] = TextManager.Get("MissionHeader" + messageIndex + "." + TextIdentifier, true) ?? subElement.GetAttributeString("header", ""); - Messages[messageIndex] = TextManager.Get("MissionMessage" + messageIndex + "." + TextIdentifier, true) ?? subElement.GetAttributeString("text", ""); + headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("header", "")); + messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("text", "")); messageIndex++; break; case "locationtype": case "connectiontype": if (subElement.Attribute("identifier") != null) { - AllowedLocationTypes.Add(subElement.GetAttributeString("identifier", "")); + AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", "")); } else { - AllowedConnectionTypes.Add((subElement.GetAttributeString("from", "").ToLowerInvariant(), subElement.GetAttributeString("to", "").ToLowerInvariant())); + AllowedConnectionTypes.Add(( + subElement.GetAttributeIdentifier("from", ""), + subElement.GetAttributeIdentifier("to", ""))); } break; case "locationtypechange": - LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeString("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); + LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeIdentifier("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); break; case "reputation": case "reputationreward": - string factionIdentifier = subElement.GetAttributeString("identifier", ""); + Identifier factionIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); float amount = subElement.GetAttributeFloat("amount", 0.0f); if (ReputationRewards.ContainsKey(factionIdentifier)) { @@ -293,18 +233,11 @@ namespace Barotrauma continue; } ReputationRewards.Add(factionIdentifier, amount); - if (!factionIdentifier.Equals("location", StringComparison.OrdinalIgnoreCase)) - { - if (FactionPrefab.Prefabs != null && !FactionPrefab.Prefabs.Any(p => p.Identifier.Equals(factionIdentifier, StringComparison.OrdinalIgnoreCase))) - { - DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Could not find a faction with the identifier \"{factionIdentifier}\"."); - } - } break; case "metadata": - string identifier = subElement.GetAttributeString("identifier", string.Empty); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); string stringValue = subElement.GetAttributeString("value", string.Empty); - if (!string.IsNullOrWhiteSpace(stringValue) && !string.IsNullOrWhiteSpace(identifier)) + if (!string.IsNullOrWhiteSpace(stringValue) && !identifier.IsEmpty) { object value = SetDataAction.ConvertXMLValue(stringValue); SetDataAction.OperationType operation = SetDataAction.OperationType.Set; @@ -315,7 +248,7 @@ namespace Barotrauma operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString); } - DataRewards.Add(Tuple.Create(identifier, value, operation)); + DataRewards.Add((identifier, value, operation)); } break; case "triggerevent": @@ -323,15 +256,17 @@ namespace Barotrauma break; } } + Headers = headers.ToImmutableArray(); + Messages = messages.ToImmutableArray(); - string missionTypeName = element.GetAttributeString("type", ""); + Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); //backwards compatibility - if (missionTypeName.Equals("outpostdestroy", StringComparison.OrdinalIgnoreCase) || missionTypeName.Equals("outpostrescue", StringComparison.OrdinalIgnoreCase)) + if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue") { - missionTypeName = "AbandonedOutpost"; + missionTypeName = "AbandonedOutpost".ToIdentifier(); } - if (!Enum.TryParse(missionTypeName, out Type)) + if (!Enum.TryParse(missionTypeName.Value, true, out Type)) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); return; @@ -362,25 +297,25 @@ namespace Barotrauma InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public bool IsAllowed(Location from, Location to) { if (from == to) { return - AllowedLocationTypes.Any(lt => lt.Equals("any", StringComparison.OrdinalIgnoreCase)) || - AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)); + AllowedLocationTypes.Any(lt => lt == "any") || + AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } - foreach ((string fromType, string toType) in AllowedConnectionTypes) + foreach (var (fromType, toType) in AllowedConnectionTypes) { - if (fromType.Equals("any", StringComparison.OrdinalIgnoreCase) || - fromType.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase) || + if (fromType == "any" || + fromType == from.Type.Identifier || (fromType == "anyoutpost" && from.HasOutpost())) { - if (toType.Equals("any", StringComparison.OrdinalIgnoreCase) || - toType.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase) || + if (toType == "any" || + toType == to.Type.Identifier || (toType == "anyoutpost" && to.HasOutpost())) { return true; @@ -406,5 +341,11 @@ namespace Barotrauma { return constructor?.Invoke(new object[] { this, locations, sub }) as Mission; } + + partial void DisposeProjectSpecific(); + public override void Dispose() + { + DisposeProjectSpecific(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 31109da8e..ec2aea487 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -36,8 +36,8 @@ namespace Barotrauma public MonsterMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null); - if (!string.IsNullOrEmpty(speciesName)) + Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("monsterfile", Identifier.Empty); + if (!speciesName.IsEmpty) { var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); if (characterPrefab != null) @@ -62,7 +62,7 @@ namespace Barotrauma foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { - speciesName = monsterElement.GetAttributeString("character", string.Empty); + speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { @@ -83,10 +83,10 @@ namespace Barotrauma if (monsterPrefabs.Any()) { - var characterParams = new CharacterParams(monsterPrefabs.First().character.FilePath); + var characterParams = new CharacterParams(monsterPrefabs.First().character.ContentFile as CharacterFile); description = description.Replace("[monster]", - TextManager.Get("character." + characterParams.SpeciesTranslationOverride, returnNull: true) ?? - TextManager.Get("character." + characterParams.SpeciesName)); + TextManager.Get("character." + characterParams.SpeciesTranslationOverride).Fallback( + TextManager.Get("character." + characterParams.SpeciesName))); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 210d5557c..5a85ee491 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -11,7 +11,7 @@ namespace Barotrauma { partial class NestMission : Mission { - private readonly XElement itemConfig; + private readonly ContentXElement itemConfig; private readonly List items = new List(); private readonly Dictionary statusEffectOnApproach = new Dictionary(); @@ -49,7 +49,7 @@ namespace Barotrauma public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); itemSpawnRadius = prefab.ConfigElement.GetAttributeFloat("itemspawnradius", 800.0f); approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f); @@ -69,7 +69,7 @@ namespace Barotrauma foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { - string speciesName = monsterElement.GetAttributeString("character", string.Empty); + Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty); int defaultCount = monsterElement.GetAttributeInt("count", -1); if (defaultCount < 0) { @@ -170,7 +170,7 @@ namespace Barotrauma } } - foreach (XElement subElement in itemConfig.Elements()) + foreach (var subElement in itemConfig.Elements()) { string itemIdentifier = subElement.GetAttributeString("identifier", ""); if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) @@ -183,8 +183,8 @@ namespace Barotrauma float rotation = 0.0f; if (spawnEdges.Any()) { - var edge = spawnEdges.GetRandom(Rand.RandSync.Server); - spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.Server)); + var edge = spawnEdges.GetRandom(Rand.RandSync.ServerAndClient); + spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.ServerAndClient)); Vector2 normal = Vector2.UnitY; if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) { @@ -204,10 +204,12 @@ namespace Barotrauma item.FindHull(); items.Add(item); - var statusEffectElement = subElement.Element("StatusEffectOnApproach") ?? subElement.Element("statuseffectonapproach"); + var statusEffectElement = + subElement.GetChildElement("StatusEffectOnApproach") + ?? subElement.GetChildElement("statuseffectonapproach"); if (statusEffectElement != null) { - statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier)); + statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier.Value)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index bc69390bf..6e0b0abc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -80,9 +80,9 @@ namespace Barotrauma public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - submarineTypeConfig = prefab.ConfigElement.Element("SubmarineTypes"); - characterConfig = prefab.ConfigElement.Element("Characters"); - characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes"); + submarineTypeConfig = prefab.ConfigElement.GetChildElement("SubmarineTypes"); + characterConfig = prefab.ConfigElement.GetChildElement("Characters"); + characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes"); addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0); // for campaign missions, set level at construction @@ -111,21 +111,21 @@ namespace Barotrauma string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } - string submarinePath = submarineConfig.GetAttributeString("path", string.Empty); - if (submarinePath == string.Empty) + ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage); + if (submarinePath.IsNullOrEmpty()) { DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); return; } // maybe a little redundant - var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarinePath); + var contentFile = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).FirstOrDefault(x => x.Path == submarinePath); if (contentFile == null) { DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); return; } - submarineInfo = new SubmarineInfo(contentFile.Path); + submarineInfo = new SubmarineInfo(contentFile.Path.Value); } private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) @@ -183,7 +183,7 @@ namespace Barotrauma var validNodes = path.Nodes.FindAll(n => !Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(n.WorldPosition)))); if (validNodes.Any()) { - preferredSpawnPos = validNodes.GetRandom().WorldPosition; // spawn the sub in a random point in the path if possible + preferredSpawnPos = validNodes.GetRandomUnsynced().WorldPosition; // spawn the sub in a random point in the path if possible } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 0bbb70d87..7526d1b25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -63,7 +63,7 @@ namespace Barotrauma string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null); if (itemIdentifier != null) { - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab; } if (itemPrefab == null) { @@ -86,22 +86,22 @@ namespace Barotrauma spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } - foreach (XElement element in prefab.ConfigElement.Elements()) + foreach (var element in prefab.ConfigElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "statuseffect": { - var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name); + var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name.Value); if (newEffect == null) { continue; } statusEffects.Add(new List { newEffect }); break; } case "chooserandom": statusEffects.Add(new List()); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name); + var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name.Value); if (newEffect == null) { continue; } statusEffects.Last().Add(newEffect); } @@ -200,12 +200,13 @@ namespace Barotrauma } if (validContainers.Any()) { - var selectedContainer = validContainers.GetRandom(Rand.RandSync.Unsynced); + var selectedContainer = validContainers.GetRandomUnsynced(); if (selectedContainer.Combine(item, user: null)) { #if SERVER originalInventoryID = selectedContainer.Item.ID; originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); + originalSlotIndex = item.ParentInventory?.FindIndex(item) ?? -1; #endif } // Placement successful } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs index c84e43647..132767396 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -10,10 +10,11 @@ namespace Barotrauma { partial class ScanMission : Mission { - private readonly XElement itemConfig; + private readonly ContentXElement itemConfig; private readonly List startingItems = new List(); private readonly List scanners = new List(); private readonly Dictionary parentInventoryIDs = new Dictionary(); + private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); private readonly int targetsToScan; private readonly Dictionary scanTargets = new Dictionary(); @@ -55,7 +56,7 @@ namespace Barotrauma public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { - itemConfig = prefab.ConfigElement.Element("Items"); + itemConfig = prefab.ConfigElement.GetChildElement("Items"); targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); } @@ -78,7 +79,7 @@ namespace Barotrauma } GetScanners(); - TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins"); @@ -101,7 +102,7 @@ namespace Barotrauma availableWaypoints.AddRange(ruinWaypoints); for (int i = 0; i < targetsToScan; i++) { - var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.Server); + var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient); scanTargets.Add(selectedWaypoint, false); availableWaypoints.Remove(selectedWaypoint); if (i < (targetsToScan - 1)) @@ -143,6 +144,7 @@ namespace Barotrauma { startingItems.Clear(); parentInventoryIDs.Clear(); + inventorySlotIndices.Clear(); parentItemContainerIndices.Clear(); scanners.Clear(); TargetRuin = null; @@ -162,6 +164,7 @@ namespace Barotrauma parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer)); parent.Combine(item, user: null); + inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1); } foreach (XElement subElement in element.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index bae60c6e4..4093d6923 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -9,8 +9,8 @@ namespace Barotrauma { class MonsterEvent : Event { - public readonly string speciesName; - public readonly int minAmount, maxAmount; + public readonly Identifier SpeciesName; + public readonly int MinAmount, MaxAmount; private List monsters; private readonly float scatter; @@ -31,8 +31,6 @@ namespace Barotrauma public List Monsters => monsters; public Vector2? SpawnPos => spawnPos; public bool SpawnPending => spawnPending; - public int MinAmount => minAmount; - public int MaxAmount => maxAmount; public override Vector2 DebugDrawPos { @@ -41,38 +39,42 @@ namespace Barotrauma public override string ToString() { - if (maxAmount <= 1) + if (MaxAmount <= 1) { - return $"MonsterEvent ({speciesName}, {SpawnPosType})"; + return $"MonsterEvent ({SpeciesName}, {SpawnPosType})"; } - else if (minAmount < maxAmount) + else if (MinAmount < MaxAmount) { - return $"MonsterEvent ({speciesName} x{minAmount}-{maxAmount}, {SpawnPosType})"; + return $"MonsterEvent ({SpeciesName} x{MinAmount}-{MaxAmount}, {SpawnPosType})"; } else { - return $"MonsterEvent ({speciesName} x{maxAmount}, {SpawnPosType})"; + return $"MonsterEvent ({SpeciesName} x{MaxAmount}, {SpawnPosType})"; } } public MonsterEvent(EventPrefab prefab) : base (prefab) { - speciesName = prefab.ConfigElement.GetAttributeString("characterfile", ""); - CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesName); + string speciesFile = prefab.ConfigElement.GetAttributeString("characterfile", ""); + CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(speciesFile); if (characterPrefab != null) { - speciesName = characterPrefab.Identifier; + SpeciesName = characterPrefab.Identifier; + } + else + { + SpeciesName = speciesFile.ToIdentifier(); } - if (string.IsNullOrEmpty(speciesName)) + if (SpeciesName.IsEmpty) { throw new Exception("speciesname is null!"); } int defaultAmount = prefab.ConfigElement.GetAttributeInt("amount", 1); - minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); - maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount); + MinAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount); + MaxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), MinAmount); MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue); @@ -97,10 +99,10 @@ namespace Barotrauma if (GameMain.NetworkMember != null) { - List monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList(); - string tryKey = monsterNames.Find(s => speciesName.ToLower() == s.ToLower()); + List monsterNames = GameMain.NetworkMember.ServerSettings.MonsterEnabled.Keys.ToList(); + Identifier tryKey = monsterNames.Find(s => SpeciesName == s); - if (!string.IsNullOrWhiteSpace(tryKey)) + if (!tryKey.IsEmpty) { if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) { @@ -117,15 +119,15 @@ namespace Barotrauma public override IEnumerable GetFilesToPreload() { - string path = CharacterPrefab.FindBySpeciesName(speciesName)?.FilePath; - if (string.IsNullOrWhiteSpace(path)) + var file = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ContentFile; + if (file == null) { - DebugConsole.ThrowError($"Failed to find config file for species \"{speciesName}\""); + DebugConsole.ThrowError($"Failed to find config file for species \"{SpeciesName}\""); yield break; } else { - yield return new ContentFile(path, ContentType.Character); + yield return file; } } @@ -137,9 +139,9 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { - DebugConsole.NewMessage("Initialized MonsterEvent (" + speciesName + ")", Color.White); + DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White); } } @@ -283,7 +285,7 @@ namespace Barotrauma Finished(); return; } - chosenPosition = availablePositions.GetRandom(); + chosenPosition = availablePositions.GetRandomUnsynced(); } if (chosenPosition.IsValid) { @@ -369,7 +371,7 @@ namespace Barotrauma { if (MaxAmountPerLevel < int.MaxValue) { - if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= MaxAmountPerLevel) + if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel) { disallowed = true; return; @@ -456,7 +458,7 @@ namespace Barotrauma spawnPending = false; //+1 because Range returns an integer less than the max value - int amount = Rand.Range(minAmount, maxAmount + 1); + int amount = Rand.Range(MinAmount, MaxAmount + 1); monsters = new List(); float scatterAmount = scatter; if (SpawnPosType.HasFlag(Level.PositionType.SidePath)) @@ -501,7 +503,7 @@ namespace Barotrauma } } - Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); + Character createdCharacter = Character.Create(SpeciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); var eventManager = GameMain.GameSession.EventManager; if (eventManager != null) { @@ -545,7 +547,7 @@ namespace Barotrauma if (GameMain.GameSession != null) { GameAnalyticsManager.AddDesignEvent( - $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"}:{SpawnPosType}:{speciesName}", + $"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}", value: Timing.TotalTime - GameMain.GameSession.RoundStartTime); } }, delayBetweenSpawns * i); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 5be24397f..c41676ff9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -7,9 +7,9 @@ namespace Barotrauma { class ScriptedEvent : Event { - private readonly Dictionary>> targetPredicates = new Dictionary>>(); + private readonly Dictionary>> targetPredicates = new Dictionary>>(); - private readonly Dictionary> cachedTargets = new Dictionary>(); + private readonly Dictionary> cachedTargets = new Dictionary>(); private int prevEntityCount; private int prevPlayerCount, prevBotCount; @@ -18,7 +18,7 @@ namespace Barotrauma public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); - public Dictionary> Targets { get; } = new Dictionary>(); + public Dictionary> Targets { get; } = new Dictionary>(); public override string ToString() { @@ -27,7 +27,7 @@ namespace Barotrauma public ScriptedEvent(EventPrefab prefab) : base(prefab) { - foreach (XElement element in prefab.ConfigElement.Elements()) + foreach (var element in prefab.ConfigElement.Elements()) { if (element.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { @@ -49,7 +49,7 @@ namespace Barotrauma GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start"); } - public void AddTarget(string tag, Entity target) + public void AddTarget(Identifier tag, Entity target) { if (target == null) { @@ -74,7 +74,7 @@ namespace Barotrauma } } - public void AddTargetPredicate(string tag, Predicate predicate) + public void AddTargetPredicate(Identifier tag, Predicate predicate) { if (!targetPredicates.ContainsKey(tag)) { @@ -88,7 +88,7 @@ namespace Barotrauma } } - public IEnumerable GetTargets(string tag) + public IEnumerable GetTargets(Identifier tag) { if (cachedTargets.ContainsKey(tag)) { @@ -140,9 +140,9 @@ namespace Barotrauma return targetsToReturn; } - public void RemoveTag(string tag) + public void RemoveTag(Identifier tag) { - if (string.IsNullOrWhiteSpace(tag)) { return; } + if (tag.IsEmpty) { return; } if (Targets.ContainsKey(tag)) { Targets.Remove(tag); } if (cachedTargets.ContainsKey(tag)) { cachedTargets.Remove(tag); } if (targetPredicates.ContainsKey(tag)) { targetPredicates.Remove(tag); } @@ -224,7 +224,7 @@ namespace Barotrauma foreach (LocationConnection c in currLocation.Connections) { if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; } - if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase))) + if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier == t)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 89327c8f0..4039e31dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System; using System.Linq; +using System.Collections.Immutable; namespace Barotrauma.Extensions { @@ -9,69 +10,102 @@ namespace Barotrauma.Extensions /// /// Randomizes the collection (using OrderBy) and returns it. /// - public static IOrderedEnumerable Randomize(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T[] Randomize(this IList source, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - return source.OrderBy(i => Rand.Value(randSync)); + return source.OrderBy(i => Rand.Value(randSync)).ToArray(); } /// /// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm. /// public static void Shuffle(this IList list, Rand.RandSync randSync = Rand.RandSync.Unsynced) + => list.Shuffle(Rand.GetRNG(randSync)); + + public static void Shuffle(this IList list, Random rng) { int n = list.Count; while (n > 1) { n--; - int k = Rand.Int(n + 1, randSync); + int k = rng.Next(n + 1); T value = list[k]; list[k] = list[n]; list[n] = value; } } - public static T GetRandom(this IEnumerable source, Func predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T GetRandom(this IReadOnlyList source, Func predicate, Rand.RandSync randSync) { if (predicate == null) { return GetRandom(source, randSync); } - return source.Where(predicate).GetRandom(randSync); + return source.Where(predicate).ToArray().GetRandom(randSync); } - public static T GetRandom(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) + /// + /// Gets a random element of a list using one of the synced random number generators. + /// It's recommended that you guarantee a deterministic order of the elements of the + /// input list via sorting. + /// + /// List to pick a random element from + /// Which RNG to use + /// A random item from the list. Return value should match between clients and + /// the server, if applicable. + public static T GetRandom(this IReadOnlyList source, Rand.RandSync randSync) { - if (source is IList list) + int count = source.Count; + return count == 0 ? default : source[Rand.Range(0, count, randSync)]; + } + + public static T GetRandom(this IReadOnlyList source, Random random) + { + int count = source.Count; + return count == 0 ? default : source[random.Next(0, count)]; + } + + // The reason these "GetRandomUnsynced" methods exist is because + // they can be used on all enumerables; GetRandom can only be used + // on lists as they can be sorted to guarantee a certain order. + public static T GetRandomUnsynced(this IEnumerable source, Func predicate) + { + if (predicate == null) { return GetRandomUnsynced(source); } + return source.Where(predicate).GetRandomUnsynced(); + } + + public static T GetRandomUnsynced(this IEnumerable source) + { + if (source is IReadOnlyList list) { - int count = list.Count; - return count == 0 ? default : list[Rand.Range(0, count, randSync)]; + return list.GetRandom(Rand.RandSync.Unsynced); } else { int count = source.Count(); - return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync)); + return count == 0 ? default : source.ElementAt(Rand.Range(0, count, Rand.RandSync.Unsynced)); } } - public static T GetRandom(this IEnumerable source, Random random) + + public static T GetRandom(this IEnumerable source, Rand.RandSync randSync) + where T : PrefabWithUintIdentifier { - if (source is IList list) - { - int count = list.Count; - return count == 0 ? default : list[random.Next(0, count)]; - } - else - { - int count = source.Count(); - return count == 0 ? default : source.ElementAt(random.Next(0, count)); - } + return source.OrderBy(p => p.UintIdentifier).ToArray().GetRandom(randSync); } - public static T RandomElementByWeight(this IEnumerable source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T GetRandom(this IEnumerable source, Func predicate, Rand.RandSync randSync) + where T : PrefabWithUintIdentifier + { + return source.Where(predicate).OrderBy(p => p.UintIdentifier).ToArray().GetRandom(randSync); + } + + + public static T RandomElementByWeight(this IList source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) { float totalWeight = source.Sum(weightSelector); float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight; float currentWeightIndex = 0; - foreach (T weightedItem in source) + for (int i = 0; i < source.Count; i++) { + T weightedItem = source[i]; float weight = weightSelector(weightedItem); currentWeightIndex += weight; @@ -107,6 +141,14 @@ namespace Barotrauma.Extensions } } + /// + /// Iterates over all elements in a given enumerable and discards the result. + /// + public static void Consume(this IEnumerable enumerable) + { + foreach (var _ in enumerable) { /* do nothing */ } + } + /// /// Shorthand for !source.Any(predicate) -> i.e. not any. /// @@ -155,6 +197,27 @@ namespace Barotrauma.Extensions if (value != null) { source.Add(value); } } + public static ImmutableDictionary ToImmutableDictionary(this IEnumerable<(TKey, TValue)> enumerable) + { + return enumerable.ToDictionary().ToImmutableDictionary(); + } + + public static Dictionary ToDictionary(this IEnumerable<(TKey, TValue)> enumerable) + { + var dictionary = new Dictionary(); + foreach (var (k,v) in enumerable) + { + dictionary.Add(k, v); + } + return dictionary; + } + + public static Dictionary ToMutable(this ImmutableDictionary immutableDictionary) + { + if (immutableDictionary == null) { return null; } + return new Dictionary(immutableDictionary); + } + /// /// Returns whether a given collection has at least a certain amount /// of elements for which the predicate returns true. @@ -172,6 +235,18 @@ namespace Barotrauma.Extensions return false; } + /// + /// Equivalent to LINQ's Enumerable.Concat. The main difference is that this + /// takes advantage of ICollection optimizations for Enumerable.Contains + /// and Enumerable.Count. + /// + /// + public static ICollection CollectionConcat(this IEnumerable self, IEnumerable other) + => new CollectionConcat(self, other); + + public static IReadOnlyList ListConcat(this IEnumerable self, IEnumerable other) + => new ListConcat(self, other); + /// /// Returns the maximum element in a given enumerable, or null if there /// aren't any elements in the input. @@ -188,6 +263,10 @@ namespace Barotrauma.Extensions return retVal; } + public static TOut? MaxOrNull(this IEnumerable enumerable, Func conversion) + where TOut : struct, IComparable + => enumerable.Select(conversion).MaxOrNull(); + public static int FindIndex(this IReadOnlyList list, Predicate predicate) { for (int i=0; i string.IsNullOrEmpty(s) ? fallback : s; + + public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s); + public static bool IsNullOrWhiteSpace(this string? s) => string.IsNullOrWhiteSpace(s); + public static bool IsNullOrEmpty(this ContentPath? p) => p?.IsNullOrEmpty() ?? true; + public static bool IsNullOrWhiteSpace(this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true; + public static bool IsNullOrEmpty(this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value); + public static bool IsNullOrWhiteSpace(this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value); + public static bool IsNullOrEmpty(this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty(); + public static bool IsNullOrWhiteSpace(this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace(); + + public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal) + => s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs index cb128e00d..64fd0ab3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/StringFormatter.cs @@ -24,9 +24,9 @@ namespace Barotrauma return new string(newString.SelectMany(str => str.ToCharArray()).ToArray()); } - public static string Remove(this string s, string substring) + public static string Remove(this string s, string substring, StringComparison comparisonType = StringComparison.Ordinal) { - return s.Replace(substring, string.Empty); + return s.Replace(substring, string.Empty, comparisonType); } public static string Remove(this string s, Func predicate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index e8d0be2ea..c44ded911 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -136,6 +136,10 @@ namespace Barotrauma public static void InitIfConsented() { + #if DEBUG + return; + #endif + if (!consentTextAvailable) { SetConsent(Consent.Unknown); @@ -225,8 +229,9 @@ namespace Barotrauma DebugConsole.ThrowError(TextManager.Get("MasterServerErrorUnavailable")); break; default: - DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" }, - new string[2] { response.StatusCode.ToString(), response.StatusDescription })); + DebugConsole.ThrowError(TextManager.GetWithVariables("MasterServerErrorDefault", + ("[statuscode]", response.StatusCode.ToString()), + ("[statusdescription]", response.StatusDescription))); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 763f59a77..82a9f705e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -230,7 +230,7 @@ namespace Barotrauma private string GetAssemblyPath(string assemblyName) => Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, $"{assemblyName}.dll"); private bool resolvingDependency; @@ -361,12 +361,9 @@ namespace Barotrauma if (!SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.Config.AllEnabledPackages != null) + if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) { - if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) - { - message = "[MODDED] " + message; - } + message = "[MODDED] " + message; } loadedImplementation?.AddErrorEvent(errorSeverity, message); @@ -480,10 +477,7 @@ namespace Barotrauma Md5Hash? exeHash = null; try { - using (var stream = File.OpenRead(exePath)) - { - exeHash = new Md5Hash(stream); - } + exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect); } catch (Exception e) { @@ -512,7 +506,7 @@ namespace Barotrauma loadedImplementation?.AddDesignEvent("Executable:" + GameMain.Version.ToString() + exeName + ":" - + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":" + + (exeHash?.ShortRepresentation ?? "Unknown") + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); } @@ -523,24 +517,21 @@ namespace Barotrauma return; } - if (GameMain.Config != null) + var allPackages = ContentPackageManager.EnabledPackages.All.ToList(); + if (allPackages?.Count > 0) { - var allPackages = GameMain.Config.AllEnabledPackages.ToList(); - if (allPackages?.Count > 0) + List packageNames = new List(); + foreach (ContentPackage cp in allPackages) { - List packageNames = new List(); - foreach (ContentPackage cp in allPackages) - { - string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); - sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); - packageNames.Add(sanitizedName); - loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); - } - packageNames.Sort(); - loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames)); + string sanitizedName = cp.Name.Replace(":", "").Replace(" ", ""); + sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length)); + packageNames.Add(sanitizedName); + loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName); } - loadedImplementation?.AddDesignEvent("Language:" + GameMain.Config.Language); + packageNames.Sort(); + loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames)); } + loadedImplementation?.AddDesignEvent("Language:" + GameSettings.CurrentConfig.Language); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index e10ee447a..d1e7c70d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -6,6 +6,10 @@ using Barotrauma.Extensions; 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; @@ -76,8 +80,8 @@ namespace Barotrauma int itemCountApprox = MapEntityPrefab.List.Count() / 3; var containers = new List(70 + 30 * subs.Count()); - var prefabsWithContainer = new List(itemCountApprox / 3); - var prefabsWithoutContainer = new List(itemCountApprox); + var prefabsItemsCanSpawnIn = new List(itemCountApprox / 3); + var singlePrefabs = new List(itemCountApprox); var removals = new List(); // generate loot only for a specific container if defined @@ -93,42 +97,45 @@ namespace Barotrauma if (item.GetRootInventoryOwner() is Character) { continue; } containers.AddRange(item.GetComponents()); } - containers.Shuffle(Rand.RandSync.Server); + containers.Shuffle(Rand.RandSync.ServerAndClient); } - foreach (MapEntityPrefab prefab in MapEntityPrefab.List) + foreach (ItemPrefab ip in ItemPrefab.Prefabs) { - if (!(prefab is ItemPrefab ip)) { continue; } - - if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase))) + 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))) { - prefabsWithContainer.Add(ip); + prefabsItemsCanSpawnIn.Add(ip); } else { - prefabsWithoutContainer.Add(ip); + singlePrefabs.Add(ip); } } - var validContainers = new Dictionary(); - prefabsWithContainer.Shuffle(Rand.RandSync.Server); - // Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) - for (int i = 0; i < prefabsWithContainer.Count; i++) + bool CanSpawnIn(ItemPrefab item, ItemPrefab container) { - var itemPrefab = prefabsWithContainer[i]; - if (itemPrefab == null) { continue; } - if (SpawnItems(itemPrefab)) + foreach (var preferredContainer in item.PreferredContainers) { - removals.Add(itemPrefab); + if (ItemPrefab.IsContainerPreferred(preferredContainer.Primary, container.Identifier.ToEnumerable().Union(container.Tags))) { return true; } } + return false; } - // Remove containers that we successfully spawned items into so that they are not counted in in the second pass. - removals.ForEach(i => prefabsWithContainer.Remove(i)); - // Another pass for items with containers because also they can spawn inside other items (like smg magazine) - prefabsWithContainer.ForEach(i => SpawnItems(i)); - // Spawn items that don't have containers last - prefabsWithoutContainer.Shuffle(Rand.RandSync.Server); - prefabsWithoutContainer.ForEach(i => SpawnItems(i)); + + var validContainers = new Dictionary(); + prefabsItemsCanSpawnIn.Shuffle(Rand.RandSync.ServerAndClient); + // Spawn items that other items can spawn in first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) + for (int i = 0; i < prefabsItemsCanSpawnIn.Count; i++) + { + var itemPrefab = prefabsItemsCanSpawnIn[i]; + if (itemPrefab == null) { continue; } + SpawnItems(itemPrefab); + } + + // Spawn items that nothing can spawn in last + singlePrefabs.Shuffle(Rand.RandSync.ServerAndClient); + singlePrefabs.ForEach(i => SpawnItems(i)); if (OutputDebugInfo) { @@ -158,12 +165,17 @@ namespace Barotrauma } } } -#if SERVER foreach (Item spawnedItem in spawnedItems) { +#if SERVER Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false); - } #endif + foreach (ItemComponent ic in spawnedItem.Components) + { + ic.OnItemLoaded(); + } + } + bool SpawnItems(ItemPrefab itemPrefab) { if (itemPrefab == null) @@ -229,13 +241,13 @@ namespace Barotrauma private static List SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer, float difficultyModifier) { List spawnedItems = new List(); - if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } + if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; } // Don't add dangerously reactive materials in thalamus wrecks if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { return spawnedItems; } - int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server); + 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)) @@ -244,15 +256,15 @@ namespace Barotrauma break; } - var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + 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.Server); + 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) + var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) { SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost, AllowStealing = validContainer.Key.Item.AllowStealing, diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 6b20e104d..234bbefa9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -217,7 +217,7 @@ namespace Barotrauma // Exchange money var itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.Money -= itemValue; - GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier); + GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); Location.StoreCurrentBalance += itemValue; if (removeFromCrate) @@ -367,7 +367,14 @@ namespace Barotrauma if (sub == Submarine.MainSub) { #if CLIENT - new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon"); + new GUIMessageBox("", + TextManager.GetWithVariable("CargoSpawnNotification", + "[roomname]", + cargoRoom.DisplayName, + FormatCapitals.Yes), + Array.Empty(), + type: GUIMessageBox.Type.InGame, + iconStyle: "StoreShoppingCrateIcon"); #else foreach (Client client in GameMain.Server.ConnectedClients) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 4856ff843..6e38a47f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -26,7 +26,17 @@ namespace Barotrauma public bool HasBots { get; set; } - public List> ActiveOrders { get; } = new List>(); + public class ActiveOrder + { + public readonly Order Order; + public float? FadeOutTime; + public ActiveOrder(Order order, float? fadeOutTime) + { + Order = order; + FadeOutTime = fadeOutTime; + } + } + public List ActiveOrders { get; } = new List(); public bool IsSinglePlayer { get; private set; } public ReadyCheck ActiveReadyCheck; @@ -54,16 +64,16 @@ namespace Barotrauma // Ignore orders work a bit differently since the "unignore" order counters the "ignore" order var isUnignoreOrder = order.Identifier == "unignorethis"; - var orderPrefab = !isUnignoreOrder ? order.Prefab : Order.GetPrefab("ignorethis"); - Pair existingOrder = ActiveOrders.Find(o => - o.First.Prefab == orderPrefab && MatchesTarget(o.First.TargetEntity, order.TargetEntity) && - (o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex)); + var orderPrefab = !isUnignoreOrder ? order.Prefab : OrderPrefab.Prefabs["ignorethis"]; + ActiveOrder existingOrder = ActiveOrders.Find(o => + o.Order.Prefab == orderPrefab && MatchesTarget(o.Order.TargetEntity, order.TargetEntity) && + (o.Order.TargetType != Order.OrderTargetType.WallSection || o.Order.WallSectionIndex == order.WallSectionIndex)); if (existingOrder != null) { if (!isUnignoreOrder) { - existingOrder.Second = fadeOutTime; + existingOrder.FadeOutTime = fadeOutTime; return false; } else @@ -74,7 +84,7 @@ namespace Barotrauma } else if (!isUnignoreOrder) { - ActiveOrders.Add(new Pair(order, fadeOutTime)); + ActiveOrders.Add(new ActiveOrder(order, fadeOutTime)); #if CLIENT HintManager.OnActiveOrderAdded(order); #endif @@ -96,7 +106,7 @@ namespace Barotrauma public void AddCharacterElements(XElement element) { - foreach (XElement characterElement in element.Elements()) + foreach (var characterElement in element.Elements()) { if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } CharacterInfo characterInfo = new CharacterInfo(characterElement); @@ -105,7 +115,7 @@ namespace Barotrauma characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1); #endif characterInfos.Add(characterInfo); - foreach (XElement subElement in characterElement.Elements()) + foreach (var subElement in characterElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -205,7 +215,7 @@ namespace Barotrauma wp.SpawnType == SpawnType.Human && wp.Submarine == Level.Loaded.StartOutpost && wp.CurrentHull != null && - wp.CurrentHull.OutpostModuleTags.Contains("airlock")); + wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier())); while (spawnWaypoints.Count > characterInfos.Count) { spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count)); @@ -236,7 +246,7 @@ namespace Barotrauma } if (character.Info.InventoryData != null) { - character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData); + character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null)); } else if (!character.Info.StartItemsGiven) { @@ -301,12 +311,12 @@ namespace Barotrauma public void Update(float deltaTime) { - foreach (Pair order in ActiveOrders) + foreach (ActiveOrder order in ActiveOrders) { - if (order.Second.HasValue) { order.Second -= deltaTime; } + if (order.FadeOutTime.HasValue) { order.FadeOutTime -= deltaTime; } } - ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) || - (o.First.TargetEntity != null && o.First.TargetEntity.Removed)); + ActiveOrders.RemoveAll(o => (o.FadeOutTime.HasValue && o.FadeOutTime <= 0.0f) || + (o.Order.TargetEntity != null && o.Order.TargetEntity.Removed)); UpdateConversations(deltaTime); UpdateProjectSpecific(deltaTime); @@ -357,20 +367,20 @@ namespace Barotrauma if (player.TeamID != npc.TeamID && !player.IsIncapacitated && player.CurrentHull == npc.CurrentHull) { List availableSpeakers = new List() { npc, player }; - List dialogFlags = new List() { "OutpostNPC", "EnterOutpost" }; + List dialogFlags = new List() { "OutpostNPC".ToIdentifier(), "EnterOutpost".ToIdentifier() }; if (GameMain.GameSession?.GameMode is CampaignMode campaignMode) { - if (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false) + if (campaignMode.Map?.CurrentLocation?.Type?.Identifier == "abandoned") { if (npc.TeamID == CharacterTeamType.None) { - dialogFlags.Remove("OutpostNPC"); - dialogFlags.Add("Bandit"); + dialogFlags.Remove("OutpostNPC".ToIdentifier()); + dialogFlags.Add("Bandit".ToIdentifier()); } else if (npc.TeamID == CharacterTeamType.FriendlyNPC) { - dialogFlags.Remove("OutpostNPC"); - dialogFlags.Add("Hostage"); + dialogFlags.Remove("OutpostNPC".ToIdentifier()); + dialogFlags.Add("Hostage".ToIdentifier()); } } else if (campaignMode.Map?.CurrentLocation?.Reputation != null) @@ -381,11 +391,11 @@ namespace Barotrauma campaignMode.Map.CurrentLocation.Reputation.Value); if (normalizedReputation < 0.2f) { - dialogFlags.Add("LowReputation"); + dialogFlags.Add("LowReputation".ToIdentifier()); } else if (normalizedReputation > 0.8f) { - dialogFlags.Add("HighReputation"); + dialogFlags.Add("HighReputation".ToIdentifier()); } } } @@ -451,13 +461,18 @@ namespace Barotrauma // Prioritize those who are on the same submarine as the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) // Prioritize those who are already ordered to operate the device - .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity)) + .ThenByDescending(c + => order.Category == OrderCategory.Operate + && c.CurrentOrders.Any(o + => o != null + && o.Identifier == order.Identifier + && o.TargetEntity == order.TargetEntity)) // Prioritize those with the appropriate job for the order - .ThenByDescending(c => order.HasAppropriateJob(c)) + .ThenByDescending(order.HasAppropriateJob) // Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters) - .ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier)) + .ThenByDescending(c => c.CurrentOrders.None(o => o != null && o.Identifier == order.Identifier)) // Prioritize those with the preferred job for the order - .ThenByDescending(c => order.HasPreferredJob(c)) + .ThenByDescending(order.HasPreferredJob) // Prioritize bots over player-controlled characters .ThenByDescending(c => c.IsBot) // Prioritize those with a lower current objective priority @@ -472,12 +487,12 @@ namespace Barotrauma { ActiveOrdersElement = new XElement("activeorders"); // Only save orders with no fade out time (e.g. ignore orders) - var ordersToSave = new List(); + var ordersToSave = new List(); foreach (var activeOrder in ActiveOrders) { - var order = activeOrder?.First; - if (order == null || activeOrder.Second.HasValue) { continue; } - ordersToSave.Add(new OrderInfo(order, null, CharacterInfo.HighestManualOrderPriority)); + var order = activeOrder?.Order; + if (order == null || activeOrder.FadeOutTime.HasValue) { continue; } + ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority)); } CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray()); parentElement?.Add(ActiveOrdersElement); @@ -489,22 +504,22 @@ namespace Barotrauma foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement)) { IIgnorable ignoreTarget = null; - if (orderInfo.Order.IsIgnoreOrder) + if (orderInfo.IsIgnoreOrder) { - switch (orderInfo.Order.TargetType) + switch (orderInfo.TargetType) { case Order.OrderTargetType.Entity: - ignoreTarget = orderInfo.Order.TargetEntity as IIgnorable; + ignoreTarget = orderInfo.TargetEntity as IIgnorable; break; - case Order.OrderTargetType.WallSection when orderInfo.Order.TargetEntity is Structure s && orderInfo.Order.WallSectionIndex.HasValue: - ignoreTarget = s.GetSection(orderInfo.Order.WallSectionIndex.Value) as IIgnorable; + case Order.OrderTargetType.WallSection when orderInfo.TargetEntity is Structure s && orderInfo.WallSectionIndex.HasValue: + ignoreTarget = s.GetSection(orderInfo.WallSectionIndex.Value); break; default: DebugConsole.ThrowError("Error loading an ignore order - can't find a proper ignore target"); continue; } } - if (orderInfo.Order.TargetEntity == null || (orderInfo.Order.IsIgnoreOrder && ignoreTarget == null)) + if (orderInfo.TargetEntity == null || (orderInfo.IsIgnoreOrder && ignoreTarget == null)) { // The order target doesn't exist anymore, just discard the loaded order continue; @@ -513,7 +528,7 @@ namespace Barotrauma { ignoreTarget.OrderedToBeIgnored = true; } - AddOrder(orderInfo.Order, null); + AddOrder(orderInfo, null); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs index ba5a9912b..1d88c39a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/CampaignMetadata.cs @@ -10,7 +10,7 @@ namespace Barotrauma { public CampaignMode Campaign { get; } - private readonly Dictionary data = new Dictionary(); + private readonly Dictionary data = new Dictionary(); public CampaignMetadata(CampaignMode campaign) { @@ -21,15 +21,15 @@ namespace Barotrauma { Campaign = campaign; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase)) { - string identifier = subElement.GetAttributeString("key", string.Empty).ToLowerInvariant(); + Identifier identifier = subElement.GetAttributeIdentifier("key", Identifier.Empty); string value = subElement.GetAttributeString("value", string.Empty); string valueType = subElement.GetAttributeString("type", string.Empty); - if (string.IsNullOrWhiteSpace(identifier) || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType)) + if (identifier.IsEmpty || string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(valueType)) { DebugConsole.ThrowError("Unable to load value because one or more of the required attributes are empty.\n" + $"key: \"{identifier}\", value: \"{value}\", type: \"{valueType}\""); @@ -43,28 +43,20 @@ namespace Barotrauma DebugConsole.ThrowError($"Type for {identifier} not found ({valueType})."); continue; } - - if (type == typeof(float)) + else if (type == typeof(Identifier)) { - if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatValue)) - { - DebugConsole.ThrowError($"Error in campaign metadata: could not parse \"{value}\" as a float."); - continue; - } - data.Add(identifier, floatValue); + data.Add(identifier, value.ToIdentifier()); } else { - data.Add(identifier, Convert.ChangeType(value, type)); + data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo)); } } } } - public void SetValue(string identifier, object value) + public void SetValue(Identifier identifier, object value) { - identifier = identifier.ToLowerInvariant(); - DebugConsole.Log($"Set the value \"{identifier}\" to {value}"); if (!data.ContainsKey(identifier)) @@ -76,33 +68,32 @@ namespace Barotrauma data[identifier] = value; } - public float GetFloat(string identifier, float? defaultValue = null) + public float GetFloat(Identifier identifier, float? defaultValue = null) { return (float)GetTypeOrDefault(identifier, typeof(float), defaultValue ?? 0f); } - public int GetInt(string identifier, int? defaultValue = null) + public int GetInt(Identifier identifier, int? defaultValue = null) { return (int)GetTypeOrDefault(identifier, typeof(int), defaultValue ?? 0); } - public bool GetBoolean(string identifier, bool? defaultValue = null) + public bool GetBoolean(Identifier identifier, bool? defaultValue = null) { return (bool)GetTypeOrDefault(identifier, typeof(bool), defaultValue ?? false); } - public string GetString(string identifier, string? defaultValue = null) + public string GetString(Identifier identifier, string? defaultValue = null) { return (string)GetTypeOrDefault(identifier, typeof(string), defaultValue ?? string.Empty); } - public bool HasKey(string identifier) + public bool HasKey(Identifier identifier) { - identifier = identifier.ToLowerInvariant(); return data.ContainsKey(identifier); } - private object GetTypeOrDefault(string identifier, Type type, object defaultValue) + private object GetTypeOrDefault(Identifier identifier, Type type, object defaultValue) { object? value = GetValue(identifier); @@ -122,7 +113,7 @@ namespace Barotrauma return defaultValue; } - public object? GetValue(string identifier) + public object? GetValue(Identifier identifier) { return data.ContainsKey(identifier) ? data[identifier] : null; } @@ -133,16 +124,16 @@ namespace Barotrauma foreach (var (key, value) in data) { - string valueStr = value?.ToString() ?? ""; - if (value?.GetType() == typeof(float)) + string valueStr = value.ToString() ?? throw new NullReferenceException(); + if (value is float f) { - valueStr = ((float)value).ToString("G", CultureInfo.InvariantCulture); + valueStr = f.ToString("G", CultureInfo.InvariantCulture); } element.Add(new XElement("Data", new XAttribute("key", key), new XAttribute("value", valueStr), - new XAttribute("type", value?.GetType()))); + new XAttribute("type", value.GetType()))); } #if DEBUG DebugConsole.Log(element.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index d22a8e83c..c31f37f60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -18,16 +18,14 @@ namespace Barotrauma } } - internal class FactionPrefab : IDisposable + internal class FactionPrefab : Prefab { - public static List Prefabs { get; set; } + public readonly static PrefabCollection Prefabs = new PrefabCollection(); - public string Name { get; } + public LocalizedString Name { get; } - public string Description { get; } - public string ShortDescription { get; } - - public string Identifier { get; } + public LocalizedString Description { get; } + public LocalizedString ShortDescription { get; } /// /// How low the reputation can drop on this faction @@ -52,17 +50,16 @@ namespace Barotrauma public Color IconColor { get; } #endif - private FactionPrefab(XElement element) + public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { - Identifier = element.GetAttributeString("identifier", string.Empty); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); - Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}", returnNull: true) ?? "Unnamed"; - Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description", returnNull: true) ?? ""; - ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription", returnNull: true) ?? ""; + Name = element.GetAttributeString("name", null) ?? TextManager.Get($"faction.{Identifier}").Fallback("Unnamed"); + Description = element.GetAttributeString("description", null) ?? TextManager.Get($"faction.{Identifier}.description").Fallback(""); + ShortDescription = element.GetAttributeString("shortdescription", null) ?? TextManager.Get($"faction.{Identifier}.shortdescription").Fallback(""); #if CLIENT - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) @@ -78,64 +75,12 @@ namespace Barotrauma #endif } - public static void LoadFactions() - { - Prefabs?.ForEach(set => set.Dispose()); - Prefabs = new List(); - IEnumerable files = GameMain.Instance.GetFilesOfType(ContentType.Factions); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - XElement? rootElement = doc?.Root; - - if (doc == null || rootElement == null) { continue; } - - if (doc.Root.IsOverride()) - { - Prefabs.Clear(); - DebugConsole.NewMessage($"Overriding all factions with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in rootElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the faction config '{elementName}' in file '{file.Path}'"); - continue; - } - - var existingParams = Prefabs.Find(set => set.Identifier == identifier); - if (existingParams != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding faction config '{identifier}' using the file '{file.Path}'", Color.Yellow); - Prefabs.Remove(existingParams); - } - else - { - DebugConsole.ThrowError($"Duplicate faction config: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - - Prefabs.Add(new FactionPrefab(element)); - } - } - } - - public void Dispose() + public override void Dispose() { #if CLIENT Icon?.Remove(); Icon = null; #endif - GC.SuppressFinalize(this); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index ada24867c..820d8227e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -13,13 +13,13 @@ namespace Barotrauma public const float MinReputationLossPerStolenItem = 0.5f; public const float MaxReputationLossPerStolenItem = 10.0f; - public string Identifier { get; } + public Identifier Identifier { get; } public int MinReputation { get; } public int MaxReputation { get; } public int InitialReputation { get; } public CampaignMetadata Metadata { get; } - private readonly string metaDataIdentifier; + private readonly Identifier metaDataIdentifier; /// /// Reputation value normalized to the range of 0-1 @@ -46,8 +46,8 @@ namespace Barotrauma if (increase != 0 && Character.Controlled != null) { Character.Controlled.AddMessage( - TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name), - increase > 0 ? GUI.Style.Green : GUI.Style.Red, + TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value, + increase > 0 ? GUIStyle.Green : GUIStyle.Red, playSound: true, Identifier, increase, lifetime: 5.0f); } #endif @@ -80,23 +80,23 @@ namespace Barotrauma public readonly Location Location; - public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) + public Reputation(CampaignMetadata metadata, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation) : this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation) { } public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation) - : this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation) + : this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}".ToIdentifier(), minReputation, maxReputation, initialReputation) { } - private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation) + private Reputation(CampaignMetadata metadata, Faction faction, Location location, Identifier identifier, int minReputation, int maxReputation, int initialReputation) { System.Diagnostics.Debug.Assert(metadata != null); System.Diagnostics.Debug.Assert(faction != null || location != null); Metadata = metadata; - Identifier = identifier.ToLowerInvariant(); - metaDataIdentifier = $"reputation.{Identifier}"; + Identifier = identifier; + metaDataIdentifier = $"reputation.{Identifier}".ToIdentifier(); MinReputation = minReputation; MaxReputation = maxReputation; InitialReputation = initialReputation; @@ -104,12 +104,12 @@ namespace Barotrauma Location = location; } - public string GetReputationName() + public LocalizedString GetReputationName() { return GetReputationName(NormalizedValue); } - public static string GetReputationName(float normalizedValue) + public static LocalizedString GetReputationName(float normalizedValue) { if (normalizedValue < HostileThreshold) { @@ -135,36 +135,36 @@ namespace Barotrauma { if (normalizedValue < HostileThreshold) { - return GUI.Style.ColorReputationVeryLow; + return GUIStyle.ColorReputationVeryLow; } else if (normalizedValue < 0.4f) { - return GUI.Style.ColorReputationLow; + return GUIStyle.ColorReputationLow; } else if (normalizedValue < 0.6f) { - return GUI.Style.ColorReputationNeutral; + return GUIStyle.ColorReputationNeutral; } else if (normalizedValue < 0.8f) { - return GUI.Style.ColorReputationHigh; + return GUIStyle.ColorReputationHigh; } - return GUI.Style.ColorReputationVeryHigh; + return GUIStyle.ColorReputationVeryHigh; } - public string GetFormattedReputationText(bool addColorTags = false) + public LocalizedString GetFormattedReputationText(bool addColorTags = false) { return GetFormattedReputationText(NormalizedValue, Value, addColorTags); } - public static string GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false) + public static LocalizedString GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false) { - string reputationName = GetReputationName(normalizedValue); - string formattedReputation = TextManager.GetWithVariables("reputationformat", - new string[] { "[reputationname]", "[reputationvalue]" }, - new string[] { reputationName, ((int)Math.Round(value)).ToString() }); + LocalizedString reputationName = GetReputationName(normalizedValue); + LocalizedString formattedReputation = TextManager.GetWithVariables("reputationformat", + ("[reputationname]", reputationName), + ("[reputationvalue]", ((int)Math.Round(value)).ToString())); if (addColorTags) { - formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖"; + formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖"+ formattedReputation+"‖end‖"; } return formattedReputation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 21e7ecf93..b384c847f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -304,11 +304,11 @@ namespace Barotrauma if (levelData.HasBeaconStation && !levelData.IsBeaconActive) { - var beaconMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))); + var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase))); if (beaconMissionPrefabs.Any()) { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, beaconMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand); + var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, p => (float)p.Commonness, rand); if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) { extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); @@ -317,7 +317,7 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); if (!huntingGroundsMissionPrefabs.Any()) { DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); @@ -325,7 +325,7 @@ namespace Barotrauma else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)Math.Max(p.Commonness, 0.1f)).ToList(), rand); + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); @@ -692,13 +692,13 @@ namespace Barotrauma if (CampaignMetadata != null) { - int loops = CampaignMetadata.GetInt("campaign.endings", 0); - CampaignMetadata.SetValue("campaign.endings", loops + 1); + int loops = CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0); + CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1); } GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Complete, - Preset?.Identifier ?? "none"); + Preset?.Identifier.Value ?? "none"); string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); @@ -717,7 +717,7 @@ namespace Barotrauma location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); Money -= characterInfo.Salary; - GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown"); + GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); return true; } @@ -740,8 +740,9 @@ namespace Barotrauma HumanAIController humanAI = npc.AIController as HumanAIController; if (humanAI == null) { yield return CoroutineStatus.Success; } - var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)); - humanAI.SetForcedOrder(waitOrder, string.Empty, null); + var waitOrderPrefab = OrderPrefab.Prefabs["wait"]; + var waitOrder = new Order(waitOrderPrefab, Identifier.Empty, null, orderGiver: null); + humanAI.SetForcedOrder(waitOrder); var waitObjective = humanAI.ObjectiveManager.ForcedOrder; humanAI.FaceTarget(interactor); @@ -782,7 +783,7 @@ namespace Barotrauma character.SetCustomInteract( NPCInteract, #if CLIENT - hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameMain.Config.KeyBindText(InputType.Use))); + hudText: TextManager.GetWithVariable("CampaignInteraction." + interactionType, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use))); #else hudText: TextManager.Get("CampaignInteraction." + interactionType)); #endif @@ -855,8 +856,8 @@ namespace Barotrauma { GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create( - TextManager.Get("RadioAnnouncerName"), - TextManager.Get("TooFarFromOutpostWarning"), Networking.ChatMessageType.Default, null), c); + TextManager.Get("RadioAnnouncerName").Value, + TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs index 1061e9754..fc2c6652e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs @@ -27,7 +27,7 @@ namespace Barotrauma get { return preset.IsSinglePlayer; } } - public string Name + public LocalizedString Name { get { return preset.Name; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index 608dd2b89..f28cb563c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -19,20 +19,20 @@ namespace Barotrauma public readonly Type GameModeType; - public readonly string Name; - public readonly string Description; + public readonly LocalizedString Name; + public readonly LocalizedString Description; - public readonly string Identifier; + public readonly Identifier Identifier; public readonly bool IsSinglePlayer; //are clients allowed to vote for this gamemode public readonly bool Votable; - public GameModePreset(string identifier, Type type, bool isSinglePlayer = false, bool votable = true) + public GameModePreset(Identifier identifier, Type type, bool isSinglePlayer = false, bool votable = true) { Name = TextManager.Get("GameMode." + identifier); - Description = TextManager.Get("GameModeDescription." + identifier, returnNull: true) ?? ""; + Description = TextManager.Get("GameModeDescription." + identifier).Fallback(""); Identifier = identifier; GameModeType = type; @@ -46,15 +46,15 @@ namespace Barotrauma public static void Init() { #if CLIENT - Tutorial = new GameModePreset("tutorial", typeof(TutorialMode), true); - DevSandbox = new GameModePreset("devsandbox", typeof(GameMode), true); - SinglePlayerCampaign = new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true); - TestMode = new GameModePreset("testmode", typeof(TestGameMode), true); + Tutorial = new GameModePreset("tutorial".ToIdentifier(), typeof(TutorialMode), true); + DevSandbox = new GameModePreset("devsandbox".ToIdentifier(), typeof(GameMode), true); + SinglePlayerCampaign = new GameModePreset("singleplayercampaign".ToIdentifier(), typeof(SinglePlayerCampaign), true); + TestMode = new GameModePreset("testmode".ToIdentifier(), typeof(TestGameMode), true); #endif - Sandbox = new GameModePreset("sandbox", typeof(GameMode), false); - Mission = new GameModePreset("mission", typeof(CoOpMode), false); - PvP = new GameModePreset("pvp", typeof(PvPMode), false); - MultiPlayerCampaign = new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false); + Sandbox = new GameModePreset("sandbox".ToIdentifier(), typeof(GameMode), false); + Mission = new GameModePreset("mission".ToIdentifier(), typeof(CoOpMode), false); + PvP = new GameModePreset("pvp".ToIdentifier(), typeof(PvPMode), false); + MultiPlayerCampaign = new GameModePreset("multiplayercampaign".ToIdentifier(), typeof(MultiPlayerCampaign), false, false); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 533a3773e..ab5ea60ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -120,7 +120,7 @@ namespace Barotrauma #endif } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -192,7 +192,7 @@ namespace Barotrauma { var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath); if (characterDataDoc?.Root == null) { return; } - foreach (XElement subElement in characterDataDoc.Root.Elements()) + foreach (var subElement in characterDataDoc.Root.Elements()) { characterData.Add(new CharacterCampaignData(subElement)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 817e4ff80..d9188c9b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1,8 +1,11 @@ -using Barotrauma.IO; +#nullable enable + +using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -14,11 +17,11 @@ namespace Barotrauma public readonly EventManager EventManager; - public GameMode GameMode; + public GameMode? GameMode; //two locations used as the start and end in the MP mode - private Location[] dummyLocations; - public CrewManager CrewManager; + private Location[]? dummyLocations; + public CrewManager? CrewManager; public double RoundStartTime; @@ -30,15 +33,15 @@ namespace Barotrauma public CharacterTeamType? WinningTeam; public bool IsRunning { get; private set; } - + public bool RoundEnding { get; private set; } - public Level Level { get; private set; } - public LevelData LevelData { get; private set; } + public Level? Level { get; private set; } + public LevelData? LevelData { get; private set; } public bool MirrorLevel { get; private set; } - public Map Map + public Map? Map { get { @@ -46,7 +49,7 @@ namespace Barotrauma } } - public CampaignMode Campaign + public CampaignMode? Campaign { get { @@ -61,6 +64,7 @@ namespace Barotrauma { if (Map != null) { return Map.CurrentLocation; } if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[0]; } } @@ -71,6 +75,7 @@ namespace Barotrauma { if (Map != null) { return Map.SelectedLocation; } if (dummyLocations == null) { CreateDummyLocations(); } + if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); } return dummyLocations[1]; } } @@ -79,13 +84,13 @@ namespace Barotrauma public List OwnedSubmarines = new List(); - public Submarine Submarine { get; set; } + public Submarine? Submarine { get; set; } - public string SavePath { get; set; } + public string? SavePath { get; set; } partial void InitProjSpecific(); - private GameSession(SubmarineInfo submarineInfo, List ownedSubmarines = null) + private GameSession(SubmarineInfo submarineInfo, List? ownedSubmarines = null) { InitProjSpecific(); SubmarineInfo = submarineInfo; @@ -109,21 +114,21 @@ namespace Barotrauma /// /// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved). /// - public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string seed = null, MissionType missionType = MissionType.None) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string? seed = null, MissionType missionType = MissionType.None) : this(submarineInfo) { this.SavePath = savePath; - CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); + CrewManager = new CrewManager(gameModePreset.IsSinglePlayer); GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType); } /// /// Start a new GameSession with a specific pre-selected mission. /// - public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable missionPrefabs = null) + public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string? seed = null, IEnumerable? missionPrefabs = null) : this(submarineInfo) { - CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); + CrewManager = new CrewManager(gameModePreset.IsSinglePlayer); GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs); } @@ -134,9 +139,10 @@ namespace Barotrauma { this.SavePath = saveFile; GameMain.GameSession = this; + XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); //selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); - foreach (XElement subElement in doc.Root.Elements()) + foreach (var subElement in rootElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -154,7 +160,7 @@ namespace Barotrauma var mpCampaign = MultiPlayerCampaign.LoadNew(subElement); GameMode = mpCampaign; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { + { mpCampaign.LoadNewLevel(); //save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance SaveUtil.SaveGame(saveFile); @@ -164,7 +170,7 @@ namespace Barotrauma } } - private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable missionPrefabs = null, MissionType missionType = MissionType.None) + private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable? missionPrefabs = null, MissionType missionType = MissionType.None) { if (gameModePreset.GameModeType == typeof(CoOpMode) || gameModePreset.GameModeType == typeof(PvPMode)) { @@ -193,7 +199,7 @@ namespace Barotrauma else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); - if (campaign != null && selectedSub != null) + if (selectedSub != null) { campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); } @@ -203,7 +209,7 @@ namespace Barotrauma else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); - if (campaign != null && selectedSub != null) + if (selectedSub != null) { campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); } @@ -277,10 +283,9 @@ namespace Barotrauma } } - Campaign.Money -= cost; + Campaign!.Money -= cost; GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); - ((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine; return newSubmarine; } @@ -317,9 +322,10 @@ namespace Barotrauma return isRadiated; } - public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams levelGenerationParams = null) + public void StartRound(string levelSeed, float? difficulty = null, LevelGenerationParams? levelGenerationParams = null) { - LevelData randomLevel = null; + if (GameMode == null) { return; } + LevelData? randomLevel = null; foreach (Mission mission in Missions.Union(GameMode.Missions)) { MissionPrefab missionPrefab = mission.Prefab; @@ -327,7 +333,7 @@ namespace Barotrauma missionPrefab.AllowedLocationTypes.Any() && !missionPrefab.AllowedConnectionTypes.Any()) { - LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase))); + LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier)); CreateDummyLocations(locationType); randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true); break; @@ -337,8 +343,10 @@ namespace Barotrauma StartRound(randomLevel); } - public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) + public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null) { + AfflictionPrefab.LoadAllEffects(); + MirrorLevel = mirrorLevel; if (SubmarineInfo == null) { @@ -375,7 +383,7 @@ namespace Barotrauma } } - foreach (Mission mission in GameMode.Missions) + foreach (Mission mission in GameMode!.Missions) { // setting level for missions that may involve difficulty-related submarine creation mission.SetLevel(levelData); @@ -403,7 +411,10 @@ namespace Barotrauma } } - Level level = null; + //Clear out the stored grids + Powered.Grids.Clear(); + + Level? level = null; if (levelData != null) { level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost); @@ -413,11 +424,11 @@ namespace Barotrauma GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Start, - GameMode?.Preset?.Identifier ?? "none"); + GameMode?.Preset?.Identifier.Value ?? "none"); - string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; + string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); foreach (Mission mission in missions) { @@ -425,12 +436,12 @@ namespace Barotrauma } if (Level.Loaded != null) { - string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : - Level.Loaded.GenerationParams?.Identifier; - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null")); + Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId); } - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none")); #if CLIENT if (GameMode is TutorialMode tutorialMode) { @@ -452,16 +463,16 @@ namespace Barotrauma { GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled"); } - bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome); + bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome); if (firstTimeInBiome) { - GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); - GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime); + GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels); } } #if CLIENT - if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } + if (GameMode is CampaignMode && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary; if (existingRoundSummary?.ContinueButton != null) @@ -474,7 +485,7 @@ namespace Barotrauma if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) { GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false); - if (EndLocation != null) + if (EndLocation != null && levelData != null) { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); @@ -503,7 +514,7 @@ namespace Barotrauma #endif } - private void InitializeLevel(Level level) + private void InitializeLevel(Level? level) { //make sure no status effects have been carried on from the next round //(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully) @@ -514,7 +525,7 @@ namespace Barotrauma GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; #endif if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } - if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameMain.Config.LosMode; } + if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } #endif LevelData = level?.LevelData; Level = level; @@ -531,28 +542,28 @@ namespace Barotrauma Entity.Spawner = new EntitySpawner(); - missions.Clear(); - GameMode.AddExtraMissions(LevelData); - missions.AddRange(GameMode.Missions); - GameMode.Start(); - foreach (Mission mission in missions) + if (GameMode != null && Submarine != null) { - int prevEntityCount = Entity.GetEntities().Count(); - mission.Start(Level.Loaded); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) + missions.Clear(); + GameMode.AddExtraMissions(LevelData); + missions.AddRange(GameMode.Missions); + GameMode.Start(); + foreach (Mission mission in missions) { - DebugConsole.ThrowError( - $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + - "The clients should not instantiate entities themselves when starting the mission," + - " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial."); + int prevEntityCount = Entity.GetEntities().Count(); + mission.Start(Level.Loaded); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) + { + DebugConsole.ThrowError( + $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + + "The clients should not instantiate entities themselves when starting the mission," + + " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial."); + } } - } - EventManager?.StartRound(Level.Loaded); - SteamAchievementManager.OnStartRound(); + EventManager?.StartRound(Level.Loaded); + SteamAchievementManager.OnStartRound(); - if (GameMode != null) - { GameMode.ShowStartMessage(); if (GameMain.NetworkMember == null) @@ -571,7 +582,7 @@ namespace Barotrauma } } - GameMain.Config.RecentlyEncounteredCreatures.Clear(); + CreatureMetrics.Instance.RecentlyEncountered.Clear(); GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundStartTime = Timing.TotalTime; @@ -579,11 +590,11 @@ namespace Barotrauma IsRunning = true; } - public void PlaceSubAtStart(Level level) + public void PlaceSubAtStart(Level? level) { - if (level == null) + if (level == null || Submarine == null) { - Submarine.MainSub.SetPosition(Vector2.Zero); + Submarine?.SetPosition(Vector2.Zero); return; } @@ -601,7 +612,7 @@ namespace Barotrauma //find the port that's the nearest to the outpost and dock if one is found float closestDistance = 0.0f; - DockingPort myPort = null, outPostPort = null; + DockingPort? myPort = null, outPostPort = null; foreach (DockingPort port in DockingPort.List) { if (port.IsHorizontal || port.Docked) { continue; } @@ -687,7 +698,7 @@ namespace Barotrauma UpdateProjSpecific(deltaTime); } - public Mission GetMission(int index) + public Mission? GetMission(int index) { if (index < 0 || index >= missions.Count) { return null; } return missions[index]; @@ -698,12 +709,13 @@ namespace Barotrauma return missions.IndexOf(mission); } - public void EnforceMissionOrder(List missionIdentifiers) + public void EnforceMissionOrder(List missionIdentifiers) { List sortedMissions = new List(); - foreach (string missionId in missionIdentifiers) + foreach (Identifier missionId in missionIdentifiers) { var matchingMission = missions.Find(m => m.Prefab.Identifier == missionId); + if (matchingMission == null) { continue; } sortedMissions.Add(matchingMission); missions.Remove(matchingMission); } @@ -711,21 +723,24 @@ namespace Barotrauma } partial void UpdateProjSpecific(float deltaTime); - + public static IEnumerable GetSessionCrewCharacters() { #if SERVER return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); #else - if (GameMain.GameSession == null) { return Enumerable.Empty(); } + if (GameMain.GameSession?.CrewManager is null) { return Enumerable.Empty(); } return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null && !c.IsDead); -#endif +#endif } - public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) + public void EndRound(string endMessage, List? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { RoundEnding = true; + //Clear the grids to allow for garbage collection + Powered.Grids.Clear(); + try { IEnumerable crewCharacters = GetSessionCrewCharacters(); @@ -744,7 +759,7 @@ namespace Barotrauma if (missions.Any()) { - if (missions.Any(m => m.Completed)) + if (missions.Any()) { foreach (Character character in crewCharacters) { @@ -786,19 +801,20 @@ namespace Barotrauma GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); + AfflictionPrefab.ClearAllEffects(); IsRunning = false; #if CLIENT - bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); + bool success = CrewManager!.GetCharacters().Any(c => !c.IsDead); #else bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); #endif double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddProgressionEvent( success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail, - GameMode?.Name ?? "none", + GameMode?.Name?.Value ?? "none", roundDuration); - string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; + string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":"; LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { @@ -820,7 +836,7 @@ namespace Barotrauma { double roundDuration = Timing.TotalTime - RoundStartTime; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), roundDuration); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration); foreach (Mission mission in missions) { @@ -828,11 +844,11 @@ namespace Barotrauma } if (Level.Loaded != null) { - string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ? + Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ? Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier : - Level.Loaded.GenerationParams?.Identifier; - GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration); - GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration); + Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier(); + GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration); + GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration); } if (Submarine.MainSub != null) @@ -874,7 +890,7 @@ namespace Barotrauma { characterType = "Player"; } - GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value); + GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value); } } #if CLIENT @@ -895,18 +911,18 @@ namespace Barotrauma public void KillCharacter(Character character) { #if CLIENT - CrewManager.KillCharacter(character); + CrewManager?.KillCharacter(character); #endif } public void ReviveCharacter(Character character) { #if CLIENT - CrewManager.ReviveCharacter(character); + CrewManager?.ReviveCharacter(character); #endif } - public static bool IsCompatibleWithEnabledContentPackages(IList contentPackagePaths, out string errorMsg) + public static bool IsCompatibleWithEnabledContentPackages(IList contentPackagePaths, out LocalizedString errorMsg) { errorMsg = ""; //no known content packages, must be an older save file @@ -915,15 +931,15 @@ namespace Barotrauma List missingPackages = new List(); foreach (string packagePath in contentPackagePaths) { - if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.Path == packagePath)) + if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.Path == packagePath)) { missingPackages.Add(packagePath); } } List excessPackages = new List(); - foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) + foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) { - if (!cp.HasMultiplayerIncompatibleContent) { continue; } + //if (!cp.HasMultiplayerIncompatibleContent) { continue; } if (!contentPackagePaths.Any(p => p == cp.Path)) { excessPackages.Add(cp.Name); @@ -933,8 +949,8 @@ namespace Barotrauma bool orderMismatch = false; if (missingPackages.Count == 0 && missingPackages.Count == 0) { - var enabledPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); - for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Count; i++) + var enabledPackages = ContentPackageManager.EnabledPackages.All/*.Where(cp => cp.HasMultiplayerIncompatibleContent)*/.ToImmutableArray(); + for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++) { if (contentPackagePaths[i] != enabledPackages[i].Path) { @@ -956,17 +972,17 @@ namespace Barotrauma } if (excessPackages.Count == 1) { - if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; } + if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackage", "[incompatiblecontentpackage]", excessPackages[0]); } else if (excessPackages.Count > 1) { - if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; } + if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } errorMsg += TextManager.GetWithVariable("campaignmode.incompatiblecontentpackages", "[incompatiblecontentpackages]", string.Join(", ", excessPackages)); } if (orderMismatch) { - if (!string.IsNullOrEmpty(errorMsg)) { errorMsg += "\n"; } + if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackagePaths)); } @@ -981,24 +997,25 @@ namespace Barotrauma } XDocument doc = new XDocument(new XElement("Gamesession")); + XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null."); - doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); - doc.Root.Add(new XAttribute("version", GameMain.Version)); + rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); + rootElement.Add(new XAttribute("version", GameMain.Version)); var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo; - doc.Root.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name)); + rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name)); if (OwnedSubmarines != null) { List ownedSubmarineNames = new List(); var ownedSubsElement = new XElement("ownedsubmarines"); - doc.Root.Add(ownedSubsElement); + rootElement.Add(ownedSubsElement); foreach (var ownedSub in OwnedSubmarines) { ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name))); } } - doc.Root.Add(new XAttribute("mapseed", Map.Seed)); - doc.Root.Add(new XAttribute("selectedcontentpackages", - string.Join("|", GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); + if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); } + rootElement.Add(new XAttribute("selectedcontentpackages", + string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); ((CampaignMode)GameMode).Save(doc.Root); @@ -1007,7 +1024,7 @@ namespace Barotrauma /*public void Load(XElement saveElement) { - foreach (XElement subElement in saveElement.Elements()) + foreach (var subElement in saveElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs index 59b6e4bc5..43baec7c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/HireManager.cs @@ -29,8 +29,8 @@ namespace Barotrauma JobPrefab job = location.Type.GetRandomHireable(); if (job == null) { return; } - var variant = Rand.Range(0, job.Variants, Rand.RandSync.Server); - AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, variant: variant)); + var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient); + AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 80fe1bb35..235b157d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -63,7 +63,7 @@ namespace Barotrauma public struct NetAffliction : INetSerializableStruct { [NetworkSerialize] - public string Identifier; + public Identifier Identifier; [NetworkSerialize] public ushort Strength; @@ -116,7 +116,7 @@ namespace Barotrauma foreach (AfflictionPrefab prefab in AfflictionPrefab.List) { - if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)) + if (prefab.Identifier == Identifier) { cachedPrefab = prefab; return prefab; @@ -128,7 +128,7 @@ namespace Barotrauma set { cachedPrefab = value; - Identifier = value?.Identifier ?? string.Empty; + Identifier = value?.Identifier ?? Identifier.Empty; Strength = 0; Price = 0; } @@ -136,12 +136,12 @@ namespace Barotrauma public readonly bool AfflictionEquals(AfflictionPrefab prefab) { - return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + return prefab.Identifier == Identifier; } public readonly bool AfflictionEquals(NetAffliction affliction) { - return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + return affliction.Identifier == Identifier; } } @@ -221,7 +221,7 @@ namespace Barotrauma foreach (NetAffliction affliction in crewMember.Afflictions) { - health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); + health.ReduceAfflictionOnAllLimbs(affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); } } @@ -333,21 +333,21 @@ namespace Barotrauma #if DEBUG && CLIENT private static readonly CharacterInfo[] TestInfos = { - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human"), - new CharacterInfo("human") + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName), + new CharacterInfo(CharacterPrefab.HumanSpeciesName) }; private static readonly NetAffliction[] TestAfflictions = { - new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 }, - new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 }, - new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 }, - new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 } + new NetAffliction { Identifier = "internaldamage".ToIdentifier(), Strength = 80, Price = 10 }, + new NetAffliction { Identifier = "blunttrauma".ToIdentifier(), Strength = 50, Price = 10 }, + new NetAffliction { Identifier = "lacerations".ToIdentifier(), Strength = 20, Price = 10 }, + new NetAffliction { Identifier = "burn".ToIdentifier(), Strength = 10, Price = 10 } }; #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 5c0429327..aede268f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -96,6 +97,8 @@ namespace Barotrauma public UpgradeManager(CampaignMode campaign) { + UpgradeCategory.Categories.ForEach(c => c.DeterminePrefabsThatAllowUpgrades()); + DebugConsole.Log("Created brand new upgrade manager."); Campaign = campaign; } @@ -112,7 +115,7 @@ namespace Barotrauma } else { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -158,7 +161,7 @@ namespace Barotrauma price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); } //refund the current item - if (replacement != item.prefab) + if (replacement != ((MapEntity)item).Prefab) { price -= item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation); } @@ -219,18 +222,18 @@ namespace Barotrauma // only make the NPC speak if more than 5 minutes have passed since the last purchased service if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) { - UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer); lastUpgradeSpeak = DateTime.Now; } } Campaign.Money -= price; - GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value); PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); #if CLIENT - DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUI.Style.Orange); + DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUIStyle.Orange); #endif if (upgrade == null) @@ -285,7 +288,7 @@ namespace Barotrauma return; } - if (itemToRemove.prefab == itemToInstall) + if (((MapEntity)itemToRemove).Prefab == itemToInstall) { DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!)."); return; @@ -318,13 +321,13 @@ namespace Barotrauma // only make the NPC speak if more than 5 minutes have passed since the last purchased service if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) { - UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer); lastUpgradeSpeak = DateTime.Now; } } Campaign.Money -= price; - GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier); + GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value); foreach (Item itemToSwap in linkedItems) { @@ -367,7 +370,7 @@ namespace Barotrauma return; } - if (itemToRemove?.PendingItemSwap == null && string.IsNullOrEmpty(itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall)) + if (itemToRemove?.PendingItemSwap == null && (itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true)) { DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured)."); return; @@ -385,7 +388,7 @@ namespace Barotrauma // only make the NPC speak if more than 5 minutes have passed since the last purchased service if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now) { - UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer); + UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer); lastUpgradeSpeak = DateTime.Now; } } @@ -429,7 +432,7 @@ namespace Barotrauma { if (!(secondLinkedEntity is Item linkedItem) || linkedItem == item) { continue; } if (linkedItem.AllowSwapping && - linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == linkedItem.prefab.Identifier) && + linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == ((MapEntity)linkedItem).Prefab.Identifier) && linkedItem.Prefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)) { linkedItems.Add(linkedItem); @@ -543,7 +546,7 @@ namespace Barotrauma if (upgrade == null || upgrade.Level != level || isOverMax) { - DebugLog($"{wall.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); + DebugLog($"{((MapEntity)wall).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing..."); FixUpgradeOnItem(wall, prefab, level); } } @@ -572,7 +575,7 @@ namespace Barotrauma if (upgrade == null || upgrade.Level != level || isOverMax) { - DebugLog($"{item.prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); + DebugLog($"{((MapEntity)item).Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}{(isOverMax ? " (Over max level!)" : string.Empty)}. Fixing..."); FixUpgradeOnItem(item, prefab, level); } } @@ -598,7 +601,7 @@ namespace Barotrauma /// /// /// New level that was applied, -1 if no upgrades were applied. - private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine parentSub = null) + private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine? parentSub = null) { int? newLevel = null; if (category.IsWallUpgrade) @@ -753,13 +756,13 @@ namespace Barotrauma // ReSharper disable once LoopCanBeConvertedToQuery foreach (XElement upgrade in element.Elements()) { - string? categoryIdentifier = upgrade.GetAttributeString("category", null); + Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty); UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier); - if (string.IsNullOrWhiteSpace(categoryIdentifier) || category == null) { continue; } + if (categoryIdentifier.IsEmpty || category == null) { continue; } - string? prefabIdentifier = upgrade.GetAttributeString("prefab", null); + Identifier prefabIdentifier = upgrade.GetAttributeIdentifier("prefab", Identifier.Empty); UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier); - if (string.IsNullOrWhiteSpace(prefabIdentifier) || prefab == null) { continue; } + if (prefabIdentifier.IsEmpty || prefab == null) { continue; } int level = upgrade.GetAttributeInt("level", -1); if (level < 0) { continue; } @@ -814,6 +817,6 @@ namespace Barotrauma private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) => PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category); - private static string FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}"; + private static Identifier FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}".ToIdentifier(); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs deleted file mode 100644 index c8b5cd5e7..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ /dev/null @@ -1,1516 +0,0 @@ -using System.Linq; -using System.Xml.Linq; -using System.Collections.Generic; -using Microsoft.Xna.Framework; -using Barotrauma.IO; -using Barotrauma.Extensions; -using System.Diagnostics; -#if CLIENT -using Microsoft.Xna.Framework.Input; -using Microsoft.Xna.Framework.Graphics; -using Barotrauma.Tutorials; -#endif -using System; - -namespace Barotrauma -{ - public enum WindowMode - { - Windowed, Fullscreen, BorderlessWindowed - } - - public enum LosMode - { - None, - Transparent, - Opaque - } - - public partial class GameSettings - { - public const string SavePath = "config.xml"; - public const string PlayerSavePath = "config_player.xml"; - public const string VanillaContentPackagePath = "Data/ContentPackages/Vanilla"; - - public int GraphicsWidth { get; set; } - public int GraphicsHeight { get; set; } - - public bool VSyncEnabled { get; set; } - - public bool TextureCompressionEnabled { get; set; } - - public bool EnableSplashScreen { get; set; } - - public int ParticleLimit { get; set; } - - public float LightMapScale { get; set; } - public bool ChromaticAberrationEnabled { get; set; } - - public bool PauseOnFocusLost { get; set; } - public bool MuteOnFocusLost { get; set; } - public bool DynamicRangeCompressionEnabled { get; set; } - public bool VoipAttenuationEnabled { get; set; } - public bool UseDirectionalVoiceChat { get; set; } - public bool DisableVoiceChatFilters { get; set; } - - public IList AudioDeviceNames; - public IList CaptureDeviceNames; - - public string AudioOutputDevice { get; set; } - - public enum VoiceMode - { - Disabled, - PushToTalk, - Activity - }; - - public VoiceMode VoiceSetting { get; set; } - public string VoiceCaptureDevice { get; set; } - - public float NoiseGateThreshold { get; set; } - - public bool UseLocalVoiceByDefault { get; set; } - -#if CLIENT - public KeyOrMouse[] keyMapping; - private KeyOrMouse[] inventoryKeyMapping; - public static Dictionary ConsoleKeybinds = new Dictionary(); -#endif - - private WindowMode windowMode; - - private LosMode losMode; - - public List> jobPreferences; - - public string QuickStartSubmarineName; - -#if USE_STEAM - public bool RequireSteamAuthentication { get; set; } - public bool UseSteamMatchmaking { get; set; } -#else - public bool RequireSteamAuthentication - { - get { return false; } - set { /*do nothing*/ } - } - public bool UseSteamMatchmaking - { - get { return false; } - set { /*do nothing*/ } - } -#endif - public bool UseDualModeSockets { get; set; } = true; - - public bool AutoUpdateWorkshopItems; - - public WindowMode WindowMode - { - get { return windowMode; } - set - { -#if (OSX) - // Fullscreen doesn't work on macOS, so just force any usage of it to borderless windowed. - if (value == WindowMode.Fullscreen) - { - windowMode = WindowMode.BorderlessWindowed; - return; - } -#endif - windowMode = value; - } - } - - public List> JobPreferences - { - get { return jobPreferences; } - set { jobPreferences = value; } - } - - public CharacterTeamType TeamPreference { get; set; } - - public bool AreJobPreferencesEqual(List> compareTo) - { - if (jobPreferences == null || compareTo == null) return false; - if (jobPreferences.Count != compareTo.Count) return false; - - for (int i = 0; i < jobPreferences.Count; i++) - { - if (jobPreferences[i].First != compareTo[i].First || jobPreferences[i].Second != compareTo[i].Second) return false; - } - - return true; - } - - internal CharacterInfo.HeadInfo PlayerCharacterCustomization { get; set; } - - private float aimAssistAmount; - public float AimAssistAmount - { - get { return aimAssistAmount; } - set { aimAssistAmount = MathHelper.Clamp(value, 0.0f, 5.0f); } - } - - public bool EnableMouseLook { get; set; } = true; - - public bool EnableRadialDistortion { get; set; } = true; - - public bool CrewMenuOpen { get; set; } = true; - public bool ChatOpen { get; set; } = true; - - public float CorpseDespawnDelay { get; set; } = 10.0f * 60.0f; - - /// - /// How many corpses there can be in a sub before they start to get despawned - /// - public int CorpsesPerSubDespawnThreshold { get; set; } = 10; - - private string overrideSaveFolder, overrideMultiplayerSaveFolder; - - private bool unsavedSettings; - public bool UnsavedSettings - { - get - { - return unsavedSettings; - } - private set - { - unsavedSettings = value; -#if CLIENT - if (applyButton != null) - { - applyButton.Enabled = unsavedSettings; - applyButton.Text = TextManager.Get(unsavedSettings ? "ApplySettingsButtonUnsavedChanges" : "ApplySettingsButton"); - } -#endif - } - } - - private float soundVolume, musicVolume, voiceChatVolume, microphoneVolume; - - public float SoundVolume - { - get { return soundVolume; } - set - { - soundVolume = MathHelper.Clamp(value, 0.0f, 1.0f); -#if CLIENT - if (GameMain.SoundManager != null) - { - GameMain.SoundManager.SetCategoryGainMultiplier("default", soundVolume, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("ui", soundVolume, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", soundVolume, 0); - } -#endif - } - } - - public float MusicVolume - { - get { return musicVolume; } - set - { - musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f); -#if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume * 0.7f, 0); -#endif - } - } - - public float VoiceChatVolume - { - get { return voiceChatVolume; } - set - { - voiceChatVolume = MathHelper.Clamp(value, 0.0f, 2.0f); -#if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", Math.Min(voiceChatVolume, 1.0f), 0); -#endif - } - } - - - public int VoiceChatCutoffPrevention - { - get; - set; - } - - public const float MaxMicrophoneVolume = 10.0f; - public float MicrophoneVolume - { - get { return microphoneVolume; } - set - { - microphoneVolume = MathHelper.Clamp(value, 0.2f, MaxMicrophoneVolume); - } - } - public string Language - { - get { return TextManager.Language; } - set { TextManager.Language = value; } - } - - public ContentPackage CurrentCorePackage { get; private set; } - private readonly List enabledRegularPackages = new List(); - public IReadOnlyList EnabledRegularPackages - { - get { return enabledRegularPackages; } - } - - public IEnumerable AllEnabledPackages - { - get - { - yield return CurrentCorePackage; - foreach (var package in EnabledRegularPackages) - { - yield return package; - } - } - } - - public bool ContentPackageSelectionDirtyNotification - { - get; - set; - } - - public bool ContentPackageSelectionDirty - { - get; - private set; - } - - public XElement ServerFilterElement - { - get; - private set; - } - - public volatile bool SuppressModFolderWatcher; - - public volatile bool WaitingForAutoUpdate; - - public bool DisableInGameHints { get; set; } - -#if DEBUG - public bool AutomaticQuickStartEnabled { get; set; } - public bool AutomaticCampaignLoadEnabled { get; set; } - public bool TextManagerDebugModeEnabled { get; set; } - public bool TestScreenEnabled { get; set; } - - public bool ModBreakerMode { get; set; } -#endif - - private static int ContentFileLoadOrder(ContentFile a) - { - switch (a.Type) - { - case ContentType.Text: - return -2; - case ContentType.Afflictions: - return -1; - case ContentType.ItemAssembly: - return 1; - default: - return 0; - } - } - - public void SelectCorePackage(ContentPackage contentPackage, bool forceReloadAll = false) - { - if (!contentPackage.IsCorePackage) { return; } - if (!contentPackage.ContainsRequiredCorePackageFiles(out _)) { return; } - - ContentPackage prevCorePackage = CurrentCorePackage; - - CurrentCorePackage = contentPackage; - - if (prevCorePackage != null) - { - List filesToRemove = prevCorePackage.Files.Where(f1 => forceReloadAll || - !contentPackage.Files.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - List filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll || - !prevCorePackage.Files.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - DisableContentPackageItems(filesToRemove); - EnableContentPackageItems(filesToAdd); - - RefreshContentPackageItems(filesToAdd.Concat(filesToRemove)); - } - else - { - EnableContentPackageItems(contentPackage.Files); - RefreshContentPackageItems(contentPackage.Files); - } - } - - public void AutoSelectCorePackage(IEnumerable toRemove) - { - SelectCorePackage(ContentPackage.CorePackages.Find(cpp => - (toRemove == null || !toRemove.Contains(cpp)) && - cpp.ContainsRequiredCorePackageFiles(out _))); - } - - private List> backupModOrder; - - public void BackUpModOrder() - { - backupModOrder = new List> - { - new Tuple(CurrentCorePackage, true) - }; - for (int i = 0; i < ContentPackage.RegularPackages.Count; i++) - { - var p = ContentPackage.RegularPackages[i]; - backupModOrder.Add(new Tuple(p, EnabledRegularPackages.Contains(p))); - } - } - - public void SwapPackages(ContentPackage corePackage, List regularPackages) - { - List packagesToDisable = new List(); - packagesToDisable.Add(CurrentCorePackage); - packagesToDisable.AddRange(enabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent)); - List packagesToEnable = new List(); - packagesToEnable.Add(corePackage); - List regularPackagesToAdd = regularPackages.Where(p => p.HasMultiplayerIncompatibleContent).ToList(); - packagesToEnable.AddRange(regularPackagesToAdd); - - IEnumerable filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files); - IEnumerable filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files); - - List filesToDisable = filesOfDisabledPkgs.Where(f1 => - !filesOfEnabledPkgs.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - List filesToEnable = filesOfEnabledPkgs.Where(f1 => - !filesOfDisabledPkgs.Any(f2 => - Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - - CurrentCorePackage = corePackage; - enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackagesToAdd); - - DisableContentPackageItems(filesToDisable); - EnableContentPackageItems(filesToEnable); - - RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable)); - - ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p), config: this); - -#if DEBUG - Debug.Assert(enabledRegularPackages.Count == enabledRegularPackages.Distinct().Count()); -#endif - } - - public void RestoreBackupPackages() - { - if (backupModOrder == null) { return; } - - SwapPackages( - backupModOrder[0].Item1, - backupModOrder.Skip(1).Where(p => p.Item2).Select(p => p.Item1).ToList()); - ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p), config: this); - - backupModOrder = null; - } - - public void EnableRegularPackage(ContentPackage contentPackage) - { - if (contentPackage.IsCorePackage) { return; } - if (!enabledRegularPackages.Contains(contentPackage)) - { - enabledRegularPackages.Add(contentPackage); - SortContentPackages(); - - EnableContentPackageItems(contentPackage.Files); - RefreshContentPackageItems(contentPackage.Files); - } - } - - public void DisableRegularPackage(ContentPackage contentPackage) - { - if (contentPackage.IsCorePackage) { return; } - if (enabledRegularPackages.Contains(contentPackage)) - { - enabledRegularPackages.Remove(contentPackage); - SortContentPackages(); - - DisableContentPackageItems(contentPackage.Files); - RefreshContentPackageItems(contentPackage.Files); - } - } - - public void SortContentPackages(bool refreshAll = false) - { - var previousEnabledRegularPackages = enabledRegularPackages.ToList(); - - for (int i = enabledRegularPackages.Count - 1; i >= 0; i--) - { - var package = enabledRegularPackages[i]; - if (!ContentPackage.RegularPackages.Contains(package)) - { - ContentPackage replacement = ContentPackage.RegularPackages.Find(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase)); - if (replacement != null) - { - enabledRegularPackages[i] = replacement; - } - else - { - DisableRegularPackage(package); - } - } - } - - if (CurrentCorePackage == null) - { - AutoSelectCorePackage(null); - } - else if (!ContentPackage.CorePackages.Contains(CurrentCorePackage)) - { - ContentPackage replacement = ContentPackage.CorePackages.Find(p => p.Name.Equals(CurrentCorePackage.Name, StringComparison.OrdinalIgnoreCase)); - if (replacement != null) - { - SelectCorePackage(replacement); - } - else - { - AutoSelectCorePackage(null); - } - } - - var sortedSelected = enabledRegularPackages - .OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p)) - .ToList(); - if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) - { - CheckModded(); - return; - } - enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected); - - CharacterPrefab.Prefabs.SortAll(); - AfflictionPrefab.Prefabs.SortAll(); - JobPrefab.Prefabs.SortAll(); - ItemPrefab.Prefabs.SortAll(); - CoreEntityPrefab.Prefabs.SortAll(); - ItemAssemblyPrefab.Prefabs.SortAll(); - StructurePrefab.Prefabs.SortAll(); - -#if CLIENT - GameMain.DecalManager?.Prefabs.SortAll(); - GameMain.ParticleManager?.Prefabs.SortAll(); -#endif - - if (refreshAll) - { - RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files)); - } - - CheckModded(); - - void CheckModded() - { - if (AllEnabledPackages.Any(p => p != GameMain.VanillaContent && p.HasMultiplayerIncompatibleContent)) - { - GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Modded); - } - else - { - GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Vanilla); - } - } - } - - public void EnableContentPackageItems(IEnumerable unorderedFiles) - { - if (WaitingForAutoUpdate) { return; } - IOrderedEnumerable files = unorderedFiles.OrderBy(ContentFileLoadOrder); - foreach (ContentFile file in files) - { - switch (file.Type) - { - case ContentType.Character: - CharacterPrefab.LoadFromFile(file); - break; - case ContentType.Corpses: - CorpsePrefab.LoadFromFile(file); - break; - case ContentType.NPCConversations: - NPCConversation.LoadFromFile(file); - break; - case ContentType.Jobs: - JobPrefab.LoadFromFile(file); - break; - case ContentType.Item: - ItemPrefab.LoadFromFile(file); - break; - case ContentType.ItemAssembly: - new ItemAssemblyPrefab(file.Path); - break; - case ContentType.Structure: - StructurePrefab.LoadFromFile(file); - break; - case ContentType.Text: - TextManager.LoadTextPack(file.Path); - break; - case ContentType.Talents: - TalentPrefab.LoadFromFile(file); - break; - case ContentType.TalentTrees: - TalentTree.LoadFromFile(file); - break; -#if CLIENT - case ContentType.Particles: - GameMain.ParticleManager?.LoadPrefabsFromFile(file); - break; - case ContentType.Decals: - GameMain.DecalManager?.LoadFromFile(file); - break; -#endif - } - - UpdateContentPackageDirtyFlag(file); - } - } - - public void DisableContentPackageItems(IEnumerable unorderedFiles) - { - if (WaitingForAutoUpdate) { return; } - IOrderedEnumerable files = unorderedFiles.OrderBy(ContentFileLoadOrder); - foreach (ContentFile file in files) - { - switch (file.Type) - { - case ContentType.Character: - CharacterPrefab.RemoveByFile(file.Path); - break; - case ContentType.Corpses: - CorpsePrefab.RemoveByFile(file.Path); - break; - case ContentType.NPCConversations: - NPCConversation.RemoveByFile(file.Path); - break; - case ContentType.Jobs: - JobPrefab.RemoveByFile(file.Path); - break; - case ContentType.Item: - ItemPrefab.RemoveByFile(file.Path); - break; - case ContentType.ItemAssembly: - ItemAssemblyPrefab.Remove(file.Path); - break; - case ContentType.Structure: - StructurePrefab.RemoveByFile(file.Path); - break; - case ContentType.Text: - TextManager.RemoveTextPack(file.Path); - break; - case ContentType.Talents: - TalentPrefab.LoadFromFile(file); - break; - case ContentType.TalentTrees: - TalentTree.LoadFromFile(file); - break; -#if CLIENT - case ContentType.Particles: - GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); - break; - case ContentType.Decals: - GameMain.DecalManager?.RemoveByFile(file.Path); - break; -#endif - } - - UpdateContentPackageDirtyFlag(file); - } - } - - public void RefreshContentPackageItems(IEnumerable files) - { - if (WaitingForAutoUpdate) { return; } - if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (files.Any(f => f.Type == ContentType.Submarine || - f.Type == ContentType.Outpost || - f.Type == ContentType.OutpostModule || - f.Type == ContentType.Wreck || - f.Type == ContentType.BeaconStation || - f.Type == ContentType.EnemySubmarine)) { SubmarineInfo.RefreshSavedSubs(); } - if (files.Any(f => f.Type == ContentType.NPCSets)) { NPCSet.LoadSets(); } - if (files.Any(f => f.Type == ContentType.OutpostConfig)) { OutpostGenerationParams.LoadPresets(); } - if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); } - if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); } - if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (files.Any(f => f.Type == ContentType.RandomEvents || - f.Type == ContentType.LocationTypes)) - { - LocationType.List.Clear(); - EventSet.LoadPrefabs(); - LocationType.Init(); - } - if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); } - if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); } - if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); } - if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); } - if (files.Any(f => f.Type == ContentType.CaveGenerationParameters)) { CaveGenerationParams.LoadPresets(); } - if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); } - if (files.Any(f => f.Type == ContentType.Orders)) { Order.Init(); } - if (files.Any(f => f.Type == ContentType.EventManagerSettings)) { EventManagerSettings.Init(); } - if (files.Any(f => f.Type == ContentType.WreckAIConfig)) { WreckAIConfig.LoadAll(); } - if (files.Any(f => f.Type == ContentType.SkillSettings)) { SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); } - -#if CLIENT - if (files.Any(f => f.Type == ContentType.Tutorials)) { Tutorial.Init(); } - if (files.Any(f => f.Type == ContentType.Sounds)) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif - } - - private readonly static ContentType[] hotswappableContentTypes = new ContentType[] - { - ContentType.Character, - ContentType.Corpses, - ContentType.NPCConversations, - ContentType.Jobs, - ContentType.Orders, - ContentType.EventManagerSettings, - ContentType.Item, - ContentType.ItemAssembly, - ContentType.Structure, - ContentType.Submarine, - ContentType.Text, - ContentType.Afflictions, - ContentType.RuinConfig, - ContentType.RandomEvents, - ContentType.Missions, - ContentType.LevelObjectPrefabs, - ContentType.LocationTypes, - ContentType.MapGenerationParameters, - ContentType.LevelGenerationParameters, - ContentType.CaveGenerationParameters, - ContentType.Sounds, - ContentType.Particles, - ContentType.Decals, - ContentType.Outpost, - ContentType.OutpostModule, - ContentType.OutpostConfig, - ContentType.NPCSets, - ContentType.Factions, - ContentType.Wreck, - ContentType.WreckAIConfig, - ContentType.BeaconStation, - ContentType.BackgroundCreaturePrefabs, - ContentType.ServerExecutable, - ContentType.TraitorMissions, - ContentType.Tutorials, - ContentType.SkillSettings, - ContentType.None - }; - - private void UpdateContentPackageDirtyFlag(ContentFile file) - { - if (!hotswappableContentTypes.Contains(file.Type)) - { - if (ContentPackage.MultiplayerIncompatibleContent.Contains(file.Type)) - { - ContentPackageSelectionDirty = true; - } - ContentPackageSelectionDirtyNotification = true; - } - } - - public string MasterServerUrl { get; set; } - public string RemoteContentUrl { get; set; } - public bool AutoCheckUpdates { get; set; } - - private string playerName; - public string PlayerName - { - get - { - return string.IsNullOrWhiteSpace(playerName) ? Steam.SteamManager.GetUsername() : playerName; - } - set - { - if (playerName != value) - { - playerName = value; - } - } - } - - public LosMode LosMode - { - get { return losMode; } - set { losMode = value; } - } - - private const float MinHUDScale = 0.75f, MaxHUDScale = 1.25f; - public static float HUDScale { get; set; } - - private const float MinInventoryScale = 0.75f, MaxInventoryScale = 1.25f; - public static float InventoryScale { get; set; } - - private const float MinTextScale = 0.5f, MaxTextScale = 1.5f; - public static float TextScale { get; set; } - private bool textScaleDirty; - - public List CompletedTutorialNames { get; private set; } - /// - /// Identifiers of hints the player has chosen not to see again - /// - public HashSet IgnoredHints { get; private set; } = new HashSet(); - public HashSet EncounteredCreatures { get; private set; } = new HashSet(); - public HashSet KilledCreatures { get; private set; } = new HashSet(); - - public readonly HashSet RecentlyEncounteredCreatures = new HashSet(); - - public static bool VerboseLogging { get; set; } - public static bool SaveDebugConsoleLogs { get; set; } - - public bool CampaignDisclaimerShown, EditorDisclaimerShown; - - public bool ShowLanguageSelectionPrompt { get; set; } - - public static bool ShowOffensiveServerPrompt { get; set; } - - private bool showTutorialSkipWarning = true; - - public static bool EnableSubmarineAutoSave { get; set; } - public static int MaximumAutoSaves { get; set; } - public static int AutoSaveIntervalSeconds { get; set; } - public static Color SubEditorBackgroundColor { get; set; } - public static int SubEditorMaxUndoBuffer { get; set; } - - public bool ShowTutorialSkipWarning - { - get { return showTutorialSkipWarning && CompletedTutorialNames.Count == 0; } - set { showTutorialSkipWarning = value; } - } - - public GameSettings() - { - ContentPackage.LoadAll(); - CompletedTutorialNames = new List(); - - LoadDefaultConfig(); - - LoadPlayerConfig(); - } - - private void LoadDefaultConfig(bool setLanguage = true, bool loadContentPackages = true) - { - XDocument doc = XMLExtensions.TryLoadXml(SavePath); - if (doc == null) - { - GraphicsWidth = 1024; - GraphicsHeight = 768; - MasterServerUrl = ""; - SelectCorePackage(ContentPackage.CorePackages.FirstOrDefault()); - jobPreferences = new List>(); - return; - } - - bool resetLanguage = setLanguage || string.IsNullOrEmpty(Language); - SetDefaultValues(resetLanguage); -#if CLIENT - SetDefaultBindings(doc, legacy: false); -#endif - - MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", MasterServerUrl); - RemoteContentUrl = doc.Root.GetAttributeString("remotecontenturl", RemoteContentUrl); - VerboseLogging = doc.Root.GetAttributeBool("verboselogging", VerboseLogging); - SaveDebugConsoleLogs = doc.Root.GetAttributeBool("savedebugconsolelogs", SaveDebugConsoleLogs); - AutoUpdateWorkshopItems = doc.Root.GetAttributeBool("autoupdateworkshopitems", AutoUpdateWorkshopItems); - - LoadGeneralSettings(doc, resetLanguage); - LoadGraphicSettings(doc); - LoadAudioSettings(doc); -#if CLIENT - LoadControls(doc); - LoadSubEditorImages(doc); -#endif - if (loadContentPackages) - { - LoadContentPackages(doc); - } - -#if DEBUG - WindowMode = WindowMode.Windowed; -#endif - - UnsavedSettings = false; - } - -#region Load PlayerConfig - public void LoadPlayerConfig() - { - bool fileFound = LoadPlayerConfigInternal(); -#if CLIENT - CheckBindings(!fileFound); -#endif - if (!fileFound) - { - ShowLanguageSelectionPrompt = true; - SaveNewPlayerConfig(); - } - } - - // TODO: DRY - /// - /// Returns false if no player config file was found, in which case a new file is created. - /// - private bool LoadPlayerConfigInternal() - { - XDocument doc = XMLExtensions.LoadXml(PlayerSavePath); - if (doc?.Root == null) - { - ShowTutorialSkipWarning = true; - return false; - } - LoadGeneralSettings(doc); - LoadGraphicSettings(doc); - LoadAudioSettings(doc); -#if CLIENT - LoadControls(doc); - LoadSubEditorImages(doc); -#endif - LoadContentPackages(doc); - - //allow overriding the save paths in the config file - if (doc.Root.Attribute("overridesavefolder") != null) - { - overrideSaveFolder = SaveUtil.SaveFolder = doc.Root.GetAttributeString("overridesavefolder", ""); - overrideMultiplayerSaveFolder = SaveUtil.MultiplayerSaveFolder = Path.Combine(overrideSaveFolder, "Multiplayer"); - } - if (doc.Root.Attribute("overridemultiplayersavefolder") != null) - { - overrideMultiplayerSaveFolder = SaveUtil.MultiplayerSaveFolder = doc.Root.GetAttributeString("overridemultiplayersavefolder", ""); - } - - XElement tutorialsElement = doc.Root.Element("tutorials"); - if (tutorialsElement != null) - { - foreach (XElement element in tutorialsElement.Elements()) - { - CompletedTutorialNames.Add(element.GetAttributeString("name", "")); - } - } - - if (doc.Root.Element("ignoredhints") is XElement ignoredHintsElement) - { - IgnoredHints = new HashSet(ignoredHintsElement.GetAttributeStringArray("identifiers", new string[0], convertToLowerInvariant: true)); - } - - XElement encounters = doc.Root.Element("encountered"); - if (encounters != null) - { - EncounteredCreatures = new HashSet(encounters.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true)); - } - XElement kills = doc.Root.Element("killed"); - if (kills != null) - { - KilledCreatures = new HashSet(kills.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true)); - } - - ServerFilterElement = doc.Root.Element("serverfilters"); - - UnsavedSettings = false; - textScaleDirty = false; - return true; - } - -#endregion - -#region Save PlayerConfig - public bool SaveNewPlayerConfig() - { - XDocument doc = new XDocument(); - UnsavedSettings = false; - - if (doc.Root == null) - { - doc.Add(new XElement("config")); - } - - doc.Root.Add( - new XAttribute("gameversion", GameMain.Version.ToString()), - new XAttribute("language", TextManager.Language), - new XAttribute("masterserverurl", MasterServerUrl), - new XAttribute("autocheckupdates", AutoCheckUpdates), - new XAttribute("musicvolume", musicVolume), - new XAttribute("soundvolume", soundVolume), - new XAttribute("verboselogging", VerboseLogging), - new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), - new XAttribute("submarineautosave", EnableSubmarineAutoSave), - new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), - new XAttribute("maxautosaves", MaximumAutoSaves), - new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds), - new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), - new XAttribute("enablesplashscreen", EnableSplashScreen), - new XAttribute("usesteammatchmaking", UseSteamMatchmaking), - new XAttribute("quickstartsub", QuickStartSubmarineName), - new XAttribute("requiresteamauthentication", RequireSteamAuthentication), - new XAttribute("autoupdateworkshopitems", AutoUpdateWorkshopItems), - new XAttribute("pauseonfocuslost", PauseOnFocusLost), - new XAttribute("aimassistamount", aimAssistAmount), - new XAttribute("enablemouselook", EnableMouseLook), - new XAttribute("radialdistortion", EnableRadialDistortion), - new XAttribute("chatopen", ChatOpen), - new XAttribute("crewmenuopen", CrewMenuOpen), - new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown), - new XAttribute("editordisclaimershown", EditorDisclaimerShown), - new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning), - new XAttribute("corpsedespawndelay", CorpseDespawnDelay), - new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold), - new XAttribute("usedualmodesockets", UseDualModeSockets), - new XAttribute("disableingamehints", DisableInGameHints) -#if DEBUG - , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) - , new XAttribute(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled) - , new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled) - , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) - , new XAttribute("modbreakermode", ModBreakerMode) -#endif - ); - - if (!string.IsNullOrEmpty(overrideSaveFolder)) - { - doc.Root.Add(new XAttribute("overridesavefolder", overrideSaveFolder)); - } - if (!string.IsNullOrEmpty(overrideMultiplayerSaveFolder)) - { - doc.Root.Add(new XAttribute("overridemultiplayersavefolder", overrideMultiplayerSaveFolder)); - } - - XElement gMode = doc.Root.Element("graphicsmode"); - if (gMode == null) - { - gMode = new XElement("graphicsmode"); - doc.Root.Add(gMode); - } - - if (GraphicsWidth == 0 || GraphicsHeight == 0) - { - gMode.ReplaceAttributes(new XAttribute("displaymode", windowMode)); - } - else - { - gMode.ReplaceAttributes( - new XAttribute("width", GraphicsWidth), - new XAttribute("height", GraphicsHeight), - new XAttribute("vsync", VSyncEnabled), - new XAttribute("compresstextures", TextureCompressionEnabled), - new XAttribute("framelimit", Timing.FrameLimit), - new XAttribute("displaymode", windowMode)); - } - - XElement audio = doc.Root.Element("audio"); - if (audio == null) - { - audio = new XElement("audio"); - doc.Root.Add(audio); - } - audio.ReplaceAttributes( - new XAttribute("musicvolume", musicVolume), - new XAttribute("soundvolume", soundVolume), - new XAttribute("voicechatvolume", voiceChatVolume), - new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), - new XAttribute("microphonevolume", microphoneVolume), - new XAttribute("muteonfocuslost", MuteOnFocusLost), - new XAttribute("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled), - new XAttribute("voipattenuationenabled", VoipAttenuationEnabled), - new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat), - new XAttribute("voicesetting", VoiceSetting), - new XAttribute("audiooutputdevice", System.Xml.XmlConvert.EncodeName(AudioOutputDevice ?? "")), - new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")), - new XAttribute("noisegatethreshold", NoiseGateThreshold), - new XAttribute("uselocalvoicebydefault", UseLocalVoiceByDefault)); - - XElement gSettings = doc.Root.Element("graphicssettings"); - if (gSettings == null) - { - gSettings = new XElement("graphicssettings"); - doc.Root.Add(gSettings); - } - - gSettings.ReplaceAttributes( - new XAttribute("particlelimit", ParticleLimit), - new XAttribute("lightmapscale", LightMapScale), - new XAttribute("chromaticaberration", ChromaticAberrationEnabled), - new XAttribute("losmode", LosMode), - new XAttribute("hudscale", HUDScale), - new XAttribute("inventoryscale", InventoryScale), - new XAttribute("textscale", TextScale)); - - XElement contentPackagesElement = new XElement("contentpackages"); - - string corePackageName = (CurrentCorePackage ?? ContentPackage.CorePackages.FirstOrDefault()).Name; - contentPackagesElement.Add(new XElement("core", new XAttribute("name", corePackageName))); - - XElement regularPackagesElement = new XElement("regular"); - foreach (ContentPackage package in ContentPackage.RegularPackages) - { - XElement packageElement = new XElement("package", new XAttribute("name", package.Name)); - if (EnabledRegularPackages.Contains(package)) { packageElement.Add(new XAttribute("enabled", "true")); } - regularPackagesElement.Add(packageElement); - } - contentPackagesElement.Add(regularPackagesElement); - - doc.Root.Add(contentPackagesElement); - -#if CLIENT - var keyMappingElement = new XElement("keymapping"); - doc.Root.Add(keyMappingElement); - for (int i = 0; i < keyMapping.Length; i++) - { - var key = keyMapping[i]; - if (key == null) { continue; } - if (key.MouseButton == MouseButton.None) - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key)); - } - else - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); - } - } - - var inventoryKeyMappingElement = new XElement("inventorykeymapping"); - doc.Root.Add(inventoryKeyMappingElement); - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - KeyOrMouse bind = inventoryKeyMapping[i]; - if (bind.MouseButton == MouseButton.None) - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); - } - else - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); - } - } - - var debugconsoleKeyMappingElement = new XElement("debugconsolemapping"); - doc.Root.Add(debugconsoleKeyMappingElement); - foreach (var (key, command) in ConsoleKeybinds) - { - debugconsoleKeyMappingElement.Add(new XElement("Keybind", - new XAttribute("key", key.ToString()), - new XAttribute("command", command))); - } - - if (ServerFilterElement == null) - { - ShowOffensiveServerPrompt = true; - ServerFilterElement = new XElement("serverfilters"); - } - GameMain.ServerListScreen?.SaveServerFilters(ServerFilterElement); - doc.Root.Add(ServerFilterElement); - - SubEditorScreen.ImageManager.Save(doc.Root); -#endif - - var gameplay = new XElement("gameplay"); - var jobPreferences = new XElement("jobpreferences"); - foreach (Pair job in JobPreferences) - { - XElement jobElement = new XElement("job"); - jobElement.Add(new XAttribute("identifier", job.First)); - jobElement.Add(new XAttribute("variant", job.Second)); - jobPreferences.Add(jobElement); - } - gameplay.Add(jobPreferences); - doc.Root.Add(gameplay); - - var playerElement = new XElement("player", new XAttribute("name", playerName ?? "")); - if (PlayerCharacterCustomization != null) - { - playerElement.SetAttributeValue("headindex", PlayerCharacterCustomization.HeadSpriteId); - if (PlayerCharacterCustomization.gender != Gender.None) { playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); } - if (PlayerCharacterCustomization.race != Race.None) { playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); } - playerElement.SetAttributeValue("hairindex", PlayerCharacterCustomization.HairIndex); - playerElement.SetAttributeValue("beardindex", PlayerCharacterCustomization.BeardIndex); - playerElement.SetAttributeValue("moustacheindex", PlayerCharacterCustomization.MoustacheIndex); - playerElement.SetAttributeValue("faceattachmentindex", PlayerCharacterCustomization.FaceAttachmentIndex); - playerElement.SetAttributeValue("skincolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.SkinColor)); - playerElement.SetAttributeValue("haircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.HairColor)); - playerElement.SetAttributeValue("facialhaircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.FacialHairColor)); - } - doc.Root.Add(playerElement); - -#if CLIENT - if (Tutorial.Tutorials != null) - { - foreach (Tutorial tutorial in Tutorial.Tutorials) - { - if (tutorial.Completed && !CompletedTutorialNames.Contains(tutorial.Identifier)) - { - CompletedTutorialNames.Add(tutorial.Identifier); - } - } - } -#endif - var tutorialElement = new XElement("tutorials"); - foreach (string tutorialName in CompletedTutorialNames) - { - tutorialElement.Add(new XElement("Tutorial", new XAttribute("name", tutorialName))); - } - doc.Root.Add(tutorialElement); - - doc.Root.Add(new XElement("ignoredhints", new XAttribute("identifiers", string.Join(",", IgnoredHints).Trim().ToLowerInvariant()))); - - doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant()))); - doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant()))); - - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings - { - Indent = true, - OmitXmlDeclaration = true, - NewLineOnAttributes = true - }; - - try - { - using (var writer = XmlWriter.Create(PlayerSavePath, settings)) - { - doc.WriteTo(writer); - writer.Flush(); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving game settings failed.", e); - GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, - "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - return false; - } - - return true; - } -#endregion - -#region Loading Configs - private void LoadGeneralSettings(XDocument doc, bool setLanguage = true) - { - if (setLanguage) - { - Language = doc.Root.GetAttributeString("language", Language); - } - AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); - QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); - EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); - MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8); - AutoSaveIntervalSeconds = doc.Root.GetAttributeInt("autosaveintervalseconds", 300); - SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f)); - SubEditorMaxUndoBuffer = doc.Root.GetAttributeInt("subeditorundobuffer", 32); - UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); - RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication); - EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); - PauseOnFocusLost = doc.Root.GetAttributeBool("pauseonfocuslost", PauseOnFocusLost); - AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", AimAssistAmount); - EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook); - EnableRadialDistortion = doc.Root.GetAttributeBool("radialdistortion", EnableRadialDistortion); - CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen); - ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen); - CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60); - CorpsesPerSubDespawnThreshold = doc.Root.GetAttributeInt("corpsespersubdespawnthreshold", 5); - CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown); - EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); - ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true); - UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true); - DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints); -#if DEBUG - AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); - TestScreenEnabled = doc.Root.GetAttributeBool(nameof(TestScreenEnabled).ToLower(), TestScreenEnabled); - AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled); - TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); - ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode); -#endif - XElement gameplayElement = doc.Root.Element("gameplay"); - jobPreferences = new List>(); - if (gameplayElement != null) - { - var preferencesElement = gameplayElement.Element("jobpreferences"); - if (preferencesElement != null) - { - foreach (XElement ele in preferencesElement.Elements("job")) - { - string jobIdentifier = ele.GetAttributeString("identifier", ""); - int outfitVariant = ele.GetAttributeInt("variant", 1); - if (string.IsNullOrEmpty(jobIdentifier)) continue; - jobPreferences.Add(new Pair(jobIdentifier, outfitVariant)); - } - } - - var teamPreferenceElement = gameplayElement.Element("teampreference"); - if (teamPreferenceElement != null) - { - TeamPreference = (CharacterTeamType)Enum.Parse(typeof(CharacterTeamType), teamPreferenceElement.GetAttributeString("team", CharacterTeamType.None.ToString())); - } - } - - XElement playerElement = doc.Root.Element("player"); - if (playerElement != null) - { - playerName = playerElement.GetAttributeString("name", playerName); - int head = playerElement.GetAttributeInt("headindex", -1); - Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender gender); - Enum.TryParse(playerElement.GetAttributeString("race", "none"), true, out Race race); - int hair = playerElement.GetAttributeInt("hairindex", -1); - int beard = playerElement.GetAttributeInt("beardindex", -1); - int moustache = playerElement.GetAttributeInt("moustacheindex", -1); - int faceAttachment = playerElement.GetAttributeInt("faceattachmentindex", -1); - Color skinColor = playerElement.GetAttributeColor("skincolor", Color.Black); - Color hairColor = playerElement.GetAttributeColor("haircolor", Color.Black); - Color facialHairColor = playerElement.GetAttributeColor("facialhaircolor", Color.Black); - PlayerCharacterCustomization = new CharacterInfo.HeadInfo(head, gender, race, hair, beard, moustache, faceAttachment) - { - SkinColor = skinColor, - HairColor = hairColor, - FacialHairColor = facialHairColor - }; - } - } - - private void LoadGraphicSettings(XDocument doc) - { - XElement graphicsMode = doc.Root.Element("graphicsmode"); - GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth); - GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight); - VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled); - TextureCompressionEnabled = graphicsMode.GetAttributeBool("compresstextures", TextureCompressionEnabled); - Timing.FrameLimit = graphicsMode.GetAttributeInt("framelimit", 200); - - XElement graphicsSettings = doc.Root.Element("graphicssettings"); - ParticleLimit = graphicsSettings.GetAttributeInt("particlelimit", ParticleLimit); - LightMapScale = MathHelper.Clamp(graphicsSettings.GetAttributeFloat("lightmapscale", LightMapScale), 0.1f, 1.0f); - ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", ChromaticAberrationEnabled); - HUDScale = graphicsSettings.GetAttributeFloat("hudscale", HUDScale); - InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", InventoryScale); - TextScale = graphicsSettings.GetAttributeFloat("textscale", TextScale); - var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent"); - if (!Enum.TryParse(losModeStr, out losMode)) - { - losMode = LosMode.Transparent; - } -#if CLIENT - if (GraphicsWidth == 0 || GraphicsHeight == 0) - { - GraphicsWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width; - GraphicsHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height; - } -#endif - var windowModeStr = graphicsMode.GetAttributeString("displaymode", "Fullscreen"); - if (!Enum.TryParse(windowModeStr, out windowMode)) - { - windowMode = WindowMode.Fullscreen; - } - } - - private void LoadAudioSettings(XDocument doc) - { - XElement audioSettings = doc.Root.Element("audio"); - if (audioSettings != null) - { - SoundVolume = audioSettings.GetAttributeFloat("soundvolume", SoundVolume); - MusicVolume = audioSettings.GetAttributeFloat("musicvolume", MusicVolume); - DynamicRangeCompressionEnabled = audioSettings.GetAttributeBool("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled); - VoipAttenuationEnabled = audioSettings.GetAttributeBool("voipattenuationenabled", VoipAttenuationEnabled); - VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume); - VoiceChatCutoffPrevention = audioSettings.GetAttributeInt("voicechatcutoffprevention", VoiceChatCutoffPrevention); - MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost); - - UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat); - VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice)); - AudioOutputDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("audiooutputdevice", AudioOutputDevice)); - NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold); - UseLocalVoiceByDefault = audioSettings.GetAttributeBool("uselocalvoicebydefault", UseLocalVoiceByDefault); - MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume); - string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", ""); - if (Enum.TryParse(voiceSettingStr, out VoiceMode voiceSetting)) - { - VoiceSetting = voiceSetting; - } - } - } - - private void LoadContentPackages(XDocument doc) - { - CurrentCorePackage = null; - enabledRegularPackages.Clear(); - -#if DEBUG && CLIENT - if (ModBreakerMode) - { - CurrentCorePackage = ContentPackage.CorePackages.GetRandom(); - foreach (var regularPackage in ContentPackage.RegularPackages) - { - if (Rand.Range(0.0, 1.0) <= 0.5) - { - enabledRegularPackages.Add(regularPackage); - } - } - ContentPackage.SortContentPackages(p => - { - return Rand.Int(int.MaxValue); - }, config: this); - - if (CurrentCorePackage == null) - { - CurrentCorePackage = ContentPackage.CorePackages.First(); - } - - TextManager.LoadTextPacks(AllEnabledPackages); - return; - } -#endif - - var contentPackagesElement = doc.Root.Element("contentpackages"); - if (contentPackagesElement != null) - { - string coreName = contentPackagesElement.Element("core")?.GetAttributeString("name", ""); - ContentPackage corePackage = ContentPackage.CorePackages.Find(p => p.Name.Equals(coreName, StringComparison.OrdinalIgnoreCase)); - if (corePackage != null) - { - CurrentCorePackage = corePackage; - } - - XElement regularElement = contentPackagesElement.Element("regular"); - - List subElements = regularElement?.Elements()?.ToList(); - if (subElements != null) - { - foreach (var subElement in subElements) - { - if (!bool.TryParse(subElement.GetAttributeString("enabled", "false"), out bool enabled) || !enabled) { continue; } - - string name = subElement.GetAttributeString("name", null); - if (string.IsNullOrEmpty(name)) { continue; } - - var package = ContentPackage.RegularPackages.Find(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (package == null) { continue; } - enabledRegularPackages.Add(package); - } - - ContentPackage.SortContentPackages(p => - { - int index = subElements.FindIndex(e => - { - string name = e.GetAttributeString("name", null); - return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase); - }); - return index; - }, config: this); - } - } - else - { - var enabledContentPackagePaths = new List(); - foreach (XElement subElement in doc.Root.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "contentpackage": - string path = subElement.GetAttributeString("path", ""); - enabledContentPackagePaths.Add(path.CleanUpPath().ToLowerInvariant()); - break; - } - } - - foreach (string path in enabledContentPackagePaths) - { - ContentPackage package = ContentPackage.AllPackages - .FirstOrDefault(p => p.Path.CleanUpPath().Equals(path, StringComparison.OrdinalIgnoreCase)); - if (package == null) { continue; } - if (package.IsCorePackage) { CurrentCorePackage = package; } - else { enabledRegularPackages.Add(package); } - } - - ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()), config: this); - } - - if (CurrentCorePackage == null) - { - CurrentCorePackage = ContentPackage.CorePackages.First(); - } - - TextManager.LoadTextPacks(AllEnabledPackages); - } -#endregion - - public void ResetToDefault() - { - LoadDefaultConfig(); -#if CLIENT - CheckBindings(true); -#endif - SaveNewPlayerConfig(); - } - - private void SetDefaultValues(bool resetLanguage = true) - { - GraphicsWidth = 0; - GraphicsHeight = 0; - VSyncEnabled = true; - TextureCompressionEnabled = true; - Timing.FrameLimit = 200; -#if DEBUG - EnableSplashScreen = false; -#else - EnableSplashScreen = true; -#endif - ParticleLimit = 1500; - LightMapScale = 0.5f; - ChromaticAberrationEnabled = true; - PauseOnFocusLost = true; - MuteOnFocusLost = false; - UseDirectionalVoiceChat = true; - VoiceSetting = VoiceMode.Disabled; - VoiceCaptureDevice = null; - NoiseGateThreshold = -45; - UseLocalVoiceByDefault = false; - windowMode = WindowMode.BorderlessWindowed; - losMode = LosMode.Transparent; - UseSteamMatchmaking = true; - RequireSteamAuthentication = true; - QuickStartSubmarineName = string.Empty; - PlayerCharacterCustomization = null; - aimAssistAmount = 0.5f; - EnableMouseLook = true; - EnableRadialDistortion = true; - CrewMenuOpen = true; - ChatOpen = true; - soundVolume = 0.5f; - musicVolume = 0.3f; - DynamicRangeCompressionEnabled = true; - VoipAttenuationEnabled = true; - voiceChatVolume = 0.5f; - microphoneVolume = 5.0f; - AutoCheckUpdates = true; - playerName = string.Empty; - HUDScale = 1; - InventoryScale = 1; - AutoUpdateWorkshopItems = true; - CampaignDisclaimerShown = false; - CorpseDespawnDelay = 10 * 60; - CorpsesPerSubDespawnThreshold = 5; - if (resetLanguage) - { - Language = "English"; - } - MasterServerUrl = "http://www.undertowgames.com/baromaster"; - VerboseLogging = false; - SaveDebugConsoleLogs = false; - AutoUpdateWorkshopItems = true; - TextScale = 1; - textScaleDirty = false; - DisableInGameHints = false; - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index bdabc58e7..f4818dc48 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -27,16 +27,28 @@ namespace Barotrauma protected bool[] IsEquipped; + /// + /// Can the inventory be accessed when the character is still alive + /// public bool AccessibleWhenAlive { get; private set; } + /// + /// Can the inventory be accessed by the character itself when the character is still alive (only has an effect if AccessibleWhenAlive false) + /// + public bool AccessibleByOwner + { + get; + private set; + } + private static string[] ParseSlotTypes(XElement element) { string slotString = element.GetAttributeString("slots", null); - return slotString == null ? new string[0] : slotString.Split(','); + return slotString == null ? Array.Empty() : slotString.Split(','); } public CharacterInventory(XElement element, Character character) @@ -47,6 +59,7 @@ namespace Barotrauma SlotTypes = new InvSlotType[capacity]; AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true); + AccessibleByOwner = element.GetAttributeBool("accessiblebyowner", AccessibleWhenAlive); string[] slotTypeNames = ParseSlotTypes(element); System.Diagnostics.Debug.Assert(slotTypeNames.Length == capacity); @@ -76,7 +89,7 @@ namespace Barotrauma if (GameMain.Client != null) { return; } #endif - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -89,7 +102,15 @@ namespace Barotrauma string slotString = subElement.GetAttributeString("slot", "None"); InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; - Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot); + Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot, onSpawned: (Item item) => + { + if (item != null && item.ParentInventory != this) + { + string errorMsg = $"Failed to spawn the initial item \"{item.Prefab.Identifier}\" in the inventory of \"{character.SpeciesName}\"."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + }); } } @@ -191,7 +212,7 @@ namespace Barotrauma if (TryPutItem(itemInSameSlot, limbSlot, allowSwapping: false, allowCombine: false, character)) { #if CLIENT - visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.412f); + visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.412f); #endif } break; @@ -348,7 +369,7 @@ namespace Barotrauma #if CLIENT for (int j = 0; j < capacity; j++) { - if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 9ec10aff0..e34ef8f1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -50,39 +50,39 @@ namespace Barotrauma.Items.Components public int DockingDir { get; set; } - [Serialize("32.0,32.0", false, description: "How close the docking port has to be to another port to dock.")] + [Serialize("32.0,32.0", IsPropertySaveable.No, description: "How close the docking port has to be to another port to dock.")] public Vector2 DistanceTolerance { get; set; } - [Serialize(32.0f, false, description: "How close together the docking ports are forced when docked.")] + [Serialize(32.0f, IsPropertySaveable.No, description: "How close together the docking ports are forced when docked.")] public float DockedDistance { get; set; } - [Serialize(true, false, description: "Is the port horizontal.")] + [Serialize(true, IsPropertySaveable.No, description: "Is the port horizontal.")] public bool IsHorizontal { get; set; } - [Editable, Serialize(false, true, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] public bool MainDockingPort { get; set; } - [Serialize(true, false, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + [Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] public bool ApplyEffectsOnDocking { get; set; } - [Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ - "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] + [Editable, Serialize(DirectionType.None, IsPropertySaveable.No, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ + "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] public DirectionType ForceDockingDirection { get; set; } public DockingPort DockingTarget { get; private set; } @@ -126,11 +126,11 @@ namespace Barotrauma.Items.Components /// public event Action OnUnDocked; - public DockingPort(Item item, XElement element) + public DockingPort(Item item, ContentXElement element) : base(item, element) { // isOpen = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { string texturePath = subElement.GetAttributeString("texture", ""); switch (subElement.Name.ToString().ToLowerInvariant()) @@ -338,33 +338,32 @@ namespace Barotrauma.Items.Components Vector2.UnitX * DockingDir : Vector2.UnitY * DockingDir; offset *= DockedDistance * 0.5f * item.Scale; - - Vector2 pos1 = item.WorldPosition + offset; + Vector2 pos1 = item.WorldPosition + offset; Vector2 pos2 = DockingTarget.item.WorldPosition - offset; if (useWeldJoint) { joint = JointFactory.CreateWeldJoint(GameMain.World, item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody, - ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true); + ConvertUnits.ToSimUnits(pos1), ConvertUnits.ToSimUnits(pos2), true); ((WeldJoint)joint).FrequencyHz = 1.0f; + joint.CollideConnected = false; } else { var distanceJoint = JointFactory.CreateDistanceJoint(GameMain.World, item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody, - ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true); + ConvertUnits.ToSimUnits(pos1), ConvertUnits.ToSimUnits(pos2), true); distanceJoint.Length = 0.01f; distanceJoint.Frequency = 1.0f; distanceJoint.DampingRatio = 0.8f; joint = distanceJoint; + joint.CollideConnected = true; } - - joint.CollideConnected = true; } public int GetDir(DockingPort dockingTarget = null) @@ -476,6 +475,10 @@ namespace Barotrauma.Items.Components wire.Connect(powerConnection, false, false); recipient.TryAddLink(wire); wire.Connect(recipient, false, false); + + //Flag connections to be updated + Powered.ChangedConnections.Add(powerConnection); + Powered.ChangedConnections.Add(recipient); } private void CreateDoorBody() @@ -545,7 +548,7 @@ namespace Barotrauma.Items.Components //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = 0; i < 2; i++) { @@ -649,7 +652,7 @@ namespace Barotrauma.Items.Components //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int upperSubBottom = int.MaxValue, lowerSubTop = int.MinValue; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { for (int i = 0; i < 2; i++) { @@ -757,7 +760,9 @@ namespace Barotrauma.Items.Components } LinkHullsToGaps(); - + + Item.UpdateHulls(); + hulls[0].ShouldBeSaved = false; hulls[1].ShouldBeSaved = false; item.linkedTo.Add(hulls[0]); @@ -907,6 +912,13 @@ namespace Barotrauma.Items.Components DockingTarget.Undock(); DockingTarget = null; + //Flag power connection + Connection powerConnection = Item.Connections.Find(c => c.IsPower); + if (powerConnection != null) + { + Powered.ChangedConnections.Add(powerConnection); + } + if (doorBody != null) { GameMain.World.Remove(doorBody); @@ -1048,8 +1060,8 @@ namespace Barotrauma.Items.Components if (initialized) { return; } initialized = true; - float maxXDist = (item.Prefab.sprite.size.X * item.Prefab.Scale) / 2; - float closestYDist = (item.Prefab.sprite.size.Y * item.Prefab.Scale) / 2; + float maxXDist = (item.Prefab.Sprite.size.X * item.Prefab.Scale) / 2; + float closestYDist = (item.Prefab.Sprite.size.Y * item.Prefab.Scale) / 2; foreach (Item it in Item.ItemList) { if (it.Submarine != item.Submarine) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 1689b9909..98dc5114d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Items.Components public bool CanBeWelded = true; private float stuck; - [Serialize(0.0f, false, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")] public float Stuck { get { return stuck; } @@ -115,13 +115,13 @@ namespace Barotrauma.Items.Components } } - [Serialize(3.0f, true, description: "How quickly the door opens."), Editable] + [Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door opens."), Editable] public float OpeningSpeed { get; private set; } - [Serialize(3.0f, true, description: "How quickly the door closes."), Editable] + [Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door closes."), Editable] public float ClosingSpeed { get; private set; } - [Serialize(1.0f, true, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable] public float ToggleCoolDown { get; private set; } public bool? PredictedState { get; private set; } @@ -155,10 +155,10 @@ namespace Barotrauma.Items.Components public bool IsHorizontal { get; private set; } - [Serialize("0.0,0.0,0.0,0.0", false, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")] + [Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")] public Rectangle Window { get; set; } - [Editable, Serialize(false, true, description: "Is the door currently open.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the door currently open.")] public bool IsOpen { get { return isOpen; } @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] + [Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")] public bool HasIntegratedButtons { get; private set; } public float OpenState @@ -187,39 +187,39 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")] + [Serialize(false, IsPropertySaveable.No, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")] public bool Impassable { get; set; } - [Editable, Serialize(true, true, description: "", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)] public bool UseBetweenOutpostModules { get; private set; } - [Editable, Serialize(false, false, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)] public bool BotsShouldKeepOpen { get; private set; } - public Door(Item item, XElement element) + public Door(Item item, ContentXElement element) : base(item, element) { IsHorizontal = element.GetAttributeBool("horizontal", false); canBePicked = element.GetAttributeBool("canbepicked", false); autoOrientGap = element.GetAttributeBool("autoorientgap", false); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - string texturePath = subElement.GetAttributeString("texture", ""); + string textureDir = GetTextureDirectory(subElement); switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - doorSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + doorSprite = new Sprite(subElement, path: textureDir); break; case "weldedsprite": - weldedSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + weldedSprite = new Sprite(subElement, path: textureDir); break; case "brokensprite": - brokenSprite = new Sprite(subElement, texturePath.Contains("/") ? "" : Path.GetDirectoryName(item.Prefab.FilePath)); + brokenSprite = new Sprite(subElement, path: textureDir); scaleBrokenSprite = subElement.GetAttributeBool("scale", false); fadeBrokenSprite = subElement.GetAttributeBool("fade", false); break; @@ -269,15 +269,15 @@ namespace Barotrauma.Items.Components #endif } - private readonly string accessDeniedTxt = TextManager.Get("AccessDenied"); - private readonly string cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); - public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + private readonly LocalizedString accessDeniedTxt = TextManager.Get("AccessDenied"); + private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar"; ParseMsg(); if (addMessage) { - msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText); + msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value; } return isBroken || base.HasRequiredItems(character, addMessage, msg); } @@ -337,7 +337,7 @@ namespace Barotrauma.Items.Components #if CLIENT else if (hasRequiredItems && character != null && character == Character.Controlled) { - GUI.AddMessage(accessDeniedTxt, GUI.Style.Red); + GUI.AddMessage(accessDeniedTxt, GUIStyle.Red); } #endif return false; @@ -561,7 +561,7 @@ namespace Barotrauma.Items.Components { if (!characterPosErrorShown.Contains(c)) { - if (GameSettings.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); } + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); } GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsManager.ErrorSeverity.Error, "Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." + " Removed: " + c.Removed + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index eb7b5381c..f4826ee0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -48,28 +48,28 @@ namespace Barotrauma.Items.Components } } - [Serialize(500.0f, true, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] + [Serialize(500.0f, IsPropertySaveable.Yes, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] public float Range { get; set; } - [Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float RangeMultiplierInWalls { get; set; } - [Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] + [Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { get; set; } - [Serialize(false, true, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] + [Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { get; @@ -90,12 +90,12 @@ namespace Barotrauma.Items.Components private readonly Attack attack; - public ElectricalDischarger(Item item, XElement element) : + public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { list.Add(this); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -119,6 +119,7 @@ namespace Barotrauma.Items.Components CurrPowerConsumption = powerConsumption; Voltage = 0.0f; + charging = true; timer = Duration; IsActive = true; @@ -142,10 +143,10 @@ namespace Barotrauma.Items.Components timer -= deltaTime; if (charging) { - if (GetAvailableInstantaneousBatteryPower() >= powerConsumption) + if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption) { - var batteries = item.GetConnectedComponents(); - float neededPower = powerConsumption; + List batteries = GetConnectedBatteries(); + float neededPower = PowerConsumption; while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); @@ -170,6 +171,14 @@ namespace Barotrauma.Items.Components } } + /// + /// Discharge coil only draws power when charging + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + return charging && IsActive ? PowerConsumption : 0; + } + public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index 5ab363484..9f135fdb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -16,55 +16,55 @@ namespace Barotrauma.Items.Components Circle } - [Editable, Serialize("", true, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")] + [Editable, Serialize("", IsPropertySaveable.Yes, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")] public string? ItemIdentifier { get; set; } - [Editable, Serialize("", true, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")] + [Editable, Serialize("", IsPropertySaveable.Yes, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")] public string? SpeciesName { get; set; } - [Editable, Serialize(true, true, "Only spawn if crew members are within certain area")] + [Editable, Serialize(true, IsPropertySaveable.Yes, "Only spawn if crew members are within certain area")] public bool OnlySpawnWhenCrewInRange { get; set; } - [Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where crew members need to stay")] + [Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where crew members need to stay")] public AreaShape CrewAreaShape { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", IsPropertySaveable.Yes, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")] public Vector2 CrewAreaBounds { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, IsPropertySaveable.Yes, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")] public float CrewAreaRadius { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the crew area from the center of the item")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", IsPropertySaveable.Yes, "Offset of the crew area from the center of the item")] public Vector2 CrewAreaOffset { get; set; } - [Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where enemies or items are spawned")] + [Editable, Serialize(AreaShape.Rectangle, IsPropertySaveable.Yes, "Shape of the area where enemies or items are spawned")] public AreaShape SpawnAreaShape { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", IsPropertySaveable.Yes, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")] public Vector2 SpawnAreaBounds { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, IsPropertySaveable.Yes, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")] public float SpawnAreaRadius { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the spawn area from the center of the item")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", IsPropertySaveable.Yes, "Offset of the spawn area from the center of the item")] public Vector2 SpawnAreaOffset { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", true, "Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", IsPropertySaveable.Yes, "Time range between spawn attempts in seconds. Set both to a negative value to disable automatic spawning.")] public Vector2 SpawnTimerRange { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", IsPropertySaveable.Yes, "Minumum and maximum amount of items or creatures to spawn in one attempt")] public Vector2 SpawnAmountRange { get; set; } - [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, IsPropertySaveable.Yes, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")] public int MaximumAmount { get; set; } - [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, IsPropertySaveable.Yes, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")] public int MaximumAmountInArea { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, IsPropertySaveable.Yes, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] public float MaximumAmountRangePadding { get; set; } - [Serialize(true, true, "")] + [Serialize(true, IsPropertySaveable.Yes, "")] public bool CanSpawn { get; set; } = true; private float spawnTimer; @@ -72,7 +72,7 @@ namespace Barotrauma.Items.Components private int spawnedAmount = 0; - public EntitySpawnerComponent(Item item, XElement element) : base(item, element) + public EntitySpawnerComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; } @@ -90,7 +90,7 @@ namespace Barotrauma.Items.Components foreach (ItemPrefab prefab in ItemPrefab.Prefabs) { - if (string.Equals(trimmedString, prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + if (trimmedString == prefab.Identifier) { found = true; break; @@ -182,11 +182,11 @@ namespace Barotrauma.Items.Components int amount; if (!string.IsNullOrWhiteSpace(SpeciesName)) { - amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); + amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName == SpeciesName && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { - amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.OrdinalIgnoreCase) && IsInRange(it.WorldPosition, crewArea: false, rangePad: true)); + amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier == ItemIdentifier && IsInRange(it.WorldPosition, crewArea: false, rangePad: true)); } else { @@ -270,15 +270,15 @@ namespace Barotrauma.Items.Components { if (!string.IsNullOrWhiteSpace(SpeciesName)) { - string[] allSpecies = SpeciesName.Split(','); - string species = allSpecies.GetRandom().Trim(); - Entity.Spawner?.AddToSpawnQueue(species, pos); + Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier species = allSpecies.GetRandomUnsynced(); + Entity.Spawner?.AddCharacterToSpawnQueue(species, pos); spawnedAmount++; } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { - string[] allItems = ItemIdentifier.Split(','); - string itemIdentifier = allItems.GetRandom().Trim(); + Identifier[] allItems = ItemIdentifier.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); + Identifier itemIdentifier = allItems.GetRandomUnsynced(); ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier); if (prefab is null) { return; } @@ -287,7 +287,7 @@ namespace Barotrauma.Items.Components pos -= sub.Position; } - Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine); + Entity.Spawner?.AddItemToSpawnQueue(prefab, pos, item.Submarine); spawnedAmount++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index c8b8782ba..d8543526e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -10,27 +10,27 @@ namespace Barotrauma.Items.Components { partial class GeneticMaterial : ItemComponent, IServerSerializable { - private readonly string materialName; + private readonly LocalizedString materialName; private Character targetCharacter; private AfflictionPrefab selectedEffect, selectedTaintedEffect; - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string Effect { get; set; } - [Serialize("geneticmaterialdebuff", true)] - public string TaintedEffect + [Serialize("geneticmaterialdebuff", IsPropertySaveable.Yes)] + public Identifier TaintedEffect { get; set; } private bool tainted; - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool Tainted { get { return tainted; } @@ -39,28 +39,27 @@ namespace Barotrauma.Items.Components if (!value) { return; } tainted = true; item.AllowDeconstruct = false; - if (!string.IsNullOrEmpty(TaintedEffect)) + if (!TaintedEffect.IsEmpty) { selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a => - a.Identifier.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase) || - a.AfflictionType.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + a.Identifier == TaintedEffect || + a.AfflictionType == TaintedEffect).GetRandomUnsynced(); } } } //only for saving the selected tainted effect - [Serialize("", true)] - public string SelectedTaintedEffect + [Serialize("", IsPropertySaveable.Yes)] + public Identifier SelectedTaintedEffect { - get { return selectedTaintedEffect?.Identifier ?? string.Empty; } + get { return selectedTaintedEffect?.Identifier ?? Identifier.Empty; } private set { - if (string.IsNullOrEmpty(value)) { return; } - selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.Identifier == value); + selectedTaintedEffect = !value.IsEmpty ? AfflictionPrefab.Prefabs.Find(a => a.Identifier == value) : null; } } - public GeneticMaterial(Item item, XElement element) + public GeneticMaterial(Item item, ContentXElement element) : base(item, element) { string nameId = element.GetAttributeString("nameidentifier", ""); @@ -71,15 +70,15 @@ namespace Barotrauma.Items.Components if (!string.IsNullOrEmpty(Effect)) { selectedEffect = AfflictionPrefab.Prefabs.Where(a => - a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) || - a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + a.Identifier == Effect || + a.AfflictionType == Effect).GetRandomUnsynced(); } } - [Serialize(3.0f, false)] + [Serialize(3.0f, IsPropertySaveable.No)] public float ConditionIncreaseOnCombineMin { get; set; } - [Serialize(8.0f, false)] + [Serialize(8.0f, IsPropertySaveable.No)] public float ConditionIncreaseOnCombineMax { get; set; } public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial) @@ -230,16 +229,16 @@ namespace Barotrauma.Items.Components #endif } - public static string TryCreateName(ItemPrefab prefab, XElement element) + public static LocalizedString TryCreateName(ItemPrefab prefab, XElement element) { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().Equals(nameof(GeneticMaterial), StringComparison.OrdinalIgnoreCase)) + if (subElement.NameAsIdentifier() == nameof(GeneticMaterial)) { - string nameId = subElement.GetAttributeString("nameidentifier", ""); - if (!string.IsNullOrEmpty(nameId)) + Identifier nameId = subElement.GetAttributeIdentifier("nameidentifier", ""); + if (!nameId.IsEmpty) { - return prefab.Name.Replace("[type]", TextManager.Get(nameId, returnNull: true) ?? nameId); + return prefab.Name.Replace("[type]", TextManager.Get(nameId).Fallback(nameId.Value)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index a9bd794cf..5cde984fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -15,25 +15,30 @@ namespace Barotrauma.Items.Components { internal class ProducedItem { - [Serialize(0f, true)] + [Serialize(0f, IsPropertySaveable.Yes)] public float Probability { get; set; } public readonly List StatusEffects = new List(); + public readonly Item Producer; + public readonly ItemPrefab? Prefab; - public ProducedItem(ItemPrefab prefab, float probability) + public ProducedItem(Item producer, ItemPrefab prefab, float probability) { + Producer = producer; Prefab = prefab; Probability = probability; } - public ProducedItem(XElement element) + public ProducedItem(Item producer, ContentXElement element) { SerializableProperty.DeserializeProperties(this, element); - string itemIdentifier = element.GetAttributeString("identifier", string.Empty); - if (!string.IsNullOrWhiteSpace(itemIdentifier)) + Producer = producer; + + Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (!itemIdentifier.IsEmpty) { Prefab = ItemPrefab.Find(null, itemIdentifier); } @@ -41,17 +46,17 @@ namespace Barotrauma.Items.Components LoadSubElements(element); } - private void LoadSubElements(XElement element) + private void LoadSubElements(ContentXElement element) { if (!element.HasElements) { return; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": { - StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name); + StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name.Value); if (effect.type != ActionType.OnProduceSpawned) { DebugConsole.ThrowError("Only OnProduceSpawned type can be used in ."); @@ -325,9 +330,14 @@ namespace Barotrauma.Items.Components } public static TileSide GetOppositeSide(this TileSide side) - { - return (TileSide) (1 << ((int) Math.Log2((int) side) + 2) % 4); - } + => side switch + { + TileSide.Left => TileSide.Right, + TileSide.Right => TileSide.Left, + TileSide.Bottom => TileSide.Top, + TileSide.Top => TileSide.Bottom, + _ => throw new ArgumentException($"Expected Left, Right, Bottom or Top, got {side}") + }; } internal partial class Growable : ItemComponent, IServerSerializable @@ -335,61 +345,61 @@ namespace Barotrauma.Items.Components // used for debugging where a vine failed to grow public readonly HashSet FailedRectangles = new HashSet(); - [Serialize(1f, true, "How fast the plant grows.")] + [Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows.")] public float GrowthSpeed { get; set; } - [Serialize(100f, true, "How long the plant can go without watering.")] + [Serialize(100f, IsPropertySaveable.Yes, "How long the plant can go without watering.")] public float MaxHealth { get; set; } - [Serialize(1f, true, "How much damage the plant takes while in water.")] + [Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while in water.")] public float FloodTolerance { get; set; } - [Serialize(1f, true, "How much damage the plant takes while growing.")] + [Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while growing.")] public float Hardiness { get; set; } - [Serialize(0.01f, true, "How often a seed is produced.")] + [Serialize(0.01f, IsPropertySaveable.Yes, "How often a seed is produced.")] public float SeedRate { get; set; } - [Serialize(0.01f, true, "How often a product item is produced.")] + [Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced.")] public float ProductRate { get; set; } - [Serialize(0.5f, true, "Probability of an attribute being randomly modified in a newly produced seed.")] + [Serialize(0.5f, IsPropertySaveable.Yes, "Probability of an attribute being randomly modified in a newly produced seed.")] public float MutationProbability { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, "Color of the flowers.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the flowers.")] public Color FlowerTint { get; set; } - [Serialize(3, true, "Number of flowers drawn when fully grown")] + [Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn when fully grown")] public int FlowerQuantity { get; set; } - [Serialize(0.25f, true, "Size of the flower sprites.")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Size of the flower sprites.")] public float BaseFlowerScale { get; set; } - [Serialize(0.5f, true, "Size of the leaf sprites.")] + [Serialize(0.5f, IsPropertySaveable.Yes, "Size of the leaf sprites.")] public float BaseLeafScale { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, "Color of the leaves.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the leaves.")] public Color LeafTint { get; set; } - [Serialize(0.33f, true, "Chance of a leaf appearing behind a branch.")] + [Serialize(0.33f, IsPropertySaveable.Yes, "Chance of a leaf appearing behind a branch.")] public float LeafProbability { get; set; } - [Serialize("1.0,1.0,1.0,1.0", true, "Color of the vines.")] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the vines.")] public Color VineTint { get; set; } - [Serialize(32, true, "Maximum number of vine tiles the plant can grow.")] + [Serialize(32, IsPropertySaveable.Yes, "Maximum number of vine tiles the plant can grow.")] public int MaximumVines { get; set; } - [Serialize(0.25f, true, "Size of the vine sprites.")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Size of the vine sprites.")] public float VineScale { get; set; } - [Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")] + [Serialize("0.26,0.27,0.29,1.0", IsPropertySaveable.Yes, "Tint of a dead plant.")] public Color DeadTint { get; set; } - [Serialize("1,1,1,1", true, "Probability for the plant to grow in a direction.")] + [Serialize("1,1,1,1", IsPropertySaveable.Yes, "Probability for the plant to grow in a direction.")] public Vector4 GrowthWeights { get; set; } - [Serialize(0.0f, true, "How much damage is taken from fires.")] + [Serialize(0.0f, IsPropertySaveable.Yes, "How much damage is taken from fires.")] public float FireVulnerability { get; set; } private const float increasedDeathSpeed = 10f; @@ -422,7 +432,7 @@ namespace Barotrauma.Items.Components private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f; private const int VineChunkSize = 32; - public Growable(Item item, XElement element) : base(item, element) + public Growable(Item item, ContentXElement element) : base(item, element) { SerializableProperty.DeserializeProperties(this, element); @@ -430,12 +440,12 @@ namespace Barotrauma.Items.Components if (element.HasElements) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "produceditem": - ProducedItems.Add(new ProducedItem(subElement)); + ProducedItems.Add(new ProducedItem(this.item, subElement)); break; case "vinesprites": LoadVines(subElement); @@ -444,7 +454,7 @@ namespace Barotrauma.Items.Components } } - ProducedSeed = new ProducedItem(this.item.Prefab, 1.0f); + ProducedSeed = new ProducedItem(this.item, this.item.Prefab, 1.0f); flowerTiles = new int[FlowerQuantity]; } @@ -471,7 +481,7 @@ namespace Barotrauma.Items.Components } } - partial void LoadVines(XElement element); + partial void LoadVines(ContentXElement element); public void OnGrowthTick(Planter planter, PlantSlot slot) { @@ -541,7 +551,7 @@ namespace Barotrauma.Items.Components if (spawnProduct || spawnSeed) { - VineTile vine = Vines.GetRandom(); + VineTile vine = Vines.GetRandomUnsynced(); spawnPos = vine.GetWorldPosition(planter, slot.Offset); } else @@ -564,9 +574,9 @@ namespace Barotrauma.Items.Components { if (producedItem.Prefab == null) { return; } - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningProduce:" + thisItem.prefab.Identifier + ":" + producedItem.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningProduce:" + thisItem.Prefab.Identifier + ":" + producedItem.Prefab.Identifier); - Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => + Entity.Spawner?.AddItemToSpawnQueue(producedItem.Prefab, pos, onSpawned: it => { foreach (StatusEffect effect in producedItem.StatusEffects) { @@ -590,7 +600,7 @@ namespace Barotrauma.Items.Components { if (!Decayed) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningDied:" + item.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningDied:" + item.Prefab.Identifier); } Decayed = true; @@ -881,14 +891,14 @@ namespace Barotrauma.Items.Components return element; } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); - flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]); + flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty())!; Decayed = componentElement.GetAttributeBool("decayed", false); Vines.Clear(); - foreach (XElement element in componentElement.Elements()) + foreach (var element in componentElement.Elements()) { if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 7d623bc39..b8caed282 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private readonly Vector2[] scaledHandlePos; private readonly InputType prevPickKey; - private string prevMsg; + private LocalizedString prevMsg; private Dictionary> prevRequiredItems; //the distance from the holding characters elbow to center of the physics body of the item @@ -39,7 +39,7 @@ namespace Barotrauma.Items.Components get; private set; } - [Serialize(true, true, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")] + [Serialize(true, IsPropertySaveable.Yes, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")] public bool CanPush { get; @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components get { return item.body ?? body; } } - [Serialize(false, true, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")] public bool Attached { get { return attached && item.ParentInventory == null; } @@ -65,56 +65,56 @@ namespace Barotrauma.Items.Components } } - [Serialize(true, true, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")] + [Serialize(true, IsPropertySaveable.Yes, description: "Can the item be pointed to a specific direction or do the characters always hold it in a static pose.")] public bool Aimable { get; set; } - [Serialize(false, false, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")] public bool ControlPose { get; set; } - [Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")] + [Serialize(false, IsPropertySaveable.No, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")] public bool UseHandRotationForHoldAngle { get; set; } - [Serialize(false, false, description: "Can the item be attached to walls.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be attached to walls.")] public bool Attachable { get { return attachable; } set { attachable = value; } } - [Serialize(true, false, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item be reattached to walls after it has been deattached (only valid if Attachable is set to true).")] public bool Reattachable { get; set; } - [Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")] public bool LimitedAttachable { get; set; } - [Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] public bool AttachedByDefault { get { return attachedByDefault; } set { attachedByDefault = value; } } - [Editable, Serialize("0.0,0.0", false, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")] public Vector2 HoldPos { @@ -122,7 +122,7 @@ namespace Barotrauma.Items.Components set { holdPos = ConvertUnits.ToSimUnits(value); } } - [Serialize("0.0,0.0", false, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+ " Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")] public Vector2 AimPos { @@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } - [Editable, Serialize(0.0f, false, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] public float HoldAngle { get { return MathHelper.ToDegrees(holdAngle); } @@ -138,24 +138,31 @@ namespace Barotrauma.Items.Components } private Vector2 swingAmount; - [Editable, Serialize("0.0,0.0", false, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] public Vector2 SwingAmount { get { return ConvertUnits.ToDisplayUnits(swingAmount); } set { swingAmount = ConvertUnits.ToSimUnits(value); } } - [Editable, Serialize(0.0f, false, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] public float SwingSpeed { get; set; } - [Editable, Serialize(false, false, description: "Should the item swing around when it's being held.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")] public bool SwingWhenHolding { get; set; } - [Editable, Serialize(false, false, description: "Should the item swing around when it's being aimed.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")] public bool SwingWhenAiming { get; set; } - [Editable, Serialize(false, false, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] + [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; } - - public Holdable(Item item, XElement element) + + [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.85f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is attached to a wall.")] + public float SpriteDepthWhenAttached + { + get; + set; + } + + public Holdable(Item item, ContentXElement element) : base(item, element) { body = item.body; @@ -238,7 +245,7 @@ namespace Barotrauma.Items.Components } private bool loadedFromXml; - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); @@ -523,6 +530,11 @@ namespace Barotrauma.Items.Components { if (!attachable) { return; } + if (body == null) + { + throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier})."); + } + //outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub if (item.CurrentHull == null && item.Submarine == null) { @@ -570,6 +582,9 @@ namespace Barotrauma.Items.Components requiredItems = new Dictionary>(prevRequiredItems); Attached = true; +#if CLIENT + item.DrawDepthOffset = SpriteDepthWhenAttached - item.SpriteDepth; +#endif } public void DeattachFromWall() @@ -578,7 +593,9 @@ namespace Barotrauma.Items.Components Attached = false; attachTargetCell = null; - +#if CLIENT + item.DrawDepthOffset = 0.0f; +#endif //make the item pickable with the default pick key and with no specific tools/items when it's deattached requiredItems.Clear(); DisplayMsg = ""; @@ -615,7 +632,7 @@ namespace Barotrauma.Items.Components int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); int currentlyAttachedCount = Item.ItemList.Count( - i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); + i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier); if (maxAttachableCount == 0) { #if CLIENT @@ -623,7 +640,7 @@ namespace Barotrauma.Items.Components #endif return false; } - else if (currentlyAttachedCount >= maxAttachableCount) + else if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index 8fcf6d265..68683091a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -1,64 +1,113 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Collections.Immutable; namespace Barotrauma.Items.Components { partial class IdCard : Pickable { - [Serialize(CharacterTeamType.None, true, alwaysUseInstanceValues: true)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public CharacterTeamType TeamID { get; set; } - [Serialize(0, true, alwaysUseInstanceValues: true)] + [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int SubmarineSpecificID { get; set; } + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string OwnerTags + { + get => string.Join(',', OwnerTagSet); + set => OwnerTagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet(); + } + + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string Description + { + get; + set; + } + private JobPrefab cachedJobPrefab; private string cachedName; - public IdCard(Item item, XElement element) : base(item, element) - { + public ImmutableHashSet OwnerTagSet { get; set; } - } + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public string OwnerName { get; set; } + + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Identifier OwnerJobId { get; set; } - public void Initialize(CharacterInfo info) + public JobPrefab OwnerJob => JobPrefab.Prefabs.TryGet(OwnerJobId, out var prefab) ? prefab : null; + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerHairIndex { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerBeardIndex { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerMoustacheIndex { get; set; } + + [Serialize(-1, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public int OwnerFaceAttachmentIndex { get; set; } + + [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Color OwnerHairColor { get; set; } + + [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Color OwnerFacialHairColor { get; set; } + + [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Color OwnerSkinColor { get; set; } + + #warning TODO: figure out how to set Vector2.Zero as the default here + [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] + public Vector2 OwnerSheetIndex { get; set; } + + public IdCard(Item item, ContentXElement element) : base(item, element) { } + + public void Initialize(WayPoint spawnPoint, Character character) { + item.AddTag("name:" + character.Name); + + CharacterInfo info = character.Info; if (info == null) { return; } - if (info.Job?.Prefab != null) + if (spawnPoint != null) { - item.AddTag("jobid:" + info.Job.Prefab.Identifier); + foreach (string s in spawnPoint.IdCardTags) + { + item.AddTag(s); + } + if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) + { + item.Description = Description = spawnPoint.IdCardDesc; + } } TeamID = info.TeamID; var head = info.Head; - if (head == null) { return; } - - if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); } - if (info.HasRaces) { item.AddTag($"race:{head.race}"); } - item.AddTag($"headspriteid:{info.HeadSpriteId}"); - item.AddTag($"hairindex:{head.HairIndex}"); - item.AddTag($"beardindex:{head.BeardIndex}"); - item.AddTag($"moustacheindex:{head.MoustacheIndex}"); - item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}"); - item.AddTag($"haircolor:{head.HairColor.ToStringHex()}"); - item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}"); - item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}"); - if (head.SheetIndex != null) - { - item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}"); - } + OwnerName = info.Name; + OwnerJobId = info.Job?.Prefab.Identifier ?? Identifier.Empty; + OwnerTagSet = info.Head.Preset.TagSet; + OwnerHairIndex = head.HairIndex; + OwnerBeardIndex = head.BeardIndex; + OwnerMoustacheIndex = head.MoustacheIndex; + OwnerFaceAttachmentIndex = head.FaceAttachmentIndex; + OwnerHairColor = head.HairColor; + OwnerFacialHairColor = head.FacialHairColor; + OwnerSkinColor = head.SkinColor; + OwnerSheetIndex = head.SheetIndex; } public override void Equip(Character character) @@ -72,48 +121,12 @@ namespace Barotrauma.Items.Components base.Unequip(character); character.Info?.CheckDisguiseStatus(true, this); } - - public JobPrefab GetJob() + public override void OnItemLoaded() { - if (cachedJobPrefab != null) + if (!string.IsNullOrEmpty(Description)) { - return cachedJobPrefab; + item.Description = Description; } - - foreach (string tag in item.GetTags()) - { - if (tag.StartsWith("jobid:")) - { - string jobIdentifier = tag.Split(':').Last(); - if (JobPrefab.Get(jobIdentifier) is { } jobPrefab) - { - cachedJobPrefab = jobPrefab; - return jobPrefab; - } - } - } - - return null; - } - - public string GetName() - { - if (cachedName != null) - { - return cachedName; - } - - foreach (string tag in item.GetTags()) - { - if (tag.StartsWith("name:")) - { - string ownerName = tag.Split(':').Last(); - cachedName = ownerName; - return ownerName; - } - } - - return null; } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index 319c60ce4..0dee7a4a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -15,14 +15,14 @@ namespace Barotrauma.Items.Components private float deattachTimer; - [Serialize(1.0f, false, description: "How long it takes to deattach the item from the level walls (in seconds).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes to deattach the item from the level walls (in seconds).")] public float DeattachDuration { get; set; } - [Serialize(0.0f, false, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How far along the item is to being deattached. When the timer goes above DeattachDuration, the item is deattached.")] public float DeattachTimer { get { return deattachTimer; } @@ -53,7 +53,7 @@ namespace Barotrauma.Items.Components { if (holdable.Attached) { - GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + item.Prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier); holdable.DeattachFromWall(); } trigger.Enabled = false; @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(1.0f, false, description: "How much the position of the item can vary from the wall the item spawns on.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How much the position of the item can vary from the wall the item spawns on.")] public float RandomOffsetFromWall { get; @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components get { return holdable != null && holdable.Attached; } } - public LevelResource(Item item, XElement element) : base(item, element) + public LevelResource(Item item, ContentXElement element) : base(item, element) { IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index ad88b23d0..95152d686 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -29,34 +29,34 @@ namespace Barotrauma.Items.Components public Character User { get; private set; } - [Serialize(0.0f, false, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")] public float Range { get { return ConvertUnits.ToDisplayUnits(range); } set { range = ConvertUnits.ToSimUnits(value); } } - [Serialize(0.5f, false, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")] + [Serialize(0.5f, IsPropertySaveable.No, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")] public float Reload { get { return reload; } set { reload = Math.Max(0.0f, value); } } - [Serialize(false, false, description: "Can the weapon hit multiple targets per swing.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the weapon hit multiple targets per swing.")] public bool AllowHitMultiple { get; set; } - [Editable, Serialize(true, false)] + [Editable, Serialize(true, IsPropertySaveable.No)] public bool Swing { get; set; } - [Editable, Serialize("2.0, 0.0", false)] + [Editable, Serialize("2.0, 0.0", IsPropertySaveable.No)] public Vector2 SwingPos { get; set; } - [Editable, Serialize("3.0, -1.0", false)] + [Editable, Serialize("3.0, -1.0", IsPropertySaveable.No)] public Vector2 SwingForce { get; set; } public bool Hitting { get { return hitting; } } @@ -64,12 +64,12 @@ namespace Barotrauma.Items.Components /// /// Defines items that boost the weapon functionality, like battery cell for stun batons. /// - public readonly string[] PreferredContainedItems; + public readonly Identifier[] PreferredContainedItems; - public MeleeWeapon(Item item, XElement element) + public MeleeWeapon(Item item, ContentXElement element) : base(item, element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item) @@ -79,7 +79,7 @@ namespace Barotrauma.Items.Components } item.IsShootable = true; item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); - PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true); + PreferredContainedItems = element.GetAttributeIdentifierArray("preferredcontaineditems", Array.Empty()); } public override void Equip(Character character) @@ -461,7 +461,7 @@ namespace Barotrauma.Items.Components if (DeleteOnUse) { - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 93001af68..432ae93bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -36,8 +36,8 @@ namespace Barotrauma.Items.Components return picker; } } - - public Pickable(Item item, XElement element) + + public Pickable(Item item, ContentXElement element) : base(item, element) { allowedSlots = new List(); @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components this, item.WorldPosition, pickTimer / requiredTime, - GUI.Style.Red, GUI.Style.Green, + GUIStyle.Red, GUIStyle.Green, !string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching"); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 39e61fcb0..971068a95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -17,15 +17,15 @@ namespace Barotrauma.Items.Components private float useState; - [Serialize(UseEnvironment.Both, false, description: "Can the item be used in air, underwater or both.")] + [Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be used in air, underwater or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(0.0f, false, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.No, description: "The force to apply to the user's body."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] public float Force { get; set; } #if CLIENT private string particles; - [Serialize("", false, description: "The name of the particle prefab the item emits when used.")] + [Serialize("", IsPropertySaveable.No, description: "The name of the particle prefab the item emits when used.")] public string Particles { get { return particles; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components } #endif - public Propulsion(Item item, XElement element) + public Propulsion(Item item, ContentXElement element) : base(item,element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index e3ff2e8be..339d218aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -18,49 +18,49 @@ namespace Barotrauma.Items.Components private Vector2 barrelPos; - [Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")] public string BarrelPos { get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); } set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); } } - [Serialize(1.0f, false, description: "How long the user has to wait before they can fire the weapon again (in seconds).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How long the user has to wait before they can fire the weapon again (in seconds).")] public float Reload { get { return reload; } set { reload = Math.Max(value, 0.0f); } } - [Serialize(false, false, description: "Tells the AI to hold the trigger down when it uses this weapon")] + [Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")] public bool HoldTrigger { get; set; } - [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How projectiles the weapon launches when fired once.")] public int ProjectileCount { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")] public float Spread { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")] public float UnskilledSpread { get; set; } - [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { get; @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components private float currentChargeTime; private bool tryingToCharge; - public RangedWeapon(Item item, XElement element) + public RangedWeapon(Item item, ContentXElement element) : base(item, element) { item.IsShootable = true; @@ -102,7 +102,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Equip(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index e172945b6..9477d5a76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components Air, Water, Both, None }; - private readonly List fixableEntities; + private readonly HashSet fixableEntities; private Vector2 pickedPosition; private float activeTimer; @@ -25,88 +25,88 @@ namespace Barotrauma.Items.Components private readonly List ignoredBodies = new List(); - [Serialize("Both", false, description: "Can the item be used in air, water or both.")] + [Serialize("Both", IsPropertySaveable.No, description: "Can the item be used in air, water or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(0.0f, false, description: "The distance at which the item can repair targets.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The distance at which the item can repair targets.")] public float Range { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with sufficient skills to use the tool (in degrees).")] public float Spread { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle when used by a character with insufficient skills to use the tool (in degrees).")] public float UnskilledSpread { get; set; } - [Serialize(0.0f, false, description: "How many units of damage the item removes from structures per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from structures per second.")] public float StructureFixAmount { get; set; } - [Serialize(0.0f, false, description: "How much damage is applied to ballast flora.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much damage is applied to ballast flora.")] public float FireDamage { get; set; } - [Serialize(0.0f, false, description: "How many units of damage the item removes from destructible level walls per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How many units of damage the item removes from destructible level walls per second.")] public float LevelWallFixAmount { get; set; } - [Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much the item decreases the size of fires per second.")] public float ExtinguishAmount { get; set; } - [Serialize(0.0f, false, description: "How much water the item provides to planters per second.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much water the item provides to planters per second.")] public float WaterAmount { get; set; } - [Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels).")] public Vector2 BarrelPos { get; set; } - [Serialize(false, false, description: "Can the item repair things through walls.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through walls.")] public bool RepairThroughWalls { get; set; } - [Serialize(false, false, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")] public bool RepairMultiple { get; set; } - [Serialize(false, false, description: "Can the item repair things through holes in walls.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } - [Serialize(100.0f, false, description: "How far two walls need to not be considered overlapping and to stop the ray.")] + [Serialize(100.0f, IsPropertySaveable.No, description: "How far two walls need to not be considered overlapping and to stop the ray.")] public float MaxOverlappingWallDist { get; set; } - [Serialize(true, false, description: "Can the item hit broken doors.")] + [Serialize(true, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitItems { get; set; } - [Serialize(false, false, description: "Can the item hit broken doors.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item hit broken doors.")] public bool HitBrokenDoors { get; set; } - [Serialize(0.0f, false, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")] public float FireProbability { get; set; } - [Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Force applied to the entity the ray hits.")] public float TargetForce { get; set; } - [Serialize(0.0f, false, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })] + [Serialize(0.0f, IsPropertySaveable.No, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })] public float BarrelRotation { get; set; @@ -124,7 +124,7 @@ namespace Barotrauma.Items.Components } } - public RepairTool(Item item, XElement element) + public RepairTool(Item item, ContentXElement element) : base(item, element) { this.item = item; @@ -134,8 +134,8 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute."); } - fixableEntities = new List(); - foreach (XElement subElement in element.Elements()) + fixableEntities = new HashSet(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -143,11 +143,11 @@ namespace Barotrauma.Items.Components if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities."); - fixableEntities.Add(subElement.Attribute("name").Value); + fixableEntities.Add(subElement.Attribute("name").Value.ToIdentifier()); } else { - fixableEntities.Add(subElement.GetAttributeString("identifier", "")); + fixableEntities.Add(subElement.GetAttributeIdentifier("identifier", "")); } break; } @@ -157,7 +157,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -489,7 +489,7 @@ namespace Barotrauma.Items.Components #if CLIENT float barOffset = 10f * GUI.Scale; Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero; - user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering"); + user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering"); #endif } } @@ -625,7 +625,7 @@ namespace Barotrauma.Items.Components this, targetItem.WorldPosition, levelResource.DeattachTimer / levelResource.DeattachDuration, - GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching"); + GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); #endif FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; @@ -817,12 +817,12 @@ namespace Barotrauma.Items.Components if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) - { - character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leaksfixed", 10.0f); + { + character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leaksfixed".ToIdentifier(), 10.0f); } else { - character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leakfixed", 10.0f); + character.Speak(TextManager.GetWithVariable("DialogLeakFixed", "[roomname]", leak.FlowTargetHull.DisplayName, FormatCapitals.Yes).Value, null, 0.0f, "leakfixed".ToIdentifier(), 10.0f); } } @@ -882,7 +882,7 @@ namespace Barotrauma.Items.Components if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } for (int i = 0; i < effect.propertyNames.Length; i++) { - string propertyName = effect.propertyNames[i]; + Identifier propertyName = effect.propertyNames[i]; if (propertyName != "stuck") { continue; } if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } object value = property.GetValue(target); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs index 32a800d50..cbf773a34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Sprayer.cs @@ -6,30 +6,30 @@ namespace Barotrauma.Items.Components { partial class Sprayer : RangedWeapon { - [Serialize(0.0f, false, description: "The distance at which the item can spray walls.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The distance at which the item can spray walls.")] public float Range { get; set; } - [Serialize(1.0f, false, description: "How fast the item changes the color of the walls.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the item changes the color of the walls.")] public float SprayStrength { get; set; } - private readonly Dictionary liquidColors; + private readonly Dictionary liquidColors; private ItemContainer liquidContainer; - public Sprayer(Item item, XElement element) : base(item, element) + public Sprayer(Item item, ContentXElement element) : base(item, element) { item.IsShootable = true; item.RequireAimToUse = true; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "paintcolors": { - liquidColors = new Dictionary(); + liquidColors = new Dictionary(); foreach (XElement paintElement in subElement.Elements()) { - string paintName = paintElement.GetAttributeString("paintitem", string.Empty); + Identifier paintName = paintElement.GetAttributeIdentifier("paintitem", Identifier.Empty); Color paintColor = paintElement.GetAttributeColor("color", Color.Transparent); if (paintName != string.Empty) @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components liquidContainer = item.GetComponent(); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); #if SERVER public override bool Use(float deltaTime, Character character = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 66b0e88f0..3e9121f91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -21,10 +21,10 @@ namespace Barotrauma.Items.Components private set; } - [Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")] public float ThrowForce { get; set; } - public Throwable(Item item, XElement element) + public Throwable(Item item, ContentXElement element) : base(item, element) { //throwForce = ToolBox.GetAttributeFloat(element, "throwforce", 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index e7ac5e34e..afe64e268 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.IO; #if CLIENT using Microsoft.Xna.Framework.Graphics; using Barotrauma.Sounds; @@ -64,26 +65,26 @@ namespace Barotrauma.Items.Components } } - public readonly XElement originalElement; + public readonly ContentXElement originalElement; protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; - [Editable, Serialize(0.0f, false, description: "How long it takes to pick up the item (in seconds).")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime { get; set; } - [Serialize("", false, description: "What to display on the progress bar when this item is being picked.")] + [Serialize("", IsPropertySaveable.No, description: "What to display on the progress bar when this item is being picked.")] public string PickingMsg { get; set; } - public Dictionary SerializableProperties { get; protected set; } + public Dictionary SerializableProperties { get; protected set; } public Action OnActiveStateChanged; @@ -135,42 +136,42 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, false, description: "Can the item be picked up (or interacted with, if the pick action does something else than picking up the item).")] //Editable for doors to do their magic + [Editable, Serialize(false, IsPropertySaveable.No, description: "Can the item be picked up (or interacted with, if the pick action does something else than picking up the item).")] //Editable for doors to do their magic public bool CanBePicked { get { return canBePicked; } set { canBePicked = value; } } - [Serialize(false, false, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")] public bool DrawHudWhenEquipped { get; protected set; } - [Serialize(false, false, description: "Can the item be selected by interacting with it.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")] public bool CanBeSelected { get { return canBeSelected; } set { canBeSelected = value; } } - [Serialize(false, false, description: "Can the item be combined with other items of the same type.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item be combined with other items of the same type.")] public bool CanBeCombined { get { return canBeCombined; } set { canBeCombined = value; } } - [Serialize(false, false, description: "Should the item be removed if combining it with an other item causes the condition of this item to drop to 0.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be removed if combining it with an other item causes the condition of this item to drop to 0.")] public bool RemoveOnCombined { get { return removeOnCombined; } set { removeOnCombined = value; } } - [Serialize(false, false, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")] public bool CharacterUsable { get { return characterUsable; } @@ -178,7 +179,7 @@ namespace Barotrauma.Items.Components } //Remove item if combination results in 0 condition - [Serialize(true, false, description: "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties)."), Editable()] + [Serialize(true, IsPropertySaveable.No, description: "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties)."), Editable()] public bool AllowInGameEditing { get; @@ -197,7 +198,7 @@ namespace Barotrauma.Items.Components protected set; } - [Serialize(false, false, description: "Should the item be deleted when it's used.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it's used.")] public bool DeleteOnUse { get; @@ -214,14 +215,14 @@ namespace Barotrauma.Items.Components get { return name; } } - [Editable, Serialize("", true, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's highlighted (generally instructs how to interact with the item, e.g. \"[Mouse1] Pick up\").")] + [Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's highlighted (generally instructs how to interact with the item, e.g. \"[Mouse1] Pick up\").")] public string Msg { get; set; } - public string DisplayMsg + public LocalizedString DisplayMsg { get; set; @@ -232,16 +233,16 @@ namespace Barotrauma.Items.Components /// /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). /// - [Serialize(0f, false, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] + [Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] public float CombatPriority { get; private set; } /// /// Which sound should be played when manual sound selection type is selected? Not [Editable] because we don't want this visible in the editor for every component. /// - [Serialize(0, true, alwaysUseInstanceValues: true)] + [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } - public ItemComponent(Item item, XElement element) + public ItemComponent(Item item, ContentXElement element) { this.item = item; originalElement = element; @@ -321,7 +322,7 @@ namespace Barotrauma.Items.Components } } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -344,11 +345,11 @@ namespace Barotrauma.Items.Components case "requiredskills": if (subElement.Attribute("name") != null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); continue; } - string skillIdentifier = subElement.GetAttributeString("identifier", ""); + Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", ""); requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); break; case "statuseffect": @@ -357,7 +358,7 @@ namespace Barotrauma.Items.Components break; default: if (LoadElemProjSpecific(subElement)) { break; } - ItemComponent ic = Load(subElement, item, item.ConfigFile, false); + ItemComponent ic = Load(subElement, item, false); if (ic == null) { break; } ic.Parent = this; @@ -369,7 +370,7 @@ namespace Barotrauma.Items.Components } } - void LoadStatusEffect(XElement subElement) + void LoadStatusEffect(ContentXElement subElement) { var statusEffect = StatusEffect.Load(subElement, item.Name); if (!statusEffectLists.TryGetValue(statusEffect.type, out List effectList)) @@ -386,7 +387,7 @@ namespace Barotrauma.Items.Components IsActive = isActive; } - public void SetRequiredItems(XElement element) + public void SetRequiredItems(ContentXElement element) { bool returnEmpty = false; #if CLIENT @@ -410,7 +411,7 @@ namespace Barotrauma.Items.Components } else { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); } } @@ -517,7 +518,7 @@ namespace Barotrauma.Items.Components } item.ParentInventory.RemoveItem(item); } - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } else { @@ -533,7 +534,7 @@ namespace Barotrauma.Items.Components } this.Item.ParentInventory.RemoveItem(this.Item); } - Entity.Spawner.AddToRemoveQueue(this.Item); + Entity.Spawner.AddItemToRemoveQueue(this.Item); } else { @@ -609,6 +610,9 @@ namespace Barotrauma.Items.Components protected virtual void RemoveComponentSpecific() { } + + protected string GetTextureDirectory(ContentXElement subElement) + => subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty; public bool HasRequiredSkills(Character character) { @@ -676,7 +680,7 @@ namespace Barotrauma.Items.Components HasRequiredContainedItems(user, addMessage: false) && (!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); - public bool HasRequiredContainedItems(Character user, bool addMessage, string msg = null) + public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null) { if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; } if (item.OwnInventory == null) { return false; } @@ -686,8 +690,8 @@ namespace Barotrauma.Items.Components if (!ri.CheckRequirements(user, item)) { #if CLIENT - msg = msg ?? ri.Msg; - if (addMessage && !string.IsNullOrEmpty(msg)) + msg ??= ri.Msg; + if (addMessage && !msg.IsNullOrEmpty()) { GUI.AddMessage(msg, Color.Red); } @@ -720,7 +724,7 @@ namespace Barotrauma.Items.Components return false; } - public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null) + public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { if (requiredItems.None()) { return true; } if (character.Inventory == null) { return false; } @@ -746,7 +750,7 @@ namespace Barotrauma.Items.Components } #if CLIENT - if (!hasRequiredItems && addMessage && !string.IsNullOrEmpty(msg)) + if (!hasRequiredItems && addMessage && !msg.IsNullOrEmpty()) { GUI.AddMessage(msg, Color.Red); } @@ -802,7 +806,7 @@ namespace Barotrauma.Items.Components } if (!hasRequiredItems) { - if (msg == null && !string.IsNullOrEmpty(relatedItem.Msg)) + if (msg == null && !relatedItem.Msg.IsNullOrEmpty()) { msg = relatedItem.Msg; } @@ -850,13 +854,13 @@ namespace Barotrauma.Items.Components #endif } - public virtual void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { if (componentElement != null) { foreach (XAttribute attribute in componentElement.Attributes()) { - if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } + if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } if (property.OverridePrefabValues || !usePrefabValues) { property.TrySetValue(this, attribute.Value); @@ -881,43 +885,48 @@ namespace Barotrauma.Items.Components public virtual void OnScaleChanged() { } - // TODO: Consider using generics, interfaces, or inheritance instead of reflection -> would be easier to debug when something changes/goes wrong. - // For example, currently we can edit the constructors but they will fail in runtime because the parameters are not changed here. - // It's also painful to find where the constructors are used, because the references exist only at runtime. - public static ItemComponent Load(XElement element, Item item, string file, bool errorMessages = true) + public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true) { - Type t; - string type = element.Name.ToString().ToLowerInvariant(); + Type type; + Identifier typeName = element.NameAsIdentifier(); try { - // Get the type of a specified class. - t = Type.GetType("Barotrauma.Items.Components." + type + "", false, true); - if (t == null) + // Get the type of a specified class. + type = ReflectionUtils.GetDerivedNonAbstract().Append(typeof(ItemComponent)).FirstOrDefault(t => t.Name == typeName); + if (type == null) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + file + ")"); + if (errorMessages) + { + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); + } return null; } } catch (Exception e) { - if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + file + ")", e); + if (errorMessages) + { + DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); + } return null; } ConstructorInfo constructor; try { - if (t != typeof(ItemComponent) && !t.IsSubclassOf(typeof(ItemComponent))) return null; - constructor = t.GetConstructor(new Type[] { typeof(Item), typeof(XElement) }); + if (type != typeof(ItemComponent) && !type.IsSubclassOf(typeof(ItemComponent))) { return null; } + constructor = type.GetConstructor(new Type[] { typeof(Item), typeof(ContentXElement) }); if (constructor == null) { - DebugConsole.ThrowError("Could not find the constructor of the component \"" + type + "\" (" + file + ")"); + DebugConsole.ThrowError( + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})"); return null; } } catch (Exception e) { - DebugConsole.ThrowError("Could not find the constructor of the component \"" + type + "\" (" + file + ")", e); + DebugConsole.ThrowError( + $"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e); return null; } ItemComponent ic = null; @@ -930,10 +939,11 @@ namespace Barotrauma.Items.Components } catch (TargetInvocationException e) { - DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException); - GameAnalyticsManager.AddErrorEventOnce("ItemComponent.Load:TargetInvocationException" + item.Name + element.Name, + DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException); + GameAnalyticsManager.AddErrorEventOnce( + $"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}", GameAnalyticsManager.ErrorSeverity.Error, - "Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace.CleanupStackTrace()); + $"Error while loading entity of the type {type} ({e.InnerException})\n{Environment.StackTrace.CleanupStackTrace()}"); } return ic; @@ -974,7 +984,7 @@ namespace Barotrauma.Items.Components OverrideRequiredItems(originalElement); } - private void OverrideRequiredItems(XElement element) + private void OverrideRequiredItems(ContentXElement element) { var prevRequiredItems = new Dictionary>(requiredItems); requiredItems.Clear(); @@ -983,7 +993,7 @@ namespace Barotrauma.Items.Components #if CLIENT returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen; #endif - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1014,8 +1024,8 @@ namespace Barotrauma.Items.Components public virtual void ParseMsg() { - string msg = TextManager.Get(Msg, true); - if (msg != null) + LocalizedString msg = TextManager.Get(Msg); + if (msg.Loaded) { msg = TextManager.ParseInputTypes(msg); DisplayMsg = msg; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index e32673000..40bd8a5b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components //how many items can be contained private int capacity; - [Serialize(5, false, description: "How many items can be contained inside this item.")] + [Serialize(5, IsPropertySaveable.No, description: "How many items can be contained inside this item.")] public int Capacity { get { return capacity; } @@ -66,7 +66,7 @@ namespace Barotrauma.Items.Components //how many items can be contained private int maxStackSize; - [Serialize(64, false, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")] + [Serialize(64, IsPropertySaveable.No, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")] public int MaxStackSize { get { return maxStackSize; } @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components } private bool hideItems; - [Serialize(true, false, description: "Should the items contained inside this item be hidden." + [Serialize(true, IsPropertySaveable.No, description: "Should the items contained inside this item be hidden." + " If set to false, you should use the ItemPos and ItemInterval properties to determine where the items get rendered.")] public bool HideItems { @@ -85,55 +85,55 @@ namespace Barotrauma.Items.Components Drawable = !hideItems; } } - - [Serialize("0.0,0.0", false, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")] + + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")] public Vector2 ItemPos { get; set; } - [Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] public Vector2 ItemInterval { get; set; } - [Serialize(100, false, description: "How many items are placed in a row before starting a new row.")] + [Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } - [Serialize(true, false, description: "Should the inventory of this item be visible when the item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")] public bool DrawInventory { get; set; } - [Serialize(true, false, "Allow dragging and dropping items to deposit items into this inventory.")] + [Serialize(true, IsPropertySaveable.No, "Allow dragging and dropping items to deposit items into this inventory.")] public bool AllowDragAndDrop { get; set; } - - [Serialize(true, false)] + + [Serialize(true, IsPropertySaveable.No)] public bool AllowSwappingContainedItems { get; set; } - [Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained { get; set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool AllowAccess { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool AccessOnlyWhenBroken { get; set; } - [Serialize(5, false, description: "How many inventory slots the inventory has per row.")] + [Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } private readonly HashSet containableRestrictions = new HashSet(); - [Editable, Serialize("", true, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] + [Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] public string ContainableRestrictions { get { return string.Join(",", containableRestrictions); } @@ -143,46 +143,46 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, true, description: "Should this container be automatically filled with items?")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should this container be automatically filled with items?")] public bool AutoFill { get; set; } private float itemRotation; - [Serialize(0.0f, false, description: "The rotation in which the contained sprites are drawn (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The rotation in which the contained sprites are drawn (in degrees).")] public float ItemRotation { get { return MathHelper.ToDegrees(itemRotation); } set { itemRotation = MathHelper.ToRadians(value); } } - [Serialize("", false, description: "Specify an item for the container to spawn with.")] + [Serialize("", IsPropertySaveable.No, description: "Specify an item for the container to spawn with.")] public string SpawnWithId { get; set; } - [Serialize(false, false, description: "Should the items configured using SpawnWithId spawn if this item is broken.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the items configured using SpawnWithId spawn if this item is broken.")] public bool SpawnWithIdWhenBroken { get; set; } - - [Serialize(false, false, description: "Should the items be injected into the user.")] + + [Serialize(false, IsPropertySaveable.No, description: "Should the items be injected into the user.")] public bool AutoInject { get; set; } - [Serialize(0.5f, false, description: "The health threshold that the user must reach in order to activate the autoinjection.")] + [Serialize(0.5f, IsPropertySaveable.No, description: "The health threshold that the user must reach in order to activate the autoinjection.")] public float AutoInjectThreshold { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool RemoveContainedItemsOnDeconstruct { get; set; } private SlotRestrictions[] slotRestrictions; @@ -202,20 +202,20 @@ namespace Barotrauma.Items.Components if (!isRestrictionsDefined) { return true; } return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id)); } - - private ImmutableHashSet containableItemIdentifiers; - public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; + + private ImmutableHashSet containableItemIdentifiers; + public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; public override bool RecreateGUIOnResolutionChange => true; public List ContainableItems { get; } - public ItemContainer(Item item, XElement element) + public ItemContainer(Item item, ContentXElement element) : base(item, element) { int totalCapacity = capacity; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } ContainableItems ??= new List(); @@ -242,7 +242,7 @@ namespace Barotrauma.Items.Components } int subContainerIndex = capacity; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; } @@ -250,14 +250,14 @@ namespace Barotrauma.Items.Components int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); List subContainableItems = null; - foreach (XElement subSubElement in subElement.Elements()) + foreach (var subSubElement in subElement.Elements()) { if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - containable with no identifiers."); continue; } subContainableItems ??= new List(); @@ -283,7 +283,7 @@ namespace Barotrauma.Items.Components return slotRestrictions[slotIndex].MaxStackSize; } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public void OnItemContained(Item containedItem) { @@ -308,7 +308,7 @@ namespace Barotrauma.Items.Components if (item.GetComponent() != null) { - GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":GardeningPlanted:" + containedItem.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier); } //no need to Update() if this item has no statuseffects and no physics body @@ -424,7 +424,7 @@ namespace Barotrauma.Items.Components } } - public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } @@ -648,7 +648,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems; - containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); + containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); if (item.Submarine == null || !item.Submarine.Loading) { SpawnAlwaysContainedItems(); @@ -716,7 +716,7 @@ namespace Barotrauma.Items.Components else { IsActive = true; - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; }); + Entity.Spawner?.AddItemToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; }); } } } @@ -745,7 +745,7 @@ namespace Barotrauma.Items.Components Inventory.AllItemsMod.ForEach(it => it.Drop(null)); } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs index 205db5844..c1cb4be89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Ladder.cs @@ -7,14 +7,14 @@ namespace Barotrauma.Items.Components { public static List List { get; } = new List(); - public Ladder(Item item, XElement element) + public Ladder(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); List.Add(this); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override bool Select(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 77323447c..f4897de76 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components public string Name => LimbType.ToString(); - public Dictionary SerializableProperties => null; + public Dictionary SerializableProperties => null; public LimbPos(LimbType limbType, Vector2 position, bool allowUsingLimb) { @@ -61,21 +61,21 @@ namespace Barotrauma.Items.Components public IEnumerable LimbPositions { get { return limbPositions; } } - [Editable, Serialize(false, false, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)] public bool IsToggle { get; set; } - [Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)] public bool State { get; set; } - [Serialize(true, false, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")] public bool HideHUD { get; @@ -87,10 +87,10 @@ namespace Barotrauma.Items.Components Air, Water, Both }; - [Serialize(UseEnvironment.Both, false, description: "Can the item be selected in air, underwater or both.")] + [Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be selected in air, underwater or both.")] public UseEnvironment UsableIn { get; set; } - [Serialize(false, false, description: "Should the character using the item be drawn behind the item.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the character using the item be drawn behind the item.")] public bool DrawUserBehind { get; @@ -114,7 +114,7 @@ namespace Barotrauma.Items.Components private set; } = true; - public Controller(Item item, XElement element) + public Controller(Item item, ContentXElement element) : base(item, element) { limbPositions = new List(); @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components Enum.TryParse(element.GetAttributeString("direction", "None"), out dir); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name != "limbposition") { continue; } string limbStr = subElement.GetAttributeString("limb", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 944fd6055..004d78dd2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -33,15 +33,15 @@ namespace Barotrauma.Items.Components get { return outputContainer; } } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DeconstructItemsSimultaneously { get; set; } - [Editable, Serialize(1.0f, true)] + [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] public float DeconstructionSpeed { get; set; } public override bool RecreateGUIOnResolutionChange => true; - public Deconstructor(Item item, XElement element) + public Deconstructor(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); @@ -88,8 +88,7 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerConsumption <= 0.0f) { Voltage = 1.0f; } - progressTimer += deltaTime * Math.Min(Voltage, 1.0f); + progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, 1.0f); float tinkeringStrength = 0f; if (repairable.IsTinkering) @@ -114,9 +113,9 @@ namespace Barotrauma.Items.Components foreach (Item targetItem in items) { if ((Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false) || !inputContainer.Inventory.AllItems.Contains(targetItem)) { continue; } - var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => - (it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) && - (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))))); + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => + (it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) && + (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it != targetItem && (it.HasTag(r) || it.Prefab.Identifier == r))))).ToList(); ProcessItem(targetItem, items, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); } @@ -133,8 +132,8 @@ namespace Barotrauma.Items.Components var targetItem = inputContainer.Inventory.LastOrDefault(); if (targetItem == null) { return; } - var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => - it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))); + var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => + it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)).ToList(); float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; @@ -230,7 +229,7 @@ namespace Barotrauma.Items.Components foreach (Item otherItem in inputItems) { if (targetItem == otherItem) { continue; } - if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) + if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier)) { user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); foreach (Character character in Character.GetFriendlyCrew(user)) @@ -246,14 +245,14 @@ namespace Barotrauma.Items.Components { inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddToRemoveQueue(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); } allowRemove = false; return; } inputContainer.Inventory.RemoveItem(otherItem); OutputContainer.Inventory.RemoveItem(otherItem); - Entity.Spawner.AddToRemoveQueue(otherItem); + Entity.Spawner.AddItemToRemoveQueue(otherItem); } } } @@ -268,7 +267,7 @@ namespace Barotrauma.Items.Components int amount = (int)amountMultiplier; for (int i = 0; i < amount; i++) { - Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => + Entity.Spawner.AddItemToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { spawnedItem.StolenDuringRound = targetItem.StolenDuringRound; spawnedItem.AllowStealing = targetItem.AllowStealing; @@ -300,7 +299,7 @@ namespace Barotrauma.Items.Components } } - GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + targetItem.prefab.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier); if (targetItem.AllowDeconstruct && allowRemove) { @@ -314,7 +313,7 @@ namespace Barotrauma.Items.Components } } inputContainer.Inventory.RemoveItem(targetItem); - Entity.Spawner.AddToRemoveQueue(targetItem); + Entity.Spawner.AddItemToRemoveQueue(targetItem); MoveInputQueue(); PutItemsToLinkedContainer(); } @@ -392,16 +391,16 @@ namespace Barotrauma.Items.Components { if (deconstructItem.RequiredDeconstructor.Length > 0) { - if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) { continue; } } if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems) { - if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))) { continue; } + if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier == r))) { continue; } bool validOtherItemFound = false; foreach (Item otherInputItem in items) { if (otherInputItem == inputItem) { continue; } - if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier == r)) { continue; } var geneticMaterial1 = inputItem.GetComponent(); var geneticMaterial2 = otherInputItem.GetComponent(); @@ -427,7 +426,7 @@ namespace Barotrauma.Items.Components if (inputContainer.Inventory.IsEmpty()) { active = false; } IsActive = active; - currPowerConsumption = IsActive ? powerConsumption : 0.0f; + //currPowerConsumption = IsActive ? powerConsumption : 0.0f; userDeconstructorSpeedMultiplier = user != null ? 1f + user.GetStatValue(StatTypes.DeconstructorSpeedMultiplier) : 1f; #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 177571933..ceab83e9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components public Character User; [Editable(0.0f, 10000000.0f), - Serialize(500.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] + Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { get { return maxForce; } @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize("0.0,0.0", true, + [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The position of the propeller as an offset from the item's center (in pixels)."+ " Determines where the particles spawn and the position that causes characters to take damage from the engine if the PropellerDamage is defined.")] public Vector2 PropellerPos @@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DisablePropellerDamage { get; @@ -75,12 +75,12 @@ namespace Barotrauma.Items.Components private const float TinkeringForceIncrease = 1.5f; - public Engine(Item item, XElement element) + public Engine(Item item, ContentXElement element) : base(item, element) { IsActive = true; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -93,7 +93,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -103,14 +103,16 @@ namespace Barotrauma.Items.Components controlLockTimer -= deltaTime; - currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; - //engines consume more power when in a bad condition - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + if (powerConsumption == 0.0f) + { + prevVoltage = 1; + hasPower = true; + } + else + { + hasPower = Voltage > MinVoltage; + } - if (powerConsumption == 0.0f) { Voltage = 1.0f; } - - prevVoltage = Voltage; - hasPower = Voltage > MinVoltage; Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) @@ -154,6 +156,33 @@ namespace Barotrauma.Items.Components } } + /// + /// Power consumption of the engine. Only consume power when active and adjust consumption based on condition and target force. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != this.powerIn) + { + return 0; + } + + currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption; + //engines consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + return currPowerConsumption; + } + + /// + /// When grid is resolved update the previous voltage + /// + public override void GridResolved(Connection connection) + { + if (connection == powerIn) + { + prevVoltage = Voltage; + } + } + private void UpdateAITargets(float noise) { if (item.AiTarget != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 43dfd81d0..4fc2c9cf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -1,7 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -11,7 +13,7 @@ namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { - private readonly List fabricationRecipes = new List(); + private ImmutableDictionary fabricationRecipes; //this is not readonly because tutorials fuck this up!!!! private FabricationRecipe fabricatedItem; private float timeUntilReady; @@ -20,7 +22,7 @@ namespace Barotrauma.Items.Components private string savedFabricatedItem; private float savedTimeUntilReady, savedRequiredTime; - private readonly Dictionary> availableIngredients = new Dictionary>(); + private readonly Dictionary> availableIngredients = new Dictionary>(); const float RefreshIngredientsInterval = 1.0f; private float refreshIngredientsTimer; @@ -31,10 +33,10 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float FabricationSpeed { get; set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float SkillRequirementMultiplier { get; set; } private const float TinkeringSpeedIncrease = 2.5f; @@ -78,10 +80,10 @@ namespace Barotrauma.Items.Components private float progressState; - public Fabricator(Item item, XElement element) + public Fabricator(Item item, ContentXElement element) : base(item, element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { @@ -90,26 +92,22 @@ namespace Barotrauma.Items.Components } } + var fabricationRecipes = new Dictionary(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { - foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes) + foreach (FabricationRecipe recipe in itemPrefab.FabricationRecipes.Values) { if (recipe.SuitableFabricatorIdentifiers.Length > 0) { - if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.prefab.Identifier == i || item.HasTag(i))) + if (!recipe.SuitableFabricatorIdentifiers.Any(i => item.Prefab.Identifier == i || item.HasTag(i))) { continue; } } - fabricationRecipes.Add(recipe); + fabricationRecipes.Add(recipe.RecipeHash, recipe); } } - fabricationRecipes.Sort((r1, r2) => - { - int hash1 = (int)r1.TargetItem.UIntIdentifier; - int hash2 = (int)r2.TargetItem.UIntIdentifier; - return hash1 - hash2; - }); + this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary(); state = FabricatorState.Stopped; @@ -129,9 +127,9 @@ namespace Barotrauma.Items.Components inputContainer = containers[0]; outputContainer = containers[1]; - foreach (var recipe in fabricationRecipes) + foreach (var recipe in fabricationRecipes.Values) { - if (recipe.RequiredItems.Count > inputContainer.Capacity) + if (recipe.RequiredItems.Length > inputContainer.Capacity) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\": There's not enough room in the input inventory for the ingredients of \"" + recipe.TargetItem.Name + "\"!"); } @@ -158,16 +156,11 @@ namespace Barotrauma.Items.Components return picker != null; } - public void RemoveFabricationRecipes(List allowedIdentifiers) + public void RemoveFabricationRecipes(IEnumerable allowedIdentifiers) { - for (int i = 0; i < fabricationRecipes.Count; i++) - { - if (!allowedIdentifiers.Contains(fabricationRecipes[i].TargetItem.Identifier)) - { - fabricationRecipes.RemoveAt(i); - i--; - } - } + fabricationRecipes = fabricationRecipes + .Where(kvp => allowedIdentifiers.Contains(kvp.Value.TargetItemPrefabIdentifier)) + .ToImmutableDictionary(); CreateRecipes(); } @@ -201,9 +194,6 @@ namespace Barotrauma.Items.Components inputContainer.Inventory.Locked = true; outputContainer.Inventory.Locked = true; - currPowerConsumption = powerConsumption; - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); - if (GameMain.NetworkMember?.IsServer ?? true) { State = FabricatorState.Active; @@ -211,7 +201,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null && addToServerLog) { - GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName.Value + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } @@ -237,7 +227,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName.Value + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #elif CLIENT itemList.Enabled = true; @@ -304,11 +294,10 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerConsumption <= 0) { Voltage = 1.0f; } float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; - timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f); + timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(powerConsumption <= 0 ? 1 : Voltage, 1.0f); UpdateRequiredTimeProjSpecific(); @@ -370,7 +359,7 @@ namespace Barotrauma.Items.Components } availableItems.Remove(availableItem); - Entity.Spawner.AddToRemoveQueue(availableItem); + Entity.Spawner.AddItemToRemoveQueue(availableItem); inputContainer.Inventory.RemoveItem(availableItem); break; } @@ -389,7 +378,7 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - + quality = GetFabricatedItemQuality(fabricatedItem, user); } @@ -397,10 +386,10 @@ namespace Barotrauma.Items.Components for (int i = 0; i < (int)fabricationitemAmount.Value; i++) { float outCondition = fabricatedItem.OutCondition; - GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); + GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); if (i < amountFittingContainer) { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, + Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); @@ -413,7 +402,7 @@ namespace Barotrauma.Items.Components } else { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, + Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality, onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); @@ -469,13 +458,30 @@ namespace Barotrauma.Items.Components } + /// + /// Power consumption of the fabricator. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + //No consumption if not powerin or is off + if (connection != this.powerIn || !IsActive) + { + return 0; + } + + currPowerConsumption = PowerConsumption; + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + + return currPowerConsumption; + } + private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { if (user == null) { return 0; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; - foreach (string tag in fabricatedItem.TargetItem.Tags) + foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); } @@ -489,8 +495,8 @@ namespace Barotrauma.Items.Components } partial void UpdateRequiredTimeProjSpecific(); - - private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary> availableIngredients, Character character) + + private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary> availableIngredients, Character character) { if (fabricableItem == null) { return false; } if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } @@ -533,13 +539,13 @@ namespace Barotrauma.Items.Components return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f); } - public float FabricationDegreeOfSuccess(Character character, List skills) + public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { - if (skills.Count == 0) { return 1.0f; } + if (skills.Length == 0) { return 1.0f; } if (character == null) { return 0.0f; } float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); - float average = skillSum / skills.Count; + float average = skillSum / skills.Length; return (average + 100.0f) / 2.0f / 100.0f; } @@ -593,7 +599,7 @@ namespace Barotrauma.Items.Components availableIngredients.Clear(); foreach (Item item in itemList) { - var itemIdentifier = item.prefab.Identifier; + var itemIdentifier = item.Prefab.Identifier; if (!availableIngredients.ContainsKey(itemIdentifier)) { availableIngredients[itemIdentifier] = new List(itemList.Count); @@ -663,7 +669,7 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", ""); @@ -678,7 +684,7 @@ namespace Barotrauma.Items.Components inputContainer?.OnMapLoaded(); outputContainer?.OnMapLoaded(); - var recipe = fabricationRecipes.Find(r => r.TargetItem.Identifier == savedFabricatedItem); + var recipe = fabricationRecipes.Values.FirstOrDefault(r => r.TargetItem.Identifier == savedFabricatedItem); if (recipe == null) { DebugConsole.ThrowError("Error while loading a fabricator. Can't continue fabricating \"" + savedFabricatedItem + "\" (matching recipe not found)."); @@ -696,13 +702,13 @@ namespace Barotrauma.Items.Components } class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier { - public AbilityFabricatorSkillGain(string skillIdentifier, float skillAmount) + public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount) { SkillIdentifier = skillIdentifier; Value = skillAmount; } public float Value { get; set; } - public string SkillIdentifier { get; set; } + public Identifier SkillIdentifier { get; set; } } class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs index d89269827..889969115 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/MiniMap.cs @@ -29,49 +29,49 @@ namespace Barotrauma.Items.Components private readonly Dictionary hullDatas; - [Editable, Serialize(false, true, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")] public bool RequireWaterDetectors { get; set; } - [Editable, Serialize(true, true, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Does the machine require inputs from oxygen detectors in order to show the oxygen levels inside rooms.")] public bool RequireOxygenDetectors { get; set; } - [Editable, Serialize(true, true, description: "Should damaged walls be displayed by the machine.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should damaged walls be displayed by the machine.")] public bool ShowHullIntegrity { get; set; } - [Editable, Serialize(true, true, description: "Enable hull status mode.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Enable hull status mode.")] public bool EnableHullStatus { get; set; } - [Editable, Serialize(true, true, description: "Enable electrical view mode.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Enable electrical view mode.")] public bool EnableElectricalView { get; set; } - [Editable, Serialize(true, true, description: "Enable item finder mode.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Enable item finder mode.")] public bool EnableItemFinder { get; set; } - public MiniMap(Item item, XElement element) + public MiniMap(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -114,9 +114,6 @@ namespace Barotrauma.Items.Components } #endif - currPowerConsumption = powerConsumption; - currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); - hasPower = Voltage > MinVoltage; if (hasPower) { @@ -124,6 +121,19 @@ namespace Barotrauma.Items.Components } } + /// + /// Power consumption of the MiniMap. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != powerIn || !IsActive) + { + return 0; + } + + return PowerConsumption * MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition); + } + public override bool Pick(Character picker) { return picker != null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs index 0143e4013..a9439661a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OutpostTerminal.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { partial class OutpostTerminal : ItemComponent { - public OutpostTerminal(Item item, XElement element) : base(item, element) + public OutpostTerminal(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index d76ec970c..304a06991 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -24,14 +24,14 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)] + [Editable, Serialize(400.0f, IsPropertySaveable.Yes, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)] public float GeneratedAmount { get { return generatedAmount; } set { generatedAmount = MathHelper.Clamp(value, -10000.0f, 10000.0f); } } - public OxygenGenerator(Item item, XElement element) + public OxygenGenerator(Item item, ContentXElement element) : base(item, element) { //randomize update timer so all oxygen generators don't update at the same time @@ -44,25 +44,15 @@ namespace Barotrauma.Items.Components UpdateOnActiveEffects(deltaTime); CurrFlow = 0.0f; - currPowerConsumption = powerConsumption; - //consume more power when in a bad condition - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); - - if (powerConsumption <= 0.0f) - { - Voltage = 1.0f; - } if (item.CurrentHull == null) { return; } - - if (Voltage < MinVoltage) + + if (Voltage < MinVoltage && PowerConsumption > 0) { return; } - - CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f; - //less effective when in bad condition + CurrFlow = Math.Min(PowerConsumption > 0 ? Voltage : 1.0f, 1.0f) * generatedAmount * 100.0f; float conditionMult = item.Condition / item.MaxCondition; //100% condition = 100% oxygen //50% condition = 25% oxygen @@ -72,6 +62,23 @@ namespace Barotrauma.Items.Components UpdateVents(CurrFlow, deltaTime); } + /// + /// Power consumption of the Oxygen Generator. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != this.powerIn) + { + return 0; + } + + float consumption = powerConsumption; + + //consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref consumption); + return consumption; + } + public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index b7cb7b076..37dbc543f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components private float pumpSpeedLockTimer, isActiveLockTimer; - [Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] public float FlowPercentage { get { return flowPercentage; } @@ -48,18 +48,18 @@ namespace Barotrauma.Items.Components { if (!MathUtils.IsValid(flowPercentage)) { return; } flowPercentage = MathHelper.Clamp(value, -100.0f, 100.0f); - flowPercentage = MathUtils.Round(flowPercentage, 1.0f); + flowPercentage = MathF.Round(flowPercentage); } } - [Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] + [Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { get { return maxFlow; } set { maxFlow = value; } } - [Editable, Serialize(true, true, alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool IsOn { get { return IsActive; } @@ -83,13 +83,13 @@ namespace Barotrauma.Items.Components public override bool UpdateWhenInactive => true; - public Pump(Item item, XElement element) + public Pump(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -120,10 +120,6 @@ namespace Barotrauma.Items.Components FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } - currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); - //pumps consume more power when in a bad condition - item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); - if (!HasPower) { return; } UpdateProjSpecific(deltaTime); @@ -147,10 +143,9 @@ namespace Barotrauma.Items.Components item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate; if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 30.0f * deltaTime; } - Voltage -= deltaTime; } - public void InfectBallast(string identifier, bool allowMultiplePerShip = false) + public void InfectBallast(Identifier identifier, bool allowMultiplePerShip = false) { Hull hull = item.CurrentHull; if (hull == null) { return; } @@ -158,7 +153,7 @@ namespace Barotrauma.Items.Components if (!allowMultiplePerShip) { // if the ship is already infected then do nothing - if (Hull.hullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; } + if (Hull.HullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; } } if (hull.BallastFlora != null) { return; } @@ -178,6 +173,24 @@ namespace Barotrauma.Items.Components #endif } + /// + /// Power consumption of the Pump. Only consume power when active and adjust consumption based on condition. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + //There shouldn't be other power connections to this + if (connection != this.powerIn) + { + return 0; + } + + currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); + //pumps consume more power when in a bad condition + item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); + + return currPowerConsumption; + } + partial void UpdateProjSpecific(float deltaTime); public override void ReceiveSignal(Signal signal, Connection connection) @@ -218,7 +231,8 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.Client != null) { return false; } #endif - switch (objective.Option.ToLowerInvariant()) + + switch (objective.Option.Value.ToLowerInvariant()) { case "pumpout": #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 02028708a..7abf1b323 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -36,8 +36,8 @@ namespace Barotrauma.Items.Components private float fireTimer, fireDelay; private float maxPowerOutput; - - private readonly Queue loadQueue = new Queue(); + private float minUpdatePowerOut; + private float maxUpdatePowerOut; private bool unsentChanges; private float sendUpdateTimer; @@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components private bool _powerOn; - [Serialize(defaultValue: false, isSaveable: true)] + [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] public bool PowerOn { get { return _powerOn; } @@ -63,9 +63,11 @@ namespace Barotrauma.Items.Components } } + protected override PowerPriority Priority { get { return PowerPriority.Reactor; } } + public Character LastAIUser { get; private set; } - [Serialize(defaultValue: false, isSaveable: true)] + [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)] public bool LastUserWasPlayer { get; private set; } private Character lastUser; @@ -81,7 +83,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { get { return maxPowerOutput; } @@ -91,21 +93,21 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue), Serialize(120.0f, true, description: "How long the temperature has to stay critical until a meltdown occurs.")] + [Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")] public float MeltdownDelay { get { return meltDownDelay; } set { meltDownDelay = Math.Max(value, 0.0f); } } - [Editable(0.0f, float.MaxValue), Serialize(30.0f, true, description: "How long the temperature has to stay critical until the reactor catches fire.")] + [Editable(0.0f, float.MaxValue), Serialize(30.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until the reactor catches fire.")] public float FireDelay { get { return fireDelay; } set { fireDelay = Math.Max(value, 0.0f); } } - [Serialize(0.0f, true, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")] public float Temperature { get { return temperature; } @@ -116,7 +118,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float FissionRate { get { return fissionRate; } @@ -127,7 +129,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float TurbineOutput { get { return turbineOutput; } @@ -138,7 +140,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] public float FuelConsumptionRate { get { return fuelConsumptionRate; } @@ -149,14 +151,14 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, true, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")] public bool TemperatureCritical { get { return temperature > allowedTemperature.Y; } set { /*do nothing*/ } } - [Serialize(false, true, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public bool AutoTemp { get { return autoTemp; } @@ -171,36 +173,36 @@ namespace Barotrauma.Items.Components private float prevAvailableFuel; - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float AvailableFuel { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public new float Load { get; private set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float TargetFissionRate { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float TargetTurbineOutput { get; set; } - [Serialize(0.0f, true)] + [Serialize(0.0f, IsPropertySaveable.Yes)] public float CorrectTurbineOutput { get; set; } - [Editable, Serialize(true, true)] + [Editable, Serialize(true, IsPropertySaveable.Yes)] public bool ExplosionDamagesOtherSubs { get; set; } - public Reactor(Item item, XElement element) + public Reactor(Item item, ContentXElement element) : base(item, element) { IsActive = true; InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { @@ -248,7 +250,7 @@ namespace Barotrauma.Items.Components //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f)) { - CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -10.0f, 10.0f) * deltaTime; + CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; } //calculate tolerances of the meters based on the skills of the user @@ -277,25 +279,6 @@ namespace Barotrauma.Items.Components TurbineOutput = MathHelper.Lerp(turbineOutput, TargetTurbineOutput, deltaTime); float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); - currPowerConsumption = -MaxPowerOutput * Math.Min(turbineOutput / 100.0f, temperatureFactor); - - //if the turbine output and coolant flow are the optimal range, - //make the generated power slightly adjust according to the load - // (-> the reactor can automatically handle small changes in load as long as the values are roughly correct) - if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y && - temperature > optimalTemperature.X && temperature < optimalTemperature.Y) - { - float maxAutoAdjust = maxPowerOutput * 0.1f; - autoAdjustAmount = MathHelper.Lerp( - autoAdjustAmount, - MathHelper.Clamp(-Load - currPowerConsumption, -maxAutoAdjust, maxAutoAdjust), - deltaTime * 10.0f); - } - else - { - autoAdjustAmount = MathHelper.Lerp(autoAdjustAmount, 0.0f, deltaTime * 10.0f); - } - currPowerConsumption += autoAdjustAmount; if (!PowerOn) { @@ -304,37 +287,9 @@ namespace Barotrauma.Items.Components } else if (autoTemp) { - UpdateAutoTemp(2.0f, deltaTime); - } - float currentLoad = 0.0f; - List connections = item.Connections; - if (connections != null && connections.Count > 0) - { - foreach (Connection connection in connections) - { - if (!connection.IsPower) { continue; } - foreach (Connection recipient in connection.Recipients) - { - if (!(recipient.Item is Item it)) { continue; } - - PowerTransfer pt = it.GetComponent(); - if (pt == null) { continue; } - - //calculate how much external power there is in the grid - //(power coming from somewhere else than this reactor, e.g. batteries) - float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f; - //reduce the external power from the load to prevent overloading the grid - currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower); - } - } + UpdateAutoTemp(10.0f, deltaTime * 2f); } - loadQueue.Enqueue(currentLoad); - while (loadQueue.Count() > 60.0f) - { - Load = loadQueue.Average(); - loadQueue.Dequeue(); - } float fuelLeft = 0.0f; var containedItems = item.OwnInventory?.AllItems; @@ -405,6 +360,77 @@ namespace Barotrauma.Items.Components } } + /// + /// Returns a negative value (indicating the reactor generates power) when querying the power output connection. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + return connection != null && connection.IsPower && connection.IsOutput ? -1 : 0; + } + + /// + /// Min and Max power output of the reactor based on tolerance + /// + public override PowerRange MinMaxPowerOut(Connection conn, float load) + { + float tolerance = 1f; + + //If within the optimal output allow for slight output adjustments + if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y && + temperature > optimalTemperature.X && temperature < optimalTemperature.Y) + { + tolerance = 3f; + } + + float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); + float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); + float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); + + minUpdatePowerOut = minOutput; + maxUpdatePowerOut = maxOutput; + + float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut; + + return new PowerRange(minOutput, maxOutput, reactorMax); + } + + /// + /// Determine how much power to output based on the load. The load is divided between reactors according to their maximum output in multi-reactor setups. + /// + public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) + { + //Load must be calculated at this stage instead of at gridResolved to remove influence of lower priority devices + float loadLeft = MathHelper.Max(load - power,0); + float expectedPower = MathHelper.Clamp(loadLeft, minMaxPower.Min, minMaxPower.Max); + + //Delta ratio of Min and Max power output capability of the grid + float ratio = MathHelper.Max((loadLeft - minMaxPower.Min) / (minMaxPower.Max - minMaxPower.Min), 0); + if (float.IsInfinity(ratio)) + { + ratio = 0; + } + + float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut); + float newLoad = loadLeft; + + //Adjust behaviour for multi reactor setup + if (MaxPowerOutput != minMaxPower.ReactorMaxOutput) + { + float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; + float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); + newLoad = MathHelper.Clamp(loadLeft - (expectedPower + output) + loadAdjust, 0, loadLeft); + } + + if (float.IsNegative(newLoad)) + { + newLoad = 0.0f; + } + + Load = newLoad; + currPowerConsumption = -output; + return output; + } + private float GetGeneratedHeat(float fissionRate) { return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f; @@ -595,7 +621,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } character.AIController.SteeringManager.Reset(); - bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase); + bool shutDown = objective.Option == "shutdown"; IsActive = true; @@ -624,7 +650,7 @@ namespace Barotrauma.Items.Components var containObjective = AIContainItems(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true); containObjective.Completed += ReportFuelRodCount; containObjective.Abandoned += ReportFuelRodCount; - character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); + character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, "reactorfuel".ToIdentifier(), 30.0f); void ReportFuelRodCount() { @@ -633,12 +659,12 @@ namespace Barotrauma.Items.Components int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); if (remainingFuelRods == 0) { - character.Speak(TextManager.Get("DialogOutOfFuelRods"), null, 0.0f, "outoffuelrods", 30.0f); + character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f); outOfFuel = true; } else if (remainingFuelRods < 3) { - character.Speak(TextManager.Get("DialogLowOnFuelRods"), null, 0.0f, "lowonfuelrods", 30.0f); + character.Speak(TextManager.Get("DialogLowOnFuelRods").Value, null, 0.0f, "lowonfuelrods".ToIdentifier(), 30.0f); } } } @@ -655,7 +681,7 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogReactorIsBroken"), identifier: "reactorisbroken", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get("DialogReactorIsBroken").Value, identifier: "reactorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } } if (TooMuchFuel()) @@ -676,7 +702,7 @@ namespace Barotrauma.Items.Components { if (lastUser.SelectedConstruction == item && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f); + character.Speak(TextManager.Get("DialogReactorTaken").Value, null, 0.0f, "reactortaken".ToIdentifier(), 10.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index ec4fd008e..97657bc39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components get { return connectedTransducers.Select(t => t.Transducer); } } - [Serialize(DefaultSonarRange, false, description: "The maximum range of the sonar.")] + [Serialize(DefaultSonarRange, IsPropertySaveable.No, description: "The maximum range of the sonar.")] public float Range { get { return range; } @@ -92,29 +92,29 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "Should the sonar display the walls of the submarine it is inside.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the sonar display the walls of the submarine it is inside.")] public bool DetectSubmarineWalls { get; set; } - [Editable, Serialize(false, false, description: "Does the sonar have to be connected to external transducers to work.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have to be connected to external transducers to work.")] public bool UseTransducers { get; set; } - [Editable, Serialize(false, false, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")] public bool CenterOnTransducers { get; set; } - [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " + - "Only available in-game when the Item has no Steering component.")] + [Editable, Serialize(false, IsPropertySaveable.No, description: "Does the sonar have mineral scanning mode. " + + "Only available in-game when the Item has no Steering component.")] public bool HasMineralScanner { get; set; } public float Zoom @@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; - public Sonar(Item item, XElement element) + public Sonar(Item item, ContentXElement element) : base(item, element) { connectedTransducers = new List(); @@ -155,12 +155,10 @@ namespace Barotrauma.Items.Components CurrentMode = Mode.Passive; } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { - currPowerConsumption = (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption; - UpdateOnActiveEffects(deltaTime); if (UseTransducers) @@ -240,8 +238,19 @@ namespace Barotrauma.Items.Components ++pingIndex; } } + } - Voltage -= deltaTime; + /// + /// Power consumption of the sonar. Only consume power when active and adjust the consumption based on the sonar mode. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != powerIn || !IsActive) + { + return 0; + } + + return (currentMode == Mode.Active) ? powerConsumption : powerConsumption * PassivePowerConsumption; } public override bool Use(float deltaTime, Character character = null) @@ -266,7 +275,8 @@ namespace Barotrauma.Items.Components if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) { continue; } - string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition); + #warning This is not the best key for a dictionary. + string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition).Value; if (!targetGroups.ContainsKey(directionName)) { targetGroups.Add(directionName, new List()); @@ -289,9 +299,10 @@ namespace Barotrauma.Items.Components if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" }, - new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() }, - new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60); + character.Speak(TextManager.GetWithVariables(dialogTag, + ("[direction]", targetGroup.Key.ToString(), FormatCapitals.Yes), + ("[count]", targetGroup.Value.Count.ToString(), FormatCapitals.No)).Value, + null, 0, $"sonartarget{targetGroup.Value[0].ID}".ToIdentifier(), 60); } //prevent the character from reporting other targets in the group @@ -304,7 +315,7 @@ namespace Barotrauma.Items.Components return true; } - private string GetDirectionName(Vector2 dir) + private LocalizedString GetDirectionName(Vector2 dir) { float angle = MathUtils.WrapAngleTwoPi((float)-Math.Atan2(dir.Y, dir.X) + MathHelper.PiOver2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs index b4c9c7252..40d736be3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/SonarTransducer.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Items.Components public Sonar ConnectedSonar; - public SonarTransducer(Item item, XElement element) : base(item, element) + public SonarTransducer(Item item, ContentXElement element) : base(item, element) { IsActive = true; } @@ -19,8 +19,6 @@ namespace Barotrauma.Items.Components { UpdateOnActiveEffects(deltaTime); - CurrPowerConsumption = powerConsumption * (ConnectedSonar?.CurrentMode == Sonar.Mode.Active ? 1.0f : Sonar.PassivePowerConsumption); - if (Voltage >= MinVoltage) { sendSignalTimer += deltaTime; @@ -31,5 +29,15 @@ namespace Barotrauma.Items.Components } } } + + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection != powerIn || !IsActive) + { + return 0; + } + + return PowerConsumption * (ConnectedSonar?.CurrentMode == Sonar.Mode.Active ? 1.0f : Sonar.PassivePowerConsumption); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index e56dcf7e9..b41f7c4bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -1,4 +1,4 @@ -using Barotrauma.Networking; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -106,7 +106,7 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 1.0f, decimals: 4), - Serialize(0.5f, true, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards." + Serialize(0.5f, IsPropertySaveable.Yes, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards." + " Can be used to compensate if the ballast tanks are too large/small relative to the size of the submarine.")] public float NeutralBallastLevel { @@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(1000.0f, true, description: "How close the docking port has to be to another docking port for the docking mode to become active.")] + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How close the docking port has to be to another docking port for the docking mode to become active.")] public float DockingAssistThreshold { get; @@ -231,14 +231,14 @@ namespace Barotrauma.Items.Components } #endregion - public Steering(Item item, XElement element) + public Steering(Item item, ContentXElement element) : base(item, element) { IsActive = true; InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { @@ -292,8 +292,6 @@ namespace Barotrauma.Items.Components controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null; } - currPowerConsumption = powerConsumption; - if (Voltage < MinVoltage) { return; } if (user != null && user.Removed) @@ -405,7 +403,7 @@ namespace Barotrauma.Items.Components float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f; user.Info.IncreaseSkillLevel( - "helm", + "helm".ToIdentifier(), SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime); } @@ -724,7 +722,7 @@ namespace Barotrauma.Items.Components { if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f); + character.Speak(TextManager.Get("DialogSteeringTaken").Value, null, 0.0f, "steeringtaken".ToIdentifier(), 10.0f); } } user = character; @@ -738,7 +736,7 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogNavTerminalIsBroken"), identifier: "navterminalisbroken", minDurationBetweenSimilar: 30.0f); + character.Speak(TextManager.Get("DialogNavTerminalIsBroken").Value, identifier: "navterminalisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } } @@ -748,64 +746,72 @@ namespace Barotrauma.Items.Components AutoPilot = true; } IncreaseSkillLevel(user, deltaTime); - switch (objective.Option.ToLowerInvariant()) + if (objective.Option == "maintainposition") { - case "maintainposition": - if (objective.Override) - { - SetMaintainPosition(); - } - break; - case "navigateback": - if (Level.IsLoadedOutpost) { break; } + if (objective.Override) + { + SetMaintainPosition(); + } + } + else if (!Level.IsLoadedOutpost) + { + if (objective.Option == "navigateback") + { if (DockingSources.Any(d => d.Docked)) { item.SendSignal("1", "toggle_docking"); } + if (objective.Override) { if (MaintainPos || LevelEndSelected || !LevelStartSelected || navigateTactically) { unsentChanges = true; } + SetDestinationLevelStart(); } - break; - case "navigatetodestination": - if (Level.IsLoadedOutpost) { break; } + } + else if (objective.Option == "navigatetodestination") + { if (DockingSources.Any(d => d.Docked)) { item.SendSignal("1", "toggle_docking"); } + if (objective.Override) { if (MaintainPos || !LevelEndSelected || LevelStartSelected || navigateTactically) { unsentChanges = true; } + SetDestinationLevelEnd(); } - break; - case "navigatetactical": - if (Level.IsLoadedOutpost) { break; } + } + else if (objective.Option == "navigatetactical") + { if (DockingSources.Any(d => d.Docked)) { item.SendSignal("1", "toggle_docking"); } + if (objective.Override) { if (MaintainPos || LevelEndSelected || LevelStartSelected || !navigateTactically) { unsentChanges = true; } + SetDestinationTactical(); } - break; + } } + sonar?.AIOperate(deltaTime, character, objective); if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f); + character.Speak(TextManager.Get("dialogicespirespottedsonar").Value, null, 0.0f, "icespirespottedsonar".ToIdentifier(), 60.0f); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs index f4cb519e8..6d51ecdc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components set { oxygenFlow = Math.Max(value, 0.0f); } } - public Vent (Item item, XElement element) : base(item, element) { } + public Vent (Item item, ContentXElement element) : base(item, element) { } public override void Update(float deltaTime, Camera cam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs index 25eebb6a9..4e5fc3b5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs @@ -4,10 +4,10 @@ namespace Barotrauma.Items.Components { class NameTag : ItemComponent { - [InGameEditable(MaxLength = 32), Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)] + [InGameEditable(MaxLength = 32), Serialize("", IsPropertySaveable.No, description: "Name written on the tag.", alwaysUseInstanceValues: true)] public string WrittenName { get; set; } - public NameTag(Item item, XElement element) : base(item, element) + public NameTag(Item item, ContentXElement element) : base(item, element) { AllowInGameEditing = true; DrawHudWhenEquipped = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index f59bc100d..ae166ebf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components public Vector2 Offset; public float Size; - public PlantSlot(XElement element) + public PlantSlot(ContentXElement element) { Offset = element.GetAttributeVector2("offset", Vector2.Zero); Size = element.GetAttributeFloat("size", 0.5f); @@ -64,14 +64,14 @@ namespace Barotrauma.Items.Components private float fertilizer; - [Serialize(0f, true, "How much fertilizer the planter has.")] + [Serialize(0f, IsPropertySaveable.Yes, "How much fertilizer the planter has.")] public float Fertilizer { get => fertilizer; set => fertilizer = Math.Clamp(value, 0, FertilizerCapacity); } - [Serialize(100f, true, "How much fertilizer can the planter hold.")] + [Serialize(100f, IsPropertySaveable.Yes, "How much fertilizer can the planter hold.")] public float FertilizerCapacity { get; set; } public Growable?[] GrowableSeeds = new Growable?[0]; @@ -81,11 +81,11 @@ namespace Barotrauma.Items.Components private ItemContainer? container; private float growthTickTimer; - public Planter(Item item, XElement element) : base(item, element) + public Planter(Item item, ContentXElement element) : base(item, element) { canBePicked = true; SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -117,7 +117,7 @@ namespace Barotrauma.Items.Components GrowableSeeds = new Growable[container.Capacity]; } - public override bool HasRequiredItems(Character character, bool addMessage, string? msg = null) + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString? msg = null) { if (container?.Inventory == null) { return false; } @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components if (!anyDecayed || seed.Decayed || seed.FullyGrown) { container?.Inventory.RemoveItem(seed.Item); - Entity.Spawner?.AddToRemoveQueue(seed.Item); + Entity.Spawner?.AddItemToRemoveQueue(seed.Item); GrowableSeeds[i] = null; ApplyStatusEffects(ActionType.OnPicked, 1.0f, character); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 576ab086d..3d1631663 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -13,8 +13,6 @@ namespace Barotrauma.Items.Components private float charge; - //private float rechargeVoltage; - //how fast the battery can be recharged private float maxRechargeSpeed; @@ -27,45 +25,52 @@ namespace Barotrauma.Items.Components protected Vector2 indicatorPosition, indicatorSize; protected bool isHorizontal; - + + protected override PowerPriority Priority { get { return PowerPriority.Battery; } } + + private float currPowerOutput; public float CurrPowerOutput { - get; - private set; + get { return currPowerOutput; } + private set + { + System.Diagnostics.Debug.Assert(value >= 0.0f); + currPowerOutput = Math.Max(0, value); + } } - [Serialize("0,0", true, description: "The position of the progress bar indicating the charge of the item. In pixels as an offset from the upper left corner of the sprite.")] + [Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the progress bar indicating the charge of the item. In pixels as an offset from the upper left corner of the sprite.")] public Vector2 IndicatorPosition { get { return indicatorPosition; } set { indicatorPosition = value; } } - [Serialize("0,0", true, description: "The size of the progress bar indicating the charge of the item (in pixels).")] + [Serialize("0,0", IsPropertySaveable.Yes, description: "The size of the progress bar indicating the charge of the item (in pixels).")] public Vector2 IndicatorSize { get { return indicatorSize; } set { indicatorSize = value; } } - [Serialize(false, true, description: "Should the progress bar indicating the charge of the item fill up horizontally or vertically.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge of the item fill up horizontally or vertically.")] public bool IsHorizontal { get { return isHorizontal; } set { isHorizontal = value; } } - [Editable, Serialize(10.0f, true, description: "Maximum output of the device when fully charged (kW).")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "Maximum output of the device when fully charged (kW).")] public float MaxOutPut { set; get; } - [Editable, Serialize(10.0f, true, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] public float Capacity { get { return capacity; } set { capacity = Math.Max(value, 1.0f); } } - [Editable, Serialize(0.0f, true, description: "The current charge of the device.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")] public float Charge { get { return charge; } @@ -87,14 +92,14 @@ namespace Barotrauma.Items.Components public float ChargePercentage => MathUtils.Percentage(Charge, Capacity); - [Editable, Serialize(10.0f, true, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed { get { return maxRechargeSpeed; } set { maxRechargeSpeed = Math.Max(value, 1.0f); } } - [Editable, Serialize(10.0f, true, description: "The current recharge speed of the device.")] + [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The current recharge speed of the device.")] public float RechargeSpeed { get { return rechargeSpeed; } @@ -110,14 +115,14 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, true, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] + [Serialize(false, IsPropertySaveable.Yes, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] public bool ExponentialRechargeSpeed { get; set; } - [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, true)] + [Editable(minValue: 0.0f, maxValue: 10.0f, decimals: 2), Serialize(0.5f, IsPropertySaveable.Yes)] public float RechargeAdjustSpeed { get; set; } private float efficiency; - [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, true, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] + [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, IsPropertySaveable.Yes, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] public float Efficiency { get { return efficiency; } @@ -130,7 +135,7 @@ namespace Barotrauma.Items.Components private bool isRunning; public bool HasBeenTuned { get; private set; } - public PowerContainer(Item item, XElement element) + public PowerContainer(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -154,92 +159,142 @@ namespace Barotrauma.Items.Components isRunning = true; float chargeRatio = charge / capacity; - float gridPower = 0.0f; - float gridLoad = 0.0f; - foreach (Connection c in item.Connections) - { - if (!c.IsPower || !c.IsOutput) { continue; } - foreach (Connection c2 in c.Recipients) - { - if (c2.Item.Condition <= 0.0f) { continue; } - - PowerTransfer pt = c2.Item.GetComponent(); - if (pt == null) - { - foreach (Powered powered in c2.Item.GetComponents()) - { - if (!powered.IsActive) continue; - gridLoad += powered.CurrPowerConsumption; - } - continue; - } - if (!pt.IsActive || !pt.CanTransfer) { continue; } - gridPower -= pt.CurrPowerConsumption; - gridLoad += pt.PowerLoad; - } - } if (chargeRatio > 0.0f) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } - if (charge >= capacity) + float loadReading = 0; + if (powerOut != null && powerOut.Grid != null) { - //rechargeVoltage = 0.0f; - charge = capacity; - CurrPowerConsumption = 0.0f; - } - else - { - float missingCharge = capacity - charge; - float targetRechargeSpeed = rechargeSpeed; - if (ExponentialRechargeSpeed) - { - targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; - } - if (missingCharge < 1.0f) - { - targetRechargeSpeed *= missingCharge; - } - if (currPowerConsumption < targetRechargeSpeed) - { - currPowerConsumption = Math.Min(currPowerConsumption + deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); - } - else - { - currPowerConsumption = Math.Max(currPowerConsumption - deltaTime * maxRechargeSpeed * RechargeAdjustSpeed, targetRechargeSpeed); - } - Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f * efficiency; - } - - if (charge <= 0.0f) - { - CurrPowerOutput = 0.0f; - charge = 0.0f; - return; - } - else - { - //output starts dropping when the charge is less than 10% - float maxOutputRatio = 1.0f; - if (chargeRatio < 0.1f) - { - maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f); - } - - CurrPowerOutput += (gridLoad - gridPower) * deltaTime; - - float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad); - CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput); - Charge -= CurrPowerOutput / 3600.0f; + loadReading = powerOut.Grid.Load; } + item.SendSignal(((int)Math.Round(-CurrPowerOutput)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate"); } + /// + /// Returns the power consumption if checking the powerIn connection, or a negative value if the output can provide power when checking powerOut. + /// Power consumption is proportional to set recharge speed and if there is less than max charge. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) + { + if (connection == powerIn) + { + //Don't draw power if fully charged + if (charge >= capacity) + { + charge = capacity; + return 0; + } + else + { + float missingCharge = capacity - charge; + float targetRechargeSpeed = rechargeSpeed; + + if (ExponentialRechargeSpeed) + { + targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; + } + //For the last kwMin scale the recharge rate linearly to prevent overcharging and to have a smooth cutoff + if (missingCharge < 1.0f) + { + targetRechargeSpeed *= missingCharge; + } + + return MathHelper.Clamp(targetRechargeSpeed, 0, MaxRechargeSpeed); + } + } + else + { + CurrPowerOutput = 0; + return charge > 0 ? -1 : 0; + } + } + + /// + /// Minimum and maximum output for the queried connection. + /// Powerin min max equals CurrPowerConsumption as its abnormal for there to be power out. + /// PowerOut min power out is zero and max is the maxout unless below 10% charge where + /// the output is scaled relative to the 10% charge. + /// + /// Connection being queried + /// Current grid load + /// Minimum and maximum power output for the connection + public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) + { + if (connection == powerOut) + { + float maxOutput; + float chargeRatio = charge / capacity; + if (chargeRatio < 0.1f) + { + maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; + } + else + { + maxOutput = MaxOutPut; + } + + //Limit max power out to not exceed the charge of the container + maxOutput = Math.Min(maxOutput, charge * 60 / UpdateInterval); + return new PowerRange(0.0f, maxOutput); + } + + return PowerRange.Zero; + } + + /// + /// Finalized power out from the container for the connection, provided the given grid information + /// Output power based on the maxpower all batteries can output. So all batteries can + /// equally share powerout based on their output capabilities. + /// + /// + /// + /// + /// + /// + public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) + { + if (connection == powerOut) + { + //Calculate the max power the container can output + float maxPowerOutput = MaxOutPut; + float chargeRatio = charge / capacity; + if (chargeRatio < 0.1f) + { + maxPowerOutput *= Math.Max(chargeRatio * 10.0f, 0.0f); + } + + //Set power output based on the relative max power output capabilities and load demand + CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * maxPowerOutput; + return CurrPowerOutput; + } + return 0.0f; + } + + /// + /// When the corresponding grid connection is resolved, adjust the container's charge. + /// + public override void GridResolved(Connection conn) + { + if (conn == powerIn) + { + //Increase charge based on how much power came in from the grid + Charge += (CurrPowerConsumption * Voltage) / 60 * UpdateInterval * efficiency; + } + else + { + //Decrease charge based on how much power is leaving the device + Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + } + } + public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } @@ -250,8 +305,8 @@ namespace Barotrauma.Items.Components } if (HasBeenTuned) { return true; } - float targetRatio = string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase) ? aiRechargeTargetRatio : -1; - if (targetRatio > 0 || float.TryParse(objective.Option, out targetRatio)) + float targetRatio = objective.Option.IsEmpty || objective.Option == "charge" ? aiRechargeTargetRatio : -1; + if (targetRatio > 0 || float.TryParse(objective.Option.Value, out targetRatio)) { if (Math.Abs(rechargeSpeed - maxRechargeSpeed * targetRatio) > 0.05f) { @@ -267,9 +322,10 @@ namespace Barotrauma.Items.Components #endif if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", + ("[itemname]", item.Name, FormatCapitals.Yes), + ("[rate]", ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString(), FormatCapitals.No)).Value, + null, 1.0f, "chargebattery".ToIdentifier(), 10.0f); } } } @@ -289,9 +345,10 @@ namespace Barotrauma.Items.Components #endif if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", + ("[itemname]", item.Name, FormatCapitals.Yes), + ("[rate]", ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString(), FormatCapitals.No)).Value, + null, 1.0f, "chargebattery".ToIdentifier(), 10.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 33f934b24..82db50a9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -26,18 +26,25 @@ namespace Barotrauma.Items.Components public float PowerLoad { - get { return powerLoad; } + get + { + if (this is RelayComponent || PowerConnections.Count == 0 || PowerConnections[0].Grid == null) + { + return powerLoad; + } + return PowerConnections[0].Grid.Load; + } set { powerLoad = value; } } - [Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged if too much power is supplied to the power grid.")] public bool CanBeOverloaded { get; set; } - [Editable(MinValueFloat = 1.0f), Serialize(2.0f, true, description: + [Editable(MinValueFloat = 1.0f), Serialize(2.0f, IsPropertySaveable.Yes, description: "How much power has to be supplied to the grid relative to the load before item starts taking damage. " + "E.g. a value of 2 means that the grid has to be receiving twice as much power as the devices in the grid are consuming.")] public float OverloadVoltage @@ -46,14 +53,14 @@ namespace Barotrauma.Items.Components set; } - [Serialize(0.15f, true, description: "The probability for a fire to start when the item breaks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.15f, IsPropertySaveable.Yes, description: "The probability for a fire to start when the item breaks."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float FireProbability { get; set; } - [Serialize(false, false, description: "Is the item currently overloaded. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(false, IsPropertySaveable.No, description: "Is the item currently overloaded. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public bool Overload { get; @@ -62,6 +69,7 @@ namespace Barotrauma.Items.Components private float extraLoad; private float extraLoadSetTime; + /// /// Additional load coming from somewhere else than the devices connected to the junction box (e.g. ballast flora or piezo crystals). /// Goes back to zero automatically if you stop setting the value. @@ -71,7 +79,7 @@ namespace Barotrauma.Items.Components get { return extraLoad; } set { - extraLoad = Math.Max(value, 0.0f); + extraLoad = value; extraLoadSetTime = (float)Timing.TotalTime; } } @@ -112,7 +120,7 @@ namespace Barotrauma.Items.Components } } - public PowerTransfer(Item item, XElement element) + public PowerTransfer(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -168,9 +176,34 @@ namespace Barotrauma.Items.Components { RefreshConnections(); + float powerReadingOut = 0; + float loadReadingOut = ExtraLoad; + if (powerLoad < 0) + { + powerReadingOut = -powerLoad; + loadReadingOut = 0; + } + + if (powerOut != null && powerOut.Grid != null) + { + powerReadingOut = powerOut.Grid.Power; + loadReadingOut = powerOut.Grid.Load; + } + + item.SendSignal(((int)Math.Round(powerReadingOut)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(loadReadingOut)).ToString(), "load_value_out"); + if (Timing.TotalTime > extraLoadSetTime + 1.0) { - extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0); + //Decay the extra load to 0 from either positive or negative + if (extraLoad > 0) + { + extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0); + } + else + { + extraLoad = Math.Min(extraLoad + 1000.0f * deltaTime, 0); + } } if (!CanTransfer) { return; } @@ -200,7 +233,9 @@ namespace Barotrauma.Items.Components item.SendSignal(loadSignal, "load_value_out"); float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); - Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; + + Overload = Voltage > maxOverVoltage; + if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { if (overloadCooldownTimer > 0.0f) @@ -239,6 +274,11 @@ namespace Barotrauma.Items.Components } } + public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) + { + return conn == powerOut ? PowerConsumption + ExtraLoad : 0; + } + public override bool Pick(Character picker) { return picker != null; @@ -376,25 +416,6 @@ namespace Barotrauma.Items.Components SetAllConnectionsDirty(); } - public override void ReceivePowerProbeSignal(Connection connection, Item source, float power) - { - //we've already received this signal - if (lastPowerProbeRecipients.Contains(this)) { return; } - if (item.Condition <= 0.0f) { return; } - - lastPowerProbeRecipients.Add(this); - - if (power < 0.0f) - { - powerLoad -= power; - } - else - { - currPowerConsumption -= power; - } - powerOut?.SendPowerProbeSignal(source, power); - } - public override void ReceiveSignal(Signal signal, Connection connection) { if (item.Condition <= 0.0f || connection.IsPower) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 07e8ab3f3..7d0e21ff8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -8,10 +8,57 @@ using Barotrauma.Sounds; namespace Barotrauma.Items.Components { + /// + /// Order in which power sources will provide to a grid, lower number is higher priority + /// + public enum PowerPriority + { + Default = 0, // Use for status effects and/or extraload + Reactor = 1, + Relay = 3, + Battery = 5 + } + + readonly struct PowerRange + { + public readonly static PowerRange Zero = default; + + public readonly float Min; + public readonly float Max; + + /// + /// Used by reactors to communicate their maximum output to each other so they can divide the grid load between each other in a sensible way + /// + public readonly float ReactorMaxOutput; + + public PowerRange(float min, float max) : this(min, max, 0.0f) + { + } + + public PowerRange(float min, float max, float reactorMaxOutput) + { + System.Diagnostics.Debug.Assert(max >= min); + System.Diagnostics.Debug.Assert(min >= 0); + System.Diagnostics.Debug.Assert(max >= 0); + Min = min; + Max = max; + ReactorMaxOutput = reactorMaxOutput; + } + + public static PowerRange operator +(PowerRange a, PowerRange b) + { + return new PowerRange(a.Min + b.Min, a.Max + b.Max, a.ReactorMaxOutput + b.ReactorMaxOutput); + } + public static PowerRange operator -(PowerRange a, PowerRange b) + { + return new PowerRange(a.Min - b.Min, a.Max - b.Max, a.ReactorMaxOutput - b.ReactorMaxOutput); + } + } + partial class Powered : ItemComponent { - private static float updateTimer; - protected static float UpdateInterval = 0.2f; + //TODO: test sparser update intervals? + protected const float UpdateInterval = (float)Timing.Step; /// /// List of all powered ItemComponents @@ -22,10 +69,9 @@ namespace Barotrauma.Items.Components get { return poweredList; } } - /// - /// Items that have already received the "probe signal" that's used to distribute power and load across the grid - /// - protected static HashSet lastPowerProbeRecipients = new HashSet(); + public static readonly List ChangedConnections = new List(); + + public readonly static Dictionary Grids = new Dictionary(); /// /// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items @@ -49,7 +95,9 @@ namespace Barotrauma.Items.Components protected Connection powerIn, powerOut; - [Editable, Serialize(0.5f, true, description: "The minimum voltage required for the device to function. " + + protected virtual PowerPriority Priority { get { return PowerPriority.Default; } } + + [Editable, Serialize(0.5f, IsPropertySaveable.Yes, description: "The minimum voltage required for the device to function. " + "The voltage is calculated as power / powerconsumption, meaning that a device " + "with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")] public float MinVoltage @@ -58,14 +106,14 @@ namespace Barotrauma.Items.Components set { minVoltage = value; } } - [Editable, Serialize(0.0f, true, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How much power the device draws (or attempts to draw) from the electrical grid when active.")] public float PowerConsumption { get { return powerConsumption; } set { powerConsumption = value; } } - [Serialize(false, true, description: "Is the device currently active. Inactive devices don't consume power.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Is the device currently active. Inactive devices don't consume power.")] public override bool IsActive { get { return base.IsActive; } @@ -79,35 +127,63 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, true, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The current power consumption of the device. Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float CurrPowerConsumption { get {return currPowerConsumption; } set { currPowerConsumption = value; } } - [Serialize(0.0f, true, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The current voltage of the item (calculated as power consumption / available power). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")] public float Voltage { - get { return voltage; } - set { voltage = Math.Max(0.0f, value); } + get + { + if (powerIn != null) + { + if (powerIn?.Grid != null) { return powerIn.Grid.Voltage; } + } + else if (powerOut != null) + { + if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; } + } + return voltage; + } + set + { + if (powerIn != null) + { + if (powerIn.Grid != null) + { + powerIn.Grid.Voltage = Math.Max(0.0f, value); + } + } + else if (powerOut != null) + { + if (powerOut.Grid != null) + { + powerOut.Grid.Voltage = Math.Max(0.0f, value); + } + } + voltage = Math.Max(0.0f, value); + } } - [Editable, Serialize(true, true, description: "Can the item be damaged by electomagnetic pulses.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the item be damaged by electomagnetic pulses.")] public bool VulnerableToEMP { get; set; } - public Powered(Item item, XElement element) + public Powered(Item item, ContentXElement element) : base(item, element) { poweredList.Add(this); InitProjectSpecific(element); } - partial void InitProjectSpecific(XElement element); + partial void InitProjectSpecific(ContentXElement element); protected void UpdateOnActiveEffects(float deltaTime) { @@ -115,19 +191,19 @@ namespace Barotrauma.Items.Components { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - if (powerConsumption <= 0.0f) + if (PowerConsumption <= 0.0f) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } return; } - if (voltage > minVoltage) + if (Voltage > minVoltage) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } #if CLIENT - if (voltage > minVoltage) + if (Voltage > minVoltage) { if (!powerOnSoundPlayed && powerOnSound != null) { @@ -135,7 +211,7 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = true; } } - else if (voltage < 0.1f) + else if (Voltage < 0.1f) { powerOnSoundPlayed = false; } @@ -144,7 +220,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - currPowerConsumption = powerConsumption; UpdateOnActiveEffects(deltaTime); } @@ -163,6 +238,7 @@ namespace Barotrauma.Items.Components else if (c.Name == "power_out") { powerOut = c; + powerOut.Priority = Priority; } else if (c.Name == "power") { @@ -182,6 +258,7 @@ namespace Barotrauma.Items.Components #endif } powerOut = c; + powerOut.Priority = Priority; } else { @@ -199,104 +276,388 @@ namespace Barotrauma.Items.Components } } - public virtual void ReceivePowerProbeSignal(Connection connection, Item source, float power) { } + /// + /// Allocate electrical devices into their grids based on connections + /// + /// Use previous grids and change in connections + public static void UpdateGrids(bool useCache = true) + { + //don't use cache if there are no existing grids + if (Grids.Count > 0 && useCache) + { + //delete all grids that were affected + foreach (Connection c in ChangedConnections) + { + if (c.Grid != null) + { + Grids.Remove(c.Grid.ID); + c.Grid = null; + } + } + + foreach (Connection c in ChangedConnections) + { + //Make sure the connection grid hasn't been resolved by another connection update + //Ensure the connection has other connections + if (c.Grid == null && c.Recipients.Count > 0 && c.Item.Condition > 0.0f) + { + GridInfo grid = PropagateGrid(c); + Grids[grid.ID] = grid; + } + } + } + else + { + //Clear all grid IDs from connections + foreach (Powered powered in poweredList) + { + //Only check devices with connectors + if (powered.powerIn != null) + { + powered.powerIn.Grid = null; + } + if (powered.powerOut != null) + { + powered.powerOut.Grid = null; + } + } + + Grids.Clear(); + + foreach (Powered powered in poweredList) + { + //Probe through all connections that don't have a gridID + if (powered.powerIn != null && powered.powerIn.Grid == null && powered.powerIn != powered.powerOut && powered.Item.Condition > 0.0f) + { + // Only create grids for networks with more than 1 device + if (powered.powerIn.Recipients.Count > 0) + { + GridInfo grid = PropagateGrid(powered.powerIn); + Grids[grid.ID] = grid; + } + } + + if (powered.powerOut != null && powered.powerOut.Grid == null && powered.Item.Condition > 0.0f) + { + //Only create grids for networks with more than 1 device + if (powered.powerOut.Recipients.Count > 0) + { + GridInfo grid = PropagateGrid(powered.powerOut); + Grids[grid.ID] = grid; + } + } + } + } + + //Clear changed connections after each update + ChangedConnections.Clear(); + } + + private static GridInfo PropagateGrid(Connection conn) + { + //Generate unique Key + int id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced); + while (Grids.ContainsKey(id)) + { + id = Rand.Int(int.MaxValue, Rand.RandSync.Unsynced); + } + + return PropagateGrid(conn, id); + } + + private static GridInfo PropagateGrid(Connection conn, int gridID) + { + Stack probeStack = new Stack(); + + GridInfo grid = new GridInfo(gridID); + + probeStack.Push(conn); + + //Non recursive approach to traversing connection tree + while (probeStack.Count > 0) + { + Connection c = probeStack.Pop(); + c.Grid = grid; + grid.AddConnection(c); + + //Add on recipients + foreach (Connection otherC in c.Recipients) + { + //Only add valid connections + if (otherC.Grid != grid && (otherC.Grid == null || !Grids.ContainsKey(otherC.Grid.ID)) && ValidPowerConnection(c, otherC)) + { + if (otherC.Item.Condition <= 0.0f) + { + continue; + } + + otherC.Grid = grid; //Assigning ID early prevents unncessary adding to stack + probeStack.Push(otherC); + } + } + } + + return grid; + } + + /// + /// Update the power calculations of all devices and grids + /// Updates grids in the order of + /// ConnCurrConsumption - Get load of device/ flag it as an outputting connection + /// -- If outputting power -- + /// MinMaxPower - Minimum and Maximum power output of the connection for devices to coordinate + /// ConnPowerOut - Final power output based on the sum of the MinMaxPower + /// -- Finally -- + /// GridResolved - Indicate that a connection's grid has been finished being calculated + /// + /// Power outputting devices are calculated in stages based on their priority + /// Reactors will output first, followed by relays then batteries. + /// + /// + /// public static void UpdatePower(float deltaTime) { + //Don't update the power if the round is ending + if (GameMain.GameSession != null && GameMain.GameSession.RoundEnding) + { + return; + } + + //Only update the power at the given update interval + /* + //Not use currently as update interval of 1/60 if (updateTimer > 0.0f) { updateTimer -= deltaTime; return; } updateTimer = UpdateInterval; + */ - //reset power first - foreach (Powered powered in poweredList) +#if CLIENT + System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); + sw.Start(); +#endif + //Ensure all grids are updated correctly and have the correct connections + UpdateGrids(); + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("GridUpdate", sw.ElapsedTicks); + sw.Restart(); +#endif + + //Reset all grids + foreach (GridInfo grid in Grids.Values) { - if (powered is PowerTransfer pt) - { - powered.CurrPowerConsumption = 0.0f; - pt.PowerLoad = 0.0f; - if (pt is RelayComponent relay) - { - relay.DisplayLoad = 0.0f; - } - } - //only reset voltage if the item has a power connector - //(other items, such as handheld devices, get power through other means and shouldn't be updated here) - if (powered.powerIn != null || powered.powerOut != null) { powered.voltage = 0.0f; } + //Wipe priority groups as connections can change to not be outputting -- Can be improved caching wise -- + grid.PowerSourceGroups.Clear(); + grid.Power = 0; + grid.Load = 0; } - //go through all the devices that are consuming/providing power - //and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load + //Determine if devices are adding a load or providing power, also resolve solo nodes foreach (Powered powered in poweredList) { - if (powered is PowerTransfer pt) + //Handle the device if it's got a power connection + if (powered.powerIn != null && powered.powerOut != powered.powerIn) { - if (pt.ExtraLoad > 0.0f) - { - lastPowerProbeRecipients.Clear(); - powered.powerIn?.SendPowerProbeSignal(powered.item, -pt.ExtraLoad); + //Get the new load for the connection + float currLoad; + if (powered.Item.GetComponent() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices && !(powered is PowerContainer)) + { + currLoad = 0.0f; + } + else + { + currLoad = powered.GetCurrentPowerConsumption(powered.powerIn); } - continue; - } - else if (powered.currPowerConsumption > 0.0f) - { - //consuming power - lastPowerProbeRecipients.Clear(); - powered.powerIn?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption); - } - } - foreach (Powered powered in poweredList) - { - if (powered is PowerTransfer) { continue; } - else if (powered.currPowerConsumption < 0.0f) - { - //providing power - lastPowerProbeRecipients.Clear(); - powered.powerOut?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption); - } - if (powered is PowerContainer pc) - { - if (pc.CurrPowerOutput <= 0.0f || pc.item.Condition <= 0.0f) { continue; } - //providing power - lastPowerProbeRecipients.Clear(); - powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput); - } - } - //go through powered items and calculate their current voltage - foreach (Powered powered in poweredList) - { - if (powered is PowerTransfer pt1 || (pt1 = powered.Item.GetComponent()) != null) - { - powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f); - continue; - } - if ((powered.powerConsumption <= 0.0f || (powered.Item.GetComponent() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices)) && !(powered is PowerContainer)) - { - powered.voltage = 1.0f; - continue; - } - if (powered.powerIn == null) { continue; } - foreach (Connection powerSource in powered.powerIn.Recipients) - { - if (!powerSource.IsPower || !powerSource.IsOutput) { continue; } - var pt = powerSource.Item.GetComponent(); - if (pt != null) + //If its a load update its grid load + if (currLoad >= 0) { - float voltage = -pt.CurrPowerConsumption / Math.Max(pt.PowerLoad, 1.0f); - powered.voltage = Math.Max(powered.voltage, voltage); - continue; + powered.CurrPowerConsumption = currLoad; + if (powered.powerIn.Grid != null) + { + powered.powerIn.Grid.Load += currLoad; + } } - var pc = powerSource.Item.GetComponent(); - if (pc != null && pc.item.Condition > 0.0f) + else if (powered.powerIn.Grid != null) { - float voltage = pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f); - powered.voltage += voltage; + //If connected to a grid add as a source to be processed + powered.powerIn.Grid.AddSrc(powered.powerIn); + } + else + { + powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); + powered.GridResolved(powered.powerIn); + } + } + + //Handle the device power depending on if its powerout + if (powered.powerOut != null) + { + //Get the connection's load + float currLoad = powered.GetCurrentPowerConsumption(powered.powerOut); + + //Update the device's output load to the correct variable + if (powered is PowerTransfer pt) + { + pt.PowerLoad = currLoad; + } + else if (powered is PowerContainer pc) + { + // PowerContainer handle its own output value + } + else + { + powered.CurrPowerConsumption = currLoad; + } + + if (currLoad >= 0) + { + //Add to the grid load if possible + if (powered.powerOut.Grid != null) + { + powered.powerOut.Grid.Load += currLoad; + } + } + else if (powered.powerOut.Grid != null) + { + //Add connection as a source to be processed + powered.powerOut.Grid.AddSrc(powered.powerOut); + } + else + { + //Perform power calculations for the singular connection + float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); + if (powered is PowerTransfer pt2) + { + pt2.PowerLoad = loadOut; + } + else if (powered is PowerContainer pc) + { + //PowerContainer handles its own output value + } + else + { + powered.CurrPowerConsumption = loadOut; + } + + //Indicate grid is resolved as it was the only device + powered.GridResolved(powered.powerOut); } } } + + //Iterate through all grids to determine the power on the grid + foreach (GridInfo grid in Grids.Values) + { + //Iterate through the priority src groups lowest first + foreach (PowerSourceGroup scrGroup in grid.PowerSourceGroups.Values) + { + scrGroup.MinMaxPower = PowerRange.Zero; + + //Iterate through all connections in the group to get their minmax power and sum them + foreach (Connection c in scrGroup.Connections) + { + Powered device = c.Item.GetComponent(); + scrGroup.MinMaxPower += device.MinMaxPowerOut(c, grid.Load); + } + + //Iterate through all connections to get their final power out provided the min max information + float addedPower = 0; + foreach (Connection c in scrGroup.Connections) + { + Powered device = c.Item.GetComponent(); + addedPower += device.GetConnectionPowerOut(c, grid.Power, scrGroup.MinMaxPower, grid.Load); + } + + //Add the power to the grid + grid.Power += addedPower; + } + + //Calculate Grid voltage, limit between 0 - 1000 + float newVoltage = MathHelper.Min(grid.Power / MathHelper.Max(grid.Load, 1E-10f), 1000); + if (float.IsNegative(newVoltage)) + { + newVoltage = 0.0f; + } + + grid.Voltage = newVoltage; + + //Iterate through all connections on that grid and run their gridResolved function + foreach (Connection con in grid.Connections) + { + Powered device = con.Item.GetComponent(); + device.GridResolved(con); + } + } + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("PowerUpdate", sw.ElapsedTicks); +#endif + } + + /// + /// Current power consumption of the device (or amount of generated power if negative) + /// + /// Connection to calculate power consumption for. + public virtual float GetCurrentPowerConsumption(Connection connection = null) + { + // If a handheld device there is no consumption + if (powerIn == null && powerOut == null) + { + return 0; + } + + // Add extraload for PowerTransfer devices + if (this is PowerTransfer pt) + { + return PowerConsumption + pt.ExtraLoad; + } + else if (connection != this.powerIn || !IsActive) + { + //If not the power in connection or is inactive there is no draw + return 0; + } + + //Otherwise return the max powerconsumption of the device + return PowerConsumption; + } + + /// + /// Minimum and maximum power the connection can provide + /// + /// Connection being queried about its power capabilities + /// Load of the connected grid + public virtual PowerRange MinMaxPowerOut(Connection conn, float load = 0) + { + return PowerRange.Zero; + } + + /// + /// Finalize how much power the device will be outputting to the connection + /// + /// Connection being queried + /// Current grid power + /// Current load on the grid + /// Power pushed to the grid + public virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load) + { + return conn == powerOut ? MathHelper.Max(-CurrPowerConsumption, 0) : 0; + } + + /// + /// Can be overridden to perform updates for the device after the connected grid has resolved its power calculations, i.e. storing voltage for later updates + /// + public virtual void GridResolved(Connection conn) { } + + public static bool ValidPowerConnection(Connection conn1, Connection conn2) + { + return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock"))); } /// @@ -314,7 +675,6 @@ namespace Barotrauma.Items.Components if (!recipient.IsPower || !recipient.IsOutput) { continue; } var battery = recipient.Item?.GetComponent(); if (battery == null) { continue; } - float maxOutputPerFrame = battery.MaxOutPut / 60.0f; float framesPerMinute = 3600.0f; availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame); @@ -323,10 +683,119 @@ namespace Barotrauma.Items.Components return availablePower; } + /// + /// Efficient method to retrieve the batteries connected to the device + /// + /// All connected PowerContainers + protected List GetConnectedBatteries(bool outputOnly = true) + { + List batteries = new List(); + GridInfo supplyingGrid = null; + + //Determine supplying grid, prefer PowerIn connection + if (powerIn != null) + { + if (powerIn.Grid != null) + { + supplyingGrid = powerIn.Grid; + } + } + else if (powerOut != null) + { + if (powerOut.Grid != null) + { + supplyingGrid = powerOut.Grid; + } + } + + if (supplyingGrid != null) + { + //Iterate through all connections to fine powerContainers + foreach (Connection c in supplyingGrid.Connections) + { + PowerContainer pc = c.Item.GetComponent(); + if (pc != null && (!outputOnly || pc.powerOut == c)) + { + batteries.Add(pc); + } + } + } + + return batteries; + } + protected override void RemoveComponentSpecific() { + //Flag power connections to be updated + if (item.Connections != null) + { + foreach (Connection c in item.Connections) + { + if (c.IsPower && c.Grid != null) + { + ChangedConnections.Add(c); + } + } + } + base.RemoveComponentSpecific(); poweredList.Remove(this); } } + + partial class GridInfo + { + public readonly int ID; + public float Voltage = 0; + public float Load = 0; + public float Power = 0; + + public readonly List Connections = new List(); + public readonly SortedList PowerSourceGroups = new SortedList(); + + public GridInfo(int id) + { + ID = id; + } + + public void RemoveConnection(Connection c) + { + Connections.Remove(c); + + //Remove the grid if it has no devices + if (Connections.Count == 0 && Powered.Grids.ContainsKey(ID)) + { + Powered.Grids.Remove(ID); + } + } + + public void AddConnection(Connection c) + { + Connections.Add(c); + } + + public void AddSrc(Connection c) + { + if (PowerSourceGroups.ContainsKey(c.Priority)) + { + PowerSourceGroups[c.Priority].Connections.Add(c); + } + else + { + PowerSourceGroup group = new PowerSourceGroup(); + group.Connections.Add(c); + PowerSourceGroups[c.Priority] = group; + } + } + } + + partial class PowerSourceGroup + { + public PowerRange MinMaxPower; + public readonly List Connections = new List(); + + public PowerSourceGroup() + { + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fb4d7bbd1..2890cc214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -88,13 +88,13 @@ namespace Barotrauma.Items.Components private float persistentStickJointTimer; - [Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] + [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } - [Serialize(0.0f, false, description: "The random percentage modifier used to add variance to the launch impulse.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The random percentage modifier used to add variance to the launch impulse.")] public float ImpulseSpread { get; set; } - [Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] public float LaunchRotation { @@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components private set; } - [Serialize(false, false, description: "When set to true, the item can stick to any target it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item can stick to any target it hits.")] //backwards compatibility, can stick to anything public bool DoesStick { @@ -116,50 +116,50 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] + [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] public bool StickPermanently { get; set; } - [Serialize(false, false, description: "Can the item stick to the character it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { get; set; } - [Serialize(false, false, description: "Can the item stick to the structure it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")] public bool StickToStructures { get; set; } - [Serialize(false, false, description: "Can the item stick to the item it hits.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")] public bool StickToItems { get; set; } - [Serialize(false, false, description: "Can the item stick even to deflective targets.")] + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")] public bool StickToDeflective { get; set; } - [Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ - "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] + [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ + "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan { get; set; } - [Serialize(1, false, description: "How many hitscans should be done when the projectile is launched. " + [Serialize(1, IsPropertySaveable.No, description: "How many hitscans should be done when the projectile is launched. " + "Multiple hitscans can be used to simulate weapons that fire multiple projectiles at the same time" + " without having to actually use multiple projectile items, for example shotguns.")] public int HitScanCount @@ -168,28 +168,28 @@ namespace Barotrauma.Items.Components set; } - [Serialize(1, false, description: "How many targets the projectile can hit before it stops.")] + [Serialize(1, IsPropertySaveable.No, description: "How many targets the projectile can hit before it stops.")] public int MaxTargetsToHit { get; set; } - [Serialize(false, false, description: "Should the item be deleted when it hits something.")] + [Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it hits something.")] public bool RemoveOnHit { get; set; } - [Serialize(0.0f, false, description: "Random spread applied to the launch angle of the projectile (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the launch angle of the projectile (in degrees).")] public float Spread { get; set; } - [Serialize(false, false, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] + [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; hitscan are launched with an equal amount of angle between them. Only applies when firing multiple hitscan.")] public bool StaticSpread { get; @@ -198,7 +198,7 @@ namespace Barotrauma.Items.Components private float deactivationTimer; - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float DeactivationTime { get; @@ -219,19 +219,19 @@ namespace Barotrauma.Items.Components private Category originalCollisionCategories; private Category originalCollisionTargets; - public Projectile(Item item, XElement element) + public Projectile(Item item, ContentXElement element) : base (item, element) { IgnoredBodies = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } Attack = new Attack(subElement, item.Name + ", Projectile", item); } InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { @@ -484,7 +484,7 @@ namespace Barotrauma.Items.Components } else { - Entity.Spawner.AddToRemoveQueue(item); + Entity.Spawner.AddItemToRemoveQueue(item); } } } @@ -697,27 +697,9 @@ namespace Barotrauma.Items.Components return false; } if (hits.Contains(target.Body)) { return false; } - if (target.Body.UserData is Submarine sub) + if (ShouldIgnoreSubmarineCollision(target, contact)) { - Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? - contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); - - //do a raycast in the sub's coordinate space to see if it hit a structure - var wallBody = Submarine.PickBody( - item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, - item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, - collisionCategory: Physics.CollisionWall); - if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && - //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction - Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) - { - target = wallBody.FixtureList.First(); - if (hits.Contains(target.Body)) { return false; } - } - else - { - return false; - } + return false; } else if (target.Body.UserData is Limb limb) { @@ -757,6 +739,44 @@ namespace Barotrauma.Items.Components } } + /// + /// Should the collision with the target submarine be ignored (e.g. did the projectile collide with the wall behind the turret when being launched) + /// + /// Fixture the projectile hit + /// Contact between the projectile and the target + /// True if the target isn't a submarine or if the collision happened behind the launch position of the projectile + public bool ShouldIgnoreSubmarineCollision(Fixture target, Contact contact) + { + return ShouldIgnoreSubmarineCollision(ref target, contact); + } + + private bool ShouldIgnoreSubmarineCollision(ref Fixture target, Contact contact) + { + if (target.Body.UserData is Submarine sub) + { + Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? + contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity); + + //do a raycast in the sub's coordinate space to see if it hit a structure + var wallBody = Submarine.PickBody( + item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, + item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, + collisionCategory: Physics.CollisionWall); + if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) && + //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction + Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) + { + target = wallBody.FixtureList.First(); + if (hits.Contains(target.Body)) { return true; } + } + else + { + return true; + } + } + return false; + } + private readonly List targets = new List(); private Fixture lastTarget; @@ -961,7 +981,7 @@ namespace Barotrauma.Items.Components removePending = true; item.HiddenInGame = true; item.body.FarseerBody.Enabled = false; - Entity.Spawner?.AddToRemoveQueue(item); + Entity.Spawner?.AddItemToRemoveQueue(item); } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 090028dc7..0be5f9561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components private int qualityLevel; - [Editable, Serialize(0, true)] + [Editable, Serialize(0, IsPropertySaveable.Yes)] public int QualityLevel { get { return qualityLevel; } @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components } } - public Quality(Item item, XElement element) : base(item, element) + public Quality(Item item, ContentXElement element) : base(item, element) { foreach (XElement subElement in element.Elements()) { @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components string statTypeString = subElement.GetAttributeString("stattype", ""); if (!Enum.TryParse(statTypeString, true, out StatType statType)) { - DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + item.prefab.Identifier + ")"); + DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in item (" + ((MapEntity)item).Prefab.Identifier + ")"); } float statValue = subElement.GetAttributeFloat("value", 0f); statValues.TryAdd(statType, statValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs index 3643d43b1..d457cb589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -5,21 +5,21 @@ namespace Barotrauma.Items.Components { partial class RemoteController : ItemComponent { - [Serialize("", false, description: "Tag or identifier of the item that should be controlled.")] + [Serialize("", IsPropertySaveable.No, description: "Tag or identifier of the item that should be controlled.")] public string Target { get; private set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool OnlyInOwnSub { get; private set; } - [Serialize(10000.0f, false)] + [Serialize(10000.0f, IsPropertySaveable.No)] public float Range { get; @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components private Character currentUser; private Submarine currentSub; - public RemoteController(Item item, XElement element) + public RemoteController(Item item, ContentXElement element) : base(item, element) { } @@ -81,7 +81,7 @@ namespace Barotrauma.Items.Components if (targetItem.Submarine != item.Submarine) { continue; } if (targetItem.Submarine.TeamID != user.TeamID) { continue; } } - if (!targetItem.HasTag(Target) && targetItem.prefab.Identifier != Target) { continue; } + if (!targetItem.HasTag(Target) && ((MapEntity)targetItem).Prefab.Identifier != Target) { continue; } float distSqr = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); if (distSqr > Range * Range || distSqr > closestDist) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 2893f94f1..3e1439340 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IServerSerializable, IClientSerializable { - private readonly string header; + private readonly LocalizedString header; private float deteriorationTimer; private float deteriorateAlwaysResetTimer; @@ -24,63 +24,63 @@ namespace Barotrauma.Items.Components public float LastActiveTime; - [Serialize(0.0f, true, description: "How fast the condition of the item deteriorates per second."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the condition of the item deteriorates per second."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2)] public float DeteriorationSpeed { get; set; } - [Serialize(0.0f, true, description: "Minimum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] public float MinDeteriorationDelay { get; set; } - [Serialize(0.0f, true, description: "Maximum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Maximum initial delay before the item starts to deteriorate."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, DecimalCount = 2)] public float MaxDeteriorationDelay { get; set; } - [Serialize(50.0f, true, description: "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors). Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(50.0f, IsPropertySaveable.Yes, description: "The item won't deteriorate spontaneously if the condition is below this value. For example, if set to 10, the condition will spontaneously drop to 10 and then stop dropping (unless the item is damaged further by external factors). Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinDeteriorationCondition { get; set; } - [Serialize(0f, true, description: "How low a traitor must get the item's condition for it to start breaking down.")] + [Serialize(0f, IsPropertySaveable.Yes, description: "How low a traitor must get the item's condition for it to start breaking down.")] public float MinSabotageCondition { get; set; } - [Serialize(80.0f, true, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(80.0f, IsPropertySaveable.Yes, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float RepairThreshold { get; set; } - [Serialize(100.0f, true, description: "The amount of time it takes to fix the item with insufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(100.0f, IsPropertySaveable.Yes, description: "The amount of time it takes to fix the item with insufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float FixDurationLowSkill { get; set; } - [Serialize(10.0f, true, description: "The amount of time it takes to fix the item with sufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "The amount of time it takes to fix the item with sufficient skill levels."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float FixDurationHighSkill { get; set; } - [Serialize(false, false, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] + [Serialize(false, IsPropertySaveable.No, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")] public bool DeteriorateAlways { get; @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components private float skillRequirementMultiplier; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float SkillRequirementMultiplier { get { return skillRequirementMultiplier; } @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components private set { currentFixerAction = value; } } - public Repairable(Item item, XElement element) + public Repairable(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -145,9 +145,9 @@ namespace Barotrauma.Items.Components this.item = item; header = - TextManager.Get(element.GetAttributeString("header", ""), returnNull: true) ?? - TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""), returnNull: true) ?? - element.GetAttributeString("name", ""); + TextManager.Get(element.GetAttributeString("header", "")).Fallback( + TextManager.Get(item.Prefab.ConfigElement.GetAttributeString("header", ""))).Fallback( + element.GetAttributeString("name", "")); //backwards compatibility var repairThresholdAttribute = @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); /// /// Check if the character manages to succesfully repair the item @@ -183,7 +183,7 @@ namespace Barotrauma.Items.Components if (bestRepairItem != null && bestRepairItem.Prefab.CannotRepairFail) { return true; } // unpowered (electrical) items can be repaired without a risk of electrical shock - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) { if (item.GetComponent() is Reactor reactor) { @@ -244,6 +244,11 @@ namespace Barotrauma.Items.Components } else { + if (CurrentFixerAction == FixActions.Tinker && action != FixActions.Tinker) + { + CurrentFixer?.CheckTalents(AbilityEffectType.OnStopTinkering); + } + Item bestRepairItem = GetBestRepairItem(character); #if SERVER if (CurrentFixer != character || currentFixerAction != action) @@ -260,8 +265,13 @@ namespace Barotrauma.Items.Components return false; } - GameServer.Log($"{GameServer.CharacterLogName(character)} started {(action == FixActions.Sabotage ? "sabotaging" : "repairing")} {item.Name}", ServerLog.MessageType.ItemInteraction); - item.CreateServerEvent(this); + if ((character != prevLoggedFixer || action != prevLoggedFixAction) && (character.TeamID == CharacterTeamType.Team1 || character.TeamID == CharacterTeamType.Team2)) + { + GameServer.Log($"{GameServer.CharacterLogName(character)} started {(action == FixActions.Sabotage ? "sabotaging" : "repairing")} {item.Name}", ServerLog.MessageType.ItemInteraction); + item.CreateServerEvent(this); + prevLoggedFixer = character; + prevLoggedFixAction = action; + } } #else if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character, bestRepairItem)) { return false; } @@ -442,7 +452,7 @@ namespace Barotrauma.Items.Components float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); - + item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); if (currentFixerAction == FixActions.Repair) @@ -478,6 +488,10 @@ namespace Barotrauma.Items.Components deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); wasBroken = false; StopRepairing(CurrentFixer); +#if SERVER + prevLoggedFixer = null; + prevLoggedFixAction = FixActions.None; +#endif } } else if (currentFixerAction == FixActions.Sabotage) @@ -523,11 +537,11 @@ namespace Barotrauma.Items.Components { if (character == null) { return 1.0f; } // kind of rough to keep this in update, but seems most robust - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical", StringComparison.OrdinalIgnoreCase))) + if (requiredSkills.Any(s => s != null && s.Identifier == "mechanical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical); } - if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + if (requiredSkills.Any(s => s != null && s.Identifier == "electrical")) { return 1 + character.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 432e96f31..7c5b320ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -18,56 +18,56 @@ namespace Barotrauma.Items.Components private float raycastTimer; private const float RayCastInterval = 0.2f; - [Serialize(0.0f, false, description: "How much force is applied to pull the projectile the rope is attached to.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the projectile the rope is attached to.")] public float ProjectilePullForce { get; set; } - [Serialize(0.0f, false, description: "How much force is applied to pull the target the rope is attached to.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the target the rope is attached to.")] public float TargetPullForce { get; set; } - [Serialize(0.0f, false, description: "How much force is applied to pull the source the rope is attached to.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the source the rope is attached to.")] public float SourcePullForce { get; set; } - [Serialize(1000.0f, false, description: "How far the source item can be from the projectile until the rope breaks.")] + [Serialize(1000.0f, IsPropertySaveable.No, description: "How far the source item can be from the projectile until the rope breaks.")] public float MaxLength { get; set; } - [Serialize(true, false, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] public bool SnapOnCollision { get; set; } - [Serialize(true, false, description: "Should the rope snap when the character drops the aim?")] + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the character drops the aim?")] public bool SnapWhenNotAimed { get; set; } - [Serialize(30.0f, false, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")] + [Serialize(30.0f, IsPropertySaveable.No, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")] public float TargetMinMass { get; set; } - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool LerpForces { get; @@ -102,12 +102,12 @@ namespace Barotrauma.Items.Components } } - public Rope(Item item, XElement element) : base(item, element) + public Rope(Item item, ContentXElement element) : base(item, element) { InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public void Snap() => Snapped = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs index 9c4998801..1d592e015 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs @@ -5,9 +5,9 @@ namespace Barotrauma.Items.Components { partial class Scanner : ItemComponent { - [Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How long it takes for the scan to be completed.")] public float ScanDuration { get; set; } - [Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")] + [Serialize(0.0f, IsPropertySaveable.No, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")] public float ScanTimer { get @@ -33,9 +33,9 @@ namespace Barotrauma.Items.Components #endif } } - [Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How far the scanner can be from the target for the scan to be successful.")] public float ScanRadius { get; set; } - [Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")] + [Serialize(true, IsPropertySaveable.No, description: "Should the progress bar always be displayed when the item has been attached.")] public bool AlwaysDisplayProgressBar { get; set; } private Holdable Holdable { get; set; } @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components public Action OnScanStarted, OnScanCompleted; - public Scanner(Item item, XElement element) : base(item, element) + public Scanner(Item item, ContentXElement element) : base(item, element) { IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs index 6fa6e8f2c..cc0e5c4b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AdderComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class AdderComponent : ArithmeticComponent { - public AdderComponent(Item item, XElement element) + public AdderComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index af4c47b13..22ceb46ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components protected readonly Character[] signalSender = new Character[2]; - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components } private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - public AndComponent(Item item, XElement element) + public AndComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index ecf63774b..c31b4ff6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [Serialize(999999.0f, true, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), + [Serialize(999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax { @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true), + [Serialize(-999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMin { @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components } [InGameEditable(DecimalCount = 2), - Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components } } - public ArithmeticComponent(Item item, XElement element) + public ArithmeticComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index 38489ca15..b9dd02749 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -9,9 +9,9 @@ namespace Barotrauma.Items.Components { partial class ButtonTerminal : ItemComponent { - [Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)] + [Editable, Serialize(new string[0], IsPropertySaveable.Yes, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)] public string[] Signals { get; set; } - [Editable, Serialize("", true, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)] + [Editable, Serialize("", IsPropertySaveable.Yes, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)] public string ActivatingItems { get; set; } private int RequiredSignalCount { get; set; } @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab))); - public ButtonTerminal(Item item, XElement element) : base(item, element) + public ButtonTerminal(Item item, ContentXElement element) : base(item, element) { RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); if (RequiredSignalCount < 1) @@ -31,7 +31,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public override void OnItemLoaded() { @@ -77,7 +77,7 @@ namespace Barotrauma.Items.Components } else { - ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t.Equals(activatingItem, StringComparison.OrdinalIgnoreCase))) + ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t == activatingItem)) .ForEach(p => ActivatingItemPrefabs.Add(p)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs index d3d41c271..fb610a6ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ColorComponent.cs @@ -11,10 +11,10 @@ namespace Barotrauma.Items.Components private string output = "0,0,0,0"; - [InGameEditable, Serialize(false, true, description: "When enabled makes the component translate the signal from HSV into RGB where red is the hue between 0 and 360, green is the saturation between 0 and 1 and blue is the value between 0 and 1.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled makes the component translate the signal from HSV into RGB where red is the hue between 0 and 360, green is the saturation between 0 and 1 and blue is the value between 0 and 1.", alwaysUseInstanceValues: true)] public bool UseHSV { get; set; } - public ColorComponent(Item item, XElement element) + public ColorComponent(Item item, ContentXElement element) : base(item, element) { receivedSignal = new float[4]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs index afcf91f2d..81ad65d84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { private int maxOutputLength; - [Editable, Serialize(256, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking load.")] + [Editable, Serialize(256, IsPropertySaveable.No, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking load.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -17,14 +17,14 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize("", false)] + [InGameEditable, Serialize("", IsPropertySaveable.No)] public string Separator { get; set; } - public ConcatComponent(Item item, XElement element) + public ConcatComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 185d8a158..f9c057603 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components public readonly int MaxWires = 5; public readonly string Name; - public readonly string DisplayName; + public readonly LocalizedString DisplayName; private readonly Wire[] wires; public IEnumerable Wires @@ -33,6 +33,12 @@ namespace Barotrauma.Items.Components public readonly ushort[] wireId; + //The grid the connection is a part of + public GridInfo Grid; + + //Priority in which power output will be handled - load is unaffected + public PowerPriority Priority = PowerPriority.Default; + public bool IsPower { get; @@ -40,7 +46,7 @@ namespace Barotrauma.Items.Components } private bool recipientsDirty = true; - private List recipients = new List(); + private readonly List recipients = new List(); public List Recipients { get @@ -66,17 +72,17 @@ namespace Barotrauma.Items.Components return "Connection (" + item.Name + ", " + Name + ")"; } - public Connection(XElement element, ConnectionPanel connectionPanel, IdRemap idRemap) + public Connection(ContentXElement element, ConnectionPanel connectionPanel, IdRemap idRemap) { #if CLIENT if (connector == null) { - connector = GUI.Style.GetComponentStyle("ConnectionPanelConnector").GetDefaultSprite(); - wireVertical = GUI.Style.GetComponentStyle("ConnectionPanelWire").GetDefaultSprite(); - connectionSprite = GUI.Style.GetComponentStyle("ConnectionPanelConnection").GetDefaultSprite(); - connectionSpriteHighlight = GUI.Style.GetComponentStyle("ConnectionPanelConnection").GetSprite(GUIComponent.ComponentState.Hover); - screwSprites = GUI.Style.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList(); + connector = GUIStyle.GetComponentStyle("ConnectionPanelConnector").GetDefaultSprite(); + wireVertical = GUIStyle.GetComponentStyle("ConnectionPanelWire").GetDefaultSprite(); + connectionSprite = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetDefaultSprite(); + connectionSpriteHighlight = GUIStyle.GetComponentStyle("ConnectionPanelConnection").GetSprite(GUIComponent.ComponentState.Hover); + screwSprites = GUIStyle.GetComponentStyle("ConnectionPanelScrew").Sprites[GUIComponent.ComponentState.None].Select(s => s.Sprite).ToList(); } #endif ConnectionPanel = connectionPanel; @@ -95,7 +101,7 @@ namespace Barotrauma.Items.Components //if displayname is not present, attempt to find it from the prefab if (element.Attribute("displayname") == null) { - foreach (XElement subElement in item.Prefab.ConfigElement.Elements()) + foreach (var subElement in item.Prefab.ConfigElement.Elements()) { if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -132,7 +138,7 @@ namespace Barotrauma.Items.Components } } - if (string.IsNullOrEmpty(DisplayName)) + if (DisplayName.IsNullOrEmpty()) { #if DEBUG DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name); @@ -145,7 +151,7 @@ namespace Barotrauma.Items.Components wireId = new ushort[MaxWires]; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -236,6 +242,34 @@ namespace Barotrauma.Items.Components 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; } } @@ -244,10 +278,32 @@ namespace Barotrauma.Items.Components recipientsDirty = true; if (wire != 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)) + { + 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.recipientsDirty = true; } } @@ -282,20 +338,17 @@ namespace Barotrauma.Items.Components } } - public void SendPowerProbeSignal(Item source, float power) - { - for (int i = 0; i < MaxWires; i++) - { - if (wires[i] == null) { continue; } - - Connection recipient = wires[i].OtherConnection(this); - if (recipient == null || !recipient.IsPower) { continue; } - - recipient.item.GetComponent()?.ReceivePowerProbeSignal(recipient, source, power); - } - } public void ClearConnections() { + if (IsPower && Grid != null) + { + Powered.ChangedConnections.Add(this); + foreach (Connection c in recipients) + { + Powered.ChangedConnections.Add(c); + } + } + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 92103f633..48258b2ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; @@ -39,7 +40,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] public bool Locked { get; @@ -63,12 +64,12 @@ namespace Barotrauma.Items.Components get { return user; } } - public ConnectionPanel(Item item, XElement element) + public ConnectionPanel(Item item, ContentXElement element) : base(item, element) { Connections = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString()) { @@ -264,13 +265,13 @@ namespace Barotrauma.Items.Components return false; } - public override void Load(XElement element, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement element, bool usePrefabValues, IdRemap idRemap) { base.Load(element, usePrefabValues, idRemap); List loadedConnections = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString()) { @@ -306,7 +307,7 @@ namespace Barotrauma.Items.Components } } - disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", new ushort[0]).ToList(); + disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty()).ToList(); for (int i = 0; i < disconnectedWireIds.Count; i++) { disconnectedWireIds[i] = idRemap.GetOffsetId(disconnectedWireIds[i]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 8c8ed6036..fb9b68e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -14,13 +15,13 @@ namespace Barotrauma.Items.Components public string ConnectionName; public Connection Connection; - [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] + [Serialize("", IsPropertySaveable.No, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } - [Serialize("1", false, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] + [Serialize("1", IsPropertySaveable.No, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] public string Signal { get; set; } - public string PropertyName { get; } + public Identifier PropertyName { get; } public bool TargetOnlyParentProperty { get; } public int NumberInputMin { get; } @@ -35,7 +36,7 @@ namespace Barotrauma.Items.Components public string Name => "CustomInterfaceElement"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } public List StatusEffects = new List(); @@ -43,16 +44,16 @@ namespace Barotrauma.Items.Components /// Pass the parent component to the constructor to access the serializable properties /// for elements which change property values. /// - public CustomInterfaceElement(XElement element, CustomInterface parent) + public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent) { Label = element.GetAttributeString("text", ""); ConnectionName = element.GetAttributeString("connection", ""); - PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant(); + PropertyName = element.GetAttributeIdentifier("propertyname", ""); TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin); NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax); MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue); - HasPropertyName = !string.IsNullOrEmpty(PropertyName); + HasPropertyName = !PropertyName.IsEmpty; IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; if (element.Attribute("signal") is XAttribute attribute) @@ -84,7 +85,7 @@ namespace Barotrauma.Items.Components Signal = "1"; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase)) { @@ -95,7 +96,7 @@ namespace Barotrauma.Items.Components } private string[] labels; - [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] + [Serialize("", IsPropertySaveable.Yes, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] public string Labels { get { return string.Join(",", labels); } @@ -104,14 +105,14 @@ namespace Barotrauma.Items.Components if (value == null) { return; } if (customInterfaceElementList.Count > 0) { - string[] splitValues = value == "" ? new string[0] : value.Split(','); + string[] splitValues = value == "" ? Array.Empty() : value.Split(','); UpdateLabels(splitValues); } } } private string[] signals; - [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.", alwaysUseInstanceValues: true)] + [Serialize("", IsPropertySaveable.Yes, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.", alwaysUseInstanceValues: true)] public string Signals { //use semicolon as a separator because comma may be needed in the signals (for color or vector values for example) @@ -122,7 +123,7 @@ namespace Barotrauma.Items.Components if (value == null) { return; } if (customInterfaceElementList.Count > 0) { - string[] splitValues = value == "" ? new string[0] : value.Split(';'); + string[] splitValues = value == "" ? Array.Empty() : value.Split(';'); UpdateSignals(splitValues); } } @@ -132,17 +133,17 @@ namespace Barotrauma.Items.Components private readonly List customInterfaceElementList = new List(); - public CustomInterface(Item item, XElement element) + public CustomInterface(Item item, ContentXElement element) : base(item, element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "button": case "textbox": case "integerinput": - var button = new CustomInterfaceElement(subElement, this) + var button = new CustomInterfaceElement(item, subElement, this) { ContinuousSignal = false }; @@ -153,7 +154,7 @@ namespace Barotrauma.Items.Components customInterfaceElementList.Add(button); break; case "tickbox": - var tickBox = new CustomInterfaceElement(subElement, this) + var tickBox = new CustomInterfaceElement(item, subElement, this) { ContinuousSignal = true }; @@ -179,7 +180,7 @@ namespace Barotrauma.Items.Components labels[i] = i < newLabels.Length ? newLabels[i] : customInterfaceElementList[i].Label; if (Screen.Selected != GameMain.SubEditorScreen) { - customInterfaceElementList[i].Label = TextManager.Get(labels[i], returnNull: true) ?? labels[i]; + customInterfaceElementList[i].Label = TextManager.Get(labels[i]).Fallback(labels[i]).Value; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index c0c7c2872..d196c1b03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private DelayedSignal prevQueuedSignal; private float delay; - [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)] + [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, IsPropertySaveable.Yes, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)] public float Delay { get { return delay; } @@ -44,21 +44,21 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)] public bool ResetWhenSignalReceived { get; set; } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)] public bool ResetWhenDifferentSignalReceived { get; set; } - public DelayComponent(Item item, XElement element) + public DelayComponent(Item item, ContentXElement element) : base (item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs index e4efa15d5..8c35c711e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DivideComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class DivideComponent : ArithmeticComponent { - public DivideComponent(Item item, XElement element) + public DivideComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 4ffc063f7..892a3afef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components protected float timeFrame; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } @@ -72,7 +72,7 @@ namespace Barotrauma.Items.Components } } - public EqualsComponent(Item item, XElement element) + public EqualsComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs index 8ea0ca87b..d2eeddbad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ExponentiationComponent : ItemComponent { private float exponent; - [InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(1.0f, IsPropertySaveable.No, description: "The exponent of the operation.", alwaysUseInstanceValues: true)] public float Exponent { get @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components } } - public ExponentiationComponent(Item item, XElement element) + public ExponentiationComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index 8d464e70a..974cac797 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -16,13 +16,13 @@ namespace Barotrauma.Items.Components SquareRoot } - [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] + [Serialize(FunctionType.Round, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; } - public FunctionComponent(Item item, XElement element) + public FunctionComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs index 0f15476c1..864046385 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { private float val1, val2; - public GreaterComponent(Item item, XElement element) + public GreaterComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index c11c33e5f..ef5d693af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private Turret turret; - [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), + [Serialize(100.0f, IsPropertySaveable.Yes, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { @@ -56,7 +56,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " + + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should structures cast shadows when light from this light source hits them. " + "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)] public bool CastShadows { @@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " + + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " + "It's recommended to enable this on decorative lights outside the submarine's hull.", alwaysUseInstanceValues: true)] public bool DrawBehindSubs { @@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Is the light currently on.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the light currently on.", alwaysUseInstanceValues: true)] public bool IsOn { get { return isOn; } @@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] public float Flicker { get { return flicker; } @@ -111,7 +111,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + [Editable, Serialize(1.0f, IsPropertySaveable.No, description: "How fast the light flickers.")] public float FlickerSpeed { get { return flickerSpeed; } @@ -124,7 +124,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, true, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] public float PulseFrequency { get { return pulseFrequency; } @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components } } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, true, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] public float PulseAmount { get { return pulseAmount; } @@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { get { return blinkFrequency; } @@ -163,7 +163,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable(FallBackTextTag = "connection.setcolor"), Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).", alwaysUseInstanceValues: true)] + [InGameEditable(FallBackTextTag = "connection.setcolor"), Serialize("255,255,255,255", IsPropertySaveable.Yes, description: "The color of the emitted light (R,G,B,A).", alwaysUseInstanceValues: true)] public Color LightColor { get { return lightColor; } @@ -179,7 +179,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(false, false, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")] public bool IgnoreContinuousToggle { get; @@ -208,7 +208,7 @@ namespace Barotrauma.Items.Components } } - public LightComponent(Item item, XElement element) + public LightComponent(Item item, ContentXElement element) : base(item, element) { #if CLIENT @@ -264,8 +264,6 @@ namespace Barotrauma.Items.Components } UpdateOnActiveEffects(deltaTime); - if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } - #if CLIENT Light.ParentSub = item.Submarine; #endif @@ -284,7 +282,7 @@ namespace Barotrauma.Items.Components return; } - currPowerConsumption = powerConsumption; + //currPowerConsumption = powerConsumption; if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage)) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index 39ee77a58..6fc568545 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components partial class MemoryComponent : ItemComponent, IServerSerializable { private int maxValueLength; - [Editable, Serialize(200, false, description: "The maximum length of the stored value. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the stored value. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxValueLength { get { return maxValueLength; } @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private string value; - [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] public string Value { get { return value; } @@ -34,14 +34,14 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(true, true, description: "Can the value stored in the memory component be changed via signals.", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the value stored in the memory component be changed via signals.", alwaysUseInstanceValues: true)] public bool Writeable { get; set; } - public MemoryComponent(Item item, XElement element) + public MemoryComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs index 2d8985857..aa7bcfdcb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ModuloComponent : ItemComponent { private float modulus; - [InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(1.0f, IsPropertySaveable.No, description: "The modulus of the operation. Must be non-zero.", alwaysUseInstanceValues: true)] public float Modulus { get { return modulus; } @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components } } - public ModuloComponent(Item item, XElement element) : base(item, element) + public ModuloComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 3c4db92a5..111ed4ea2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -22,17 +22,17 @@ namespace Barotrauma.Items.Components Wall } - [Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] + [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).")] public bool MotionDetected { get; set; } - [InGameEditable, Serialize(TargetType.Any, true, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)] public TargetType Target { get; set; } - [InGameEditable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] public bool IgnoreDead { get; @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } - [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)] public float RangeX { get { return rangeX; } @@ -52,7 +52,7 @@ namespace Barotrauma.Items.Components #endif } } - [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)] public float RangeY { get { return rangeY; } @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("0,0", true, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] + [InGameEditable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] public Vector2 DetectOffset { get { return detectOffset; } @@ -85,7 +85,7 @@ namespace Barotrauma.Items.Components } } - [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, true, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, IsPropertySaveable.Yes, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] public float UpdateInterval { get; @@ -93,7 +93,7 @@ namespace Barotrauma.Items.Components } private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -120,7 +120,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -135,21 +135,21 @@ namespace Barotrauma.Items.Components } } - [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] + [Editable(DecimalCount = 3), Serialize(0.01f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; set; } - [Serialize(true, true, description: "Should the sensor trigger when the item itself moves.")] + [Serialize(true, IsPropertySaveable.Yes, description: "Should the sensor trigger when the item itself moves.")] public bool DetectOwnMotion { get; set; } - public MotionSensor(Item item, XElement element) + public MotionSensor(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components updateTimer = Rand.Range(0.0f, UpdateInterval); } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs index 5f671aa21..69d75613f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MultiplyComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class MultiplyComponent : ArithmeticComponent { - public MultiplyComponent(Item item, XElement element) + public MultiplyComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs index 99e3cc3e2..6f6059251 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs @@ -7,14 +7,14 @@ namespace Barotrauma.Items.Components private bool signalReceived; private bool continuousOutput; - [Editable, Serialize(false, true, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get { return continuousOutput; } set { continuousOutput = IsActive = value; } } - public NotComponent(Item item, XElement element) + public NotComponent(Item item, ContentXElement element) : base (item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs index 8596b8070..3d3c7ab9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class OrComponent : AndComponent { - public OrComponent(Item item, XElement element) + public OrComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index 6d92474fe..4c4493ead 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components private float phase; - [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + + [InGameEditable, Serialize(WaveType.Pulse, IsPropertySaveable.Yes, description: "What kind of a signal the item outputs." + " Pulse: periodically sends out a signal of 1." + " Sawtooth: sends out a periodic wave that increases linearly from 0 to 1." + " Sine: sends out a sine wave oscillating between -1 and 1." + @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components set; } - [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).", alwaysUseInstanceValues: true)] + [InGameEditable(DecimalCount = 2), Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).", alwaysUseInstanceValues: true)] public float Frequency { get { return frequency; } @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components } } - public OscillatorComponent(Item item, XElement element) : + public OscillatorComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs index 9cc1020e7..146019cdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs @@ -4,10 +4,12 @@ namespace Barotrauma.Items.Components { class OxygenDetector : ItemComponent { + public const int LowOxygenPercentage = 35; + private int prevSentOxygenValue; private string oxygenSignal; - public OxygenDetector(Item item, XElement element) + public OxygenDetector(Item item, ContentXElement element) : base (item, element) { IsActive = true; @@ -17,13 +19,15 @@ namespace Barotrauma.Items.Components { if (item.CurrentHull == null) { return; } - if (prevSentOxygenValue != (int)item.CurrentHull.OxygenPercentage || oxygenSignal == null) + int currOxygenPercentage = (int)item.CurrentHull.OxygenPercentage; + if (prevSentOxygenValue != currOxygenPercentage || oxygenSignal == null) { - prevSentOxygenValue = (int)item.CurrentHull.OxygenPercentage; + prevSentOxygenValue = currOxygenPercentage; oxygenSignal = prevSentOxygenValue.ToString(); } - item.SendSignal(oxygenSignal, "signal_out"); + item.SendSignal(oxygenSignal, "signal_out"); + item.SendSignal(currOxygenPercentage <= LowOxygenPercentage ? "1" : "0", "low_oxygen"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index 8e7ea8674..f23292212 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private bool nonContinuousOutputSent; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components private string output; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -48,16 +48,16 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] public bool UseCaptureGroup { get; set; } - [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(true, IsPropertySaveable.Yes, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get; set; } - [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The regular expression used to check the incoming signals.", alwaysUseInstanceValues: true)] public string Expression { get { return expression; } @@ -82,7 +82,7 @@ namespace Barotrauma.Items.Components } } - public RegExFindComponent(Item item, XElement element) + public RegExFindComponent(Item item, ContentXElement element) : base(item, element) { nonContinuousOutputSent = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 138277cee..9ade22e1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -13,7 +13,23 @@ namespace Barotrauma.Items.Components private bool isOn; - private float throttlePowerOutput; + private float prevVoltage; + + private float? newVoltage = null; + + //internal load buffer that's used to isolate the input and output sides from each other + private float internalLoadBuffer = 0; + + //previous load (used to smooth changes in the load to prevent oscillations) + private float prevInternalLoad = 0; + + //previous load on the output side + private float prevExternalLoad = 0; + + //difference between the internal load buffer and external load + private float bufferDiff = 0; + + private float thirdInverseMax = 0, loadEqnConstant = 0; private static readonly Dictionary connectionPairs = new Dictionary { @@ -25,19 +41,36 @@ namespace Barotrauma.Items.Components { "signal_in4", "signal_out4" }, { "signal_in5", "signal_out5" } }; - public float DisplayLoad { get; set; } - [Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")] + protected override PowerPriority Priority { get { return PowerPriority.Relay; } } + + public float DisplayLoad + { + get + { + if (powerOut != null && powerOut.Grid != null) + { + return powerOut.Grid.Load; + } + else + { + return 0; + } + } + } + + [Editable, Serialize(1000.0f, IsPropertySaveable.Yes, description: "The maximum amount of power that can pass through the item.")] public float MaxPower { get { return maxPower; } set { maxPower = Math.Max(0.0f, value); + SetLoadFormulaValues(); } } - [Editable, Serialize(true, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get @@ -55,13 +88,23 @@ namespace Barotrauma.Items.Components } } - public RelayComponent(Item item, XElement element) + public RelayComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; - throttlePowerOutput = MaxPower; + prevVoltage = 0; + SetLoadFormulaValues(); } + private void SetLoadFormulaValues() + { + internalLoadBuffer = MaxPower * 2; + // Set constants for load Formula to reduce calculation time + thirdInverseMax = 1 / (3 * maxPower); + loadEqnConstant = (8 * maxPower) / 3; + } + + public override void OnItemLoaded() { base.OnItemLoaded(); @@ -87,8 +130,8 @@ namespace Barotrauma.Items.Components RefreshConnections(); item.SendSignal(IsOn ? "1" : "0", "state_out"); - - if (!CanTransfer) { Voltage = 0.0f; return; } + item.SendSignal(((int)Math.Round(-PowerLoad)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(DisplayLoad)).ToString(), "load_value_out"); if (isBroken) { @@ -98,75 +141,199 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerOut != null) - { - bool overloaded = false; - foreach (Connection recipient in powerOut.Recipients) - { - var pt = recipient.Item.GetComponent(); - if (pt != null) - { - float overload = -pt.CurrPowerConsumption - pt.PowerLoad; - throttlePowerOutput += overload * deltaTime * 0.5f; - overloaded = overload > 1.0f; - } - } - throttlePowerOutput = overloaded ? - MathHelper.Clamp(throttlePowerOutput, 0.0f, MaxPower): - Math.Max(throttlePowerOutput - MaxPower * 0.1f * deltaTime, 0.0f); - } - - if (Math.Min(-currPowerConsumption, PowerLoad) > maxPower && CanBeOverloaded) + if (Voltage > OverloadVoltage && CanBeOverloaded) { item.Condition = 0.0f; } } - public override void ReceivePowerProbeSignal(Connection connection, Item source, float power) + /// + /// Relay power consumption. Load consumption is based on the internal buffer. + /// This allows for the relay to react to demand and find equilibrium in loop configurations. + /// + public override float GetCurrentPowerConsumption(Connection connection = null) { - if (!IsOn || item.Condition <= 0.0f) { return; } - - //we've already received this signal - if (lastPowerProbeRecipients.Contains(this)) { return; } - lastPowerProbeRecipients.Add(this); - - if (power < 0.0f) + //Can't output or draw if broken + if (isBroken) { - if (!connection.IsOutput || powerIn == null) { return; } + return 0; + } - //power being drawn from the power_out connection - DisplayLoad -= Math.Min(power, 0.0f); - powerLoad -= Math.Min(power + throttlePowerOutput, 0.0f); + if (connection == powerIn) + { + float currentLoad = MaxPower; + if (internalLoadBuffer > MaxPower) + { + //Buffer load charging curve - special relay sauce + // Original formula (buffer - 3 * maxPower)^2 / (3 * maxPower) - (maxPower / 3) + //loadDraw = MathHelper.Clamp((float)Math.Pow(internalBuffer - 3 * MaxPower, 2) / (3 * MaxPower) - MaxPower / 3, 1, MaxPower); + //Optimised formula 0.2% error from original + currentLoad = MathHelper.Clamp(internalLoadBuffer * internalLoadBuffer * thirdInverseMax - 2 * internalLoadBuffer + loadEqnConstant, 0.001f, MaxPower); - //pass the load to items connected to the input - powerIn.SendPowerProbeSignal(source, Math.Max(power, -MaxPower)); + //Slight smoothing to load to minimise relay jank + currentLoad = MathHelper.Clamp((currentLoad + prevInternalLoad * 0.1f) / 1.1f, 0.001f, MaxPower); + prevInternalLoad = currentLoad; + } + + //Add on extra load after calculation + currentLoad += ExtraLoad; + return currentLoad; } else { - if (connection.IsOutput || powerOut == null) { return; } - //power being supplied to the power_in connection - if (currPowerConsumption - power < -MaxPower) + //Flag output as power out + return -1; + } + } + + private bool RelayCanOutput() + { + //Only allow output if device is on, buffers have charge and the connected grids aren't short circuited + return isOn && powerIn != null && powerIn.Grid != null && internalLoadBuffer > 0 && powerOut != null && powerOut.Grid != null && powerIn.Grid != powerOut.Grid; + } + + /// + /// Minimum and maximum power out for the relay. + /// Max out is adjusted to allow for other relays to compensate if this relay is undervolted. + /// + public override PowerRange MinMaxPowerOut(Connection connection, float load = 0) + { + if (connection == powerOut) + { + if (RelayCanOutput()) { - power += MaxPower + (currPowerConsumption - power); - } + //Determine output limits from buffer and voltage + float bufferDraw = MathHelper.Min(internalLoadBuffer, MaxPower); + float voltageLimit = MathHelper.Min(MaxPower, load) * prevVoltage; - currPowerConsumption -= power; + //If undervolted adjust max output so that other relays can compensate + if (prevVoltage < 1) + { + voltageLimit *= prevVoltage; + } - foreach (Connection recipient in powerOut.Recipients) - { - if (!recipient.IsPower) { continue; } - var powered = recipient.Item.GetComponent(); - if (powered == null) { continue; } - - float load = powered.CurrPowerConsumption; - var powerTransfer = powered as PowerTransfer; - if (powerTransfer != null) { load = powerTransfer.PowerLoad; } - - float powerOut = power * (load / Math.Max(powerLoad + throttlePowerOutput, 0.01f)); - powered.ReceivePowerProbeSignal(recipient, source, Math.Min(powerOut, power)); + float maxOutput = MathHelper.Min(voltageLimit, bufferDraw); + return new PowerRange(0.0f, maxOutput); } } + + return PowerRange.Zero; + } + + /// + /// Power out for the relay connection. + /// Relay will output the necessary power to the grid based on maximum power output of other + /// relays and will undervolt and overvolt the grid following its supply grid. + /// + /// Power outputted to the grid + public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) + { + if (connection == powerIn) + { + return 0.0f; + } + else if (RelayCanOutput()) + { + //Determine output limits of the relay + float bufferDraw = MathHelper.Min(internalLoadBuffer, MaxPower); + float voltageLimit = MathHelper.Min(MaxPower, load) * prevVoltage; + float maxOut = MathHelper.Min(voltageLimit, bufferDraw); + + //Don't output negative power to the grid + if (maxOut < 0) + { + PowerLoad = 0; + return 0; + } + + prevExternalLoad = load; + + //Calculate power out + PowerLoad = MathHelper.Clamp((load * prevVoltage - power) / MathHelper.Max(minMaxPower.Max, 1E-20f) * -maxOut, -maxOut, 0); + return -PowerLoad; + } + //Else relay isn't outputting + PowerLoad = 0; + return 0; + } + + /// + /// Connection's grid resolved, determine the difference to be added to the buffer. + /// Ensure the prevVoltage voltage is updated once both grids are resolved. + /// + public override void GridResolved(Connection conn) + { + if (conn == powerIn) + { + if (powerIn != null && powerIn.Grid != null) + { + float addToBuffer = powerIn.Grid.Voltage * (CurrPowerConsumption - ExtraLoad); + + //Limit power input to the previous voltage to prevent wild oscillations in overload + if (powerIn.Grid.Voltage > 1) + { + addToBuffer = prevVoltage * (CurrPowerConsumption - ExtraLoad); + } + + //Cap the max power input + if (addToBuffer > MaxPower) + { + addToBuffer = MaxPower; + } + + //To prevent problems with grid order, only update voltage and buffer after input and output grids have been resolved + if (newVoltage == null) + { + //temporarily store the new voltage and also indicates that the input connection side has been updated + newVoltage = powerIn.Grid.Voltage; + bufferDiff = addToBuffer; + } + else + { + UpdateBuffer(addToBuffer, powerIn.Grid.Voltage); + } + } + } + else + { + //To prevent problems with grid order, only update voltage and buffer after input and output grids have been resolved + if (newVoltage == null) + { + //Flag that output connection has been updated already + newVoltage = -1; + bufferDiff = PowerLoad; + } + else + { + UpdateBuffer(PowerLoad, (float)newVoltage); + } + } + } + + private void UpdateBuffer(float addToBuffer, float newVoltage) + { + //Update buffer and voltage + float limit = MaxPower * 2; + + //Clamp the buffer to have a constant load in a severe overload event, otherwise wild oscillation will occur + if (RelayCanOutput() && powerIn.Grid.Voltage > 2) + { + limit = MathHelper.Min(limit, 3 * MaxPower - (float)Math.Sqrt((3 * prevExternalLoad + MaxPower) * MaxPower)); + } + + //Add to the internal buffer + internalLoadBuffer = MathHelper.Clamp(internalLoadBuffer + bufferDiff + addToBuffer, 0, limit); + + //Decay overvoltage slightly, helps resolve large chain loops to a grid + if (newVoltage > 1) + { + newVoltage = MathHelper.Max(newVoltage - 0.0005f, 1); + } + + prevVoltage = newVoltage; + this.newVoltage = null; + bufferDiff = 0; } public override void ReceiveSignal(Signal signal, Connection connection) @@ -192,7 +359,7 @@ namespace Barotrauma.Items.Components public void SetState(bool on, bool isNetworkMessage) { #if CLIENT - if (GameMain.Client != null && !isNetworkMessage) return; + if (GameMain.Client != null && !isNetworkMessage) { return; } #endif #if SERVER @@ -210,7 +377,7 @@ namespace Barotrauma.Items.Components msg.Write(isOn); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientRead(ServerNetObject type, IReadMessage msg, float _) { SetState(msg.ReadBoolean(), true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs index 9cc306e6a..34d856955 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class SignalCheckComponent : ItemComponent { private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -48,10 +48,10 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] public string TargetSignal { get; set; } - public SignalCheckComponent(Item item, XElement element) + public SignalCheckComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index 1a924aa6f..22cbef2ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components private bool fireInRange; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected a fire.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item outputs when it has detected a fire.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected a fire.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected a fire.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -53,7 +53,7 @@ namespace Barotrauma.Items.Components } } - public SmokeDetector(Item item, XElement element) + public SmokeDetector(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs index 5333bec84..a8c69fdeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components [InGameEditable(DecimalCount = 2), - Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components } } - public StringComponent(Item item, XElement element) + public StringComponent(Item item, ContentXElement element) : base(item, element) { timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs index c4a870661..ed7dcc8d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SubtractComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class SubtractComponent : ArithmeticComponent { - public SubtractComponent(Item item, XElement element) + public SubtractComponent(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 22fd92e52..13409a48e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -32,14 +32,14 @@ namespace Barotrauma.Items.Components private List messageHistory = new List(MaxMessages); - public string DisplayedWelcomeMessage + public LocalizedString DisplayedWelcomeMessage { get; private set; } private string welcomeMessage; - [InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.", AlwaysUseInstanceValues = true)] + [InGameEditable, Serialize("", IsPropertySaveable.Yes, "Message to be displayed on the terminal display when it is first opened.", translationTextTag: "terminalwelcomemsg.", alwaysUseInstanceValues: true)] public string WelcomeMessage { get { return welcomeMessage; } @@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components { if (welcomeMessage == value) { return; } welcomeMessage = value; - DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage.Replace("\\n", "\n"); + DisplayedWelcomeMessage = TextManager.Get(welcomeMessage).Fallback(welcomeMessage.Replace("\\n", "\n")); } } @@ -64,12 +64,12 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "The terminal will use a monospace font if this box is ticked.", alwaysUseInstanceValues: true)] public bool UseMonospaceFont { get; set; } private Color textColor = Color.LimeGreen; - [Editable, Serialize("50,205,50,255", true, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] + [Editable, Serialize("50,205,50,255", IsPropertySaveable.Yes, description: "Color of the terminal text.", alwaysUseInstanceValues: true)] public Color TextColor { get => textColor; @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components private string prevColorSignal; - public Terminal(Item item, XElement element) + public Terminal(Item item, ContentXElement element) : base(item, element) { IsActive = true; @@ -143,9 +143,9 @@ namespace Barotrauma.Items.Components #endif base.OnItemLoaded(); - if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) + if (!DisplayedWelcomeMessage.IsNullOrEmpty()) { - ShowOnDisplay(DisplayedWelcomeMessage, addToHistory: !isSubEditor, TextColor); + ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor); DisplayedWelcomeMessage = ""; //remove welcome message if a game session is running so it doesn't reappear on successive rounds if (GameMain.GameSession != null && !isSubEditor) @@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); for (int i = 0; i < MaxMessages; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 6814915aa..b16dce24c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -20,21 +20,21 @@ namespace Barotrauma.Items.Components private readonly float[] receivedSignal = new float[2]; private readonly float[] timeSinceReceived = new float[2]; - [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] + [Serialize(FunctionType.Sin, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; } - [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the trigonometric function uses radians instead of degrees.", alwaysUseInstanceValues: true)] public bool UseRadians { get; set; } - public TrigonometricFunctionComponent(Item item, XElement element) + public TrigonometricFunctionComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 6d78cea05..7fc471035 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components private float stateSwitchDelay; private int maxOutputLength; - [Editable, Serialize(200, false, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] + [Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")] public int MaxOutputLength { get { return maxOutputLength; } @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components } private string output; - [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("1", IsPropertySaveable.Yes, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] public string Output { get { return output; } @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components } private string falseOutput; - [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components } } - public WaterDetector(Item item, XElement element) + public WaterDetector(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index ec74d3894..c95c55497 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -30,10 +30,10 @@ namespace Barotrauma.Items.Components private Connection signalInConnection; private Connection signalOutConnection; - [Serialize(CharacterTeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] + [Serialize(CharacterTeamType.None, IsPropertySaveable.Yes, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] public CharacterTeamType TeamID { get; set; } - [Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)] + [Editable, Serialize(20000.0f, IsPropertySaveable.No, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)] public float Range { get { return range; } @@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(0, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(0, IsPropertySaveable.Yes, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] public int Channel { get { return channel; } @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components } - [Editable, Serialize(false, true, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)] public bool AllowCrossTeamCommunication { get; @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components } [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)] - [Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + + [Serialize(false, IsPropertySaveable.No, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + "as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)] public bool LinkToChat { @@ -73,7 +73,7 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(1.0f, true, description: "How many seconds have to pass between signals for a message to be displayed in the chatbox. " + + [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How many seconds have to pass between signals for a message to be displayed in the chatbox. " + "Setting this to a very low value is not recommended, because it may cause an excessive amount of chat messages to be created " + "if there are chat-linked wifi components that transmit a continuous signal.")] public float MinChatMessageInterval @@ -82,14 +82,14 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, true, description: "If set to true, the component will only create chat messages when the received signal changes.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the component will only create chat messages when the received signal changes.")] public bool DiscardDuplicateChatMessages { get; set; } - public WifiComponent(Item item, XElement element) + public WifiComponent(Item item, ContentXElement element) : base (item, element) { list.Add(this); @@ -194,8 +194,6 @@ namespace Barotrauma.Items.Components { item.LastSentSignalRecipients.Clear(); } - var senderComponent = signal.source?.GetComponent(); - if (senderComponent != null && !CanReceive(senderComponent)) { return; } bool chatMsgSent = false; @@ -247,7 +245,7 @@ namespace Barotrauma.Items.Components wifiComp.item.ParentInventory.Owner != null) { string chatMsg = signal.value; - if (senderComponent != null) + if (sentSignalStrength <= 1.0f) { chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index ec7a0db91..ee21d0939 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -81,35 +81,35 @@ namespace Barotrauma.Items.Components get { return connections; } } - [Serialize(5000.0f, false, description: "The maximum distance the wire can extend (in pixels).")] + [Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")] public float MaxLength { get; set; } - [Serialize(false, false, description: "If enabled, the wire will not be visible in connection panels outside the submarine editor.")] + [Serialize(false, IsPropertySaveable.No, description: "If enabled, the wire will not be visible in connection panels outside the submarine editor.")] public bool HiddenInGame { get; set; } - [Editable, Serialize(false, true, "If enabled, this wire will be ignored by the \"Lock all default wires\" setting.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, "If enabled, this wire will be ignored by the \"Lock all default wires\" setting.", alwaysUseInstanceValues: true)] public bool NoAutoLock { get; set; } - [Editable, Serialize(false, true, "If enabled, this wire will use the sprite depth instead of a constant depth.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, "If enabled, this wire will use the sprite depth instead of a constant depth.")] public bool UseSpriteDepth { get; set; } - public Wire(Item item, XElement element) + public Wire(Item item, ContentXElement element) : base(item, element) { nodes = new List(); @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public Connection OtherConnection(Connection connection) { @@ -785,7 +785,7 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs index bf493f470..71981bb8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class XorComponent : AndComponent { - public XorComponent(Item item, XElement element) + public XorComponent(Item item, ContentXElement element) : base(item, element) { IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs index 833b66c2e..5e46bd13c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/StatusHUD.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class StatusHUD : ItemComponent { - public StatusHUD(Item item, XElement element) + public StatusHUD(Item item, ContentXElement element) : base(item, element) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index a748459c7..582357d8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components { partial class TriggerComponent : ItemComponent { - [Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] + [Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] public float Force { get; set; } public PhysicsBody PhysicsBody { get; private set; } @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components /// private readonly List attacks = new List(); - public TriggerComponent(Item item, XElement element) : base(item, element) + public TriggerComponent(Item item, ContentXElement element) : base(item, element) { string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); if (!Enum.TryParse(triggeredByAttribute, out triggeredBy)) @@ -72,7 +72,7 @@ namespace Barotrauma.Items.Components forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f); string parentDebugName = $"TriggerComponent in {item.Name}"; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 0de5ab645..ca3398fee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components get { return rotation; } } - [Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] + [Serialize("0,0", IsPropertySaveable.No, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] public Vector2 BarrelPos { get @@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components } } - [Serialize("0,0", false, description: "The projectile launching location relative to transformed barrel position (in pixels).")] + [Serialize("0,0", IsPropertySaveable.No, description: "The projectile launching location relative to transformed barrel position (in pixels).")] public Vector2 FiringOffset { get; @@ -106,49 +106,49 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")] public float LaunchImpulse { get { return launchImpulse; } set { launchImpulse = value; } } - [Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")] + [Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, IsPropertySaveable.No, description: "The period of time the user has to wait between shots.")] public float Reload { get { return reloadTime; } set { reloadTime = value; } } - [Editable(0.1f, 10f), Serialize(1.0f, false, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] + [Editable(0.1f, 10f), Serialize(1.0f, IsPropertySaveable.No, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] public float RetractionDurationMultiplier { get; set; } - [Editable(0.1f, 10f), Serialize(0.1f, false, description: "How quickly the recoil moves the barrel after launching.")] + [Editable(0.1f, 10f), Serialize(0.1f, IsPropertySaveable.No, description: "How quickly the recoil moves the barrel after launching.")] public float RecoilTime { get; set; } - [Editable(0f, 1000f), Serialize(0f, false, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] + [Editable(0f, 1000f), Serialize(0f, IsPropertySaveable.No, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] public float RetractionDelay { get; set; } - [Serialize(1, false, description: "How many projectiles the weapon launches when fired once.")] + [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; set; } - [Serialize(false, false, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] + [Serialize(false, IsPropertySaveable.No, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] public bool LaunchWithoutProjectile { get; @@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components } [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), - Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] + Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] public Vector2 RotationLimits { get @@ -179,7 +179,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] + [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] public float Spread { get; @@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(5.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character" + Serialize(5.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character" + " with insufficient skills to operate it. Higher values make the barrel rotate faster.")] public float SpringStiffnessLowSkill { @@ -195,7 +195,7 @@ namespace Barotrauma.Items.Components private set; } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(2.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character" + Serialize(2.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character" + " with sufficient skills to operate it. Higher values make the barrel rotate faster.")] public float SpringStiffnessHighSkill { @@ -204,7 +204,7 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(50.0f, false, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + Serialize(50.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + " with insufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")] public float SpringDampingLowSkill { @@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components private set; } [Editable(0.0f, 1000.0f, DecimalCount = 2), - Serialize(10.0f, false, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + Serialize(10.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character" + " with sufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at.")] public float SpringDampingHighSkill { @@ -221,28 +221,28 @@ namespace Barotrauma.Items.Components } [Editable(0.0f, 100.0f, DecimalCount = 2), - Serialize(1.0f, false, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")] + Serialize(1.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")] public float RotationSpeedLowSkill { get; private set; } [Editable(0.0f, 100.0f, DecimalCount = 2), - Serialize(5.0f, false, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),] + Serialize(5.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),] public float RotationSpeedHighSkill { get; private set; } - [Serialize(1.0f, false, description: "How fast the turret can rotate while firing (for charged weapons).")] + [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the turret can rotate while firing (for charged weapons).")] public float FiringRotationSpeedModifier { get; private set; } - [Serialize(false, true, description: "Whether the turret should always charge-up fully to shoot.")] + [Serialize(false, IsPropertySaveable.Yes, description: "Whether the turret should always charge-up fully to shoot.")] public bool SingleChargedShot { get; @@ -251,7 +251,7 @@ namespace Barotrauma.Items.Components private float prevScale; float prevBaseRotation; - [Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] public float BaseRotation { get { return item.Rotation; } @@ -262,33 +262,33 @@ namespace Barotrauma.Items.Components } } - [Serialize(3000.0f, true, description: "How close to a target the turret has to be for an AI character to fire it.")] + [Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")] public float AIRange { get; set; } - [Serialize(-1, true, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] + [Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] public int MaxActiveProjectiles { get; set; } - [Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")] + [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")] public float MaxChargeTime { get; private set; } - public Turret(Item item, XElement element) + public Turret(Item item, ContentXElement element) : base(item, element) { IsActive = true; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -315,7 +315,7 @@ namespace Barotrauma.Items.Components InitProjSpecific(element); } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); private void UpdateTransformedBarrelPos() { @@ -456,7 +456,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)) { - user.Info.IncreaseSkillLevel("weapons", + user.Info.IncreaseSkillLevel("weapons".ToIdentifier(), SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f)); } @@ -601,7 +601,7 @@ namespace Barotrauma.Items.Components //use linked projectile containers in case they have to react to the turret being launched somehow //(play a sound, spawn more projectiles) if (!(e is Item linkedItem)) { continue; } - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.Condition <= 0.0f) { loaderBroken = true; @@ -644,7 +644,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { if (!(e is Item linkedItem)) { continue; } - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) { tinkeringStrength = repairable.TinkeringStrength; @@ -653,8 +653,9 @@ namespace Barotrauma.Items.Components if (!ignorePower) { - var batteries = item.GetConnectedComponents(); + List batteries = GetConnectedBatteries(); float neededPower = GetPowerRequiredToShoot(); + // tinkering is currently not factored into the common method as it is checked only when shooting // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); @@ -864,8 +865,8 @@ namespace Barotrauma.Items.Components foreach (var character in Character.CharacterList) { if (character == null || character.Removed || character.IsDead) { continue; } - if (character.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) { continue; } - bool isHuman = character.IsHuman || character.Params.Group.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); + if (character.Params.Group == ai.Config.Entity) { continue; } + bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName; if (isHuman) { if (!targetHumans) @@ -901,7 +902,7 @@ namespace Barotrauma.Items.Components closestDist = shootDistance * shootDistance; if (closestSub != null) { - foreach (var hull in Hull.hullList) + foreach (var hull in Hull.HullList) { if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); @@ -984,8 +985,8 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character.Speak(TextManager.Get("DialogTurretTargetDead"), - identifier: "killedtarget" + previousTarget.ID, + character.Speak(TextManager.Get("DialogTurretTargetDead").Value, + identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(), minDurationBetweenSimilar: 10.0f); character.AIController.SelectTarget(null); } @@ -993,7 +994,7 @@ namespace Barotrauma.Items.Components bool canShoot = true; if (!HasPowerToShoot()) { - var batteries = item.GetConnectedComponents(); + List batteries = GetConnectedBatteries(); float lowestCharge = 0.0f; PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) @@ -1013,8 +1014,8 @@ namespace Barotrauma.Items.Components } else { - character.Speak(TextManager.Get("DialogSupercapacitorIsBroken"), - identifier: "supercapacitorisbroken", + character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value, + identifier: "supercapacitorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f); canShoot = false; } @@ -1023,13 +1024,13 @@ namespace Barotrauma.Items.Components if (batteryToLoad == null) { return true; } if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f) { - objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: "", requireEquip: false)); + objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: Identifier.Empty, requireEquip: false)); return false; } if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0) { - character.Speak(TextManager.Get("DialogTurretHasNoPower"), - identifier: "turrethasnopower", + character.Speak(TextManager.Get("DialogTurretHasNoPower").Value, + identifier: "turrethasnopower".ToIdentifier(), minDurationBetweenSimilar: 30.0f); canShoot = false; } @@ -1040,7 +1041,7 @@ namespace Barotrauma.Items.Components foreach (MapEntity e in item.linkedTo) { if (!item.IsInteractable(character)) { continue; } - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (e is Item projectileContainer) { var container = projectileContainer.GetComponent(); @@ -1070,8 +1071,8 @@ namespace Barotrauma.Items.Components { if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: true), - identifier: "cannotloadturret", + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value, + identifier: "cannotloadturret".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } return true; @@ -1079,11 +1080,11 @@ namespace Barotrauma.Items.Components if (objective.SubObjectives.None()) { var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true); - loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; + loadItemsObjective.ignoredContainerIdentifiers = new Identifier[] { ((MapEntity)containerItem).Prefab.Identifier }; if (character.IsOnPlayerTeam) { - character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: true), - identifier: "loadturret", + character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value, + identifier: "loadturret".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } loadItemsObjective.Abandoned += CheckRemainingAmmo; @@ -1094,18 +1095,18 @@ namespace Barotrauma.Items.Components { if (!character.IsOnPlayerTeam) { return; } if (character.Submarine != Submarine.MainSub) { return; } - string ammoType = container.ContainableItemIdentifiers.FirstOrDefault() ?? "ammobox"; + Identifier ammoType = container.ContainableItemIdentifiers.FirstOrNull() ?? "ammobox".ToIdentifier(); int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); if (remainingAmmo == 0) { - character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), - identifier: "outofammo", + character.Speak(TextManager.Get($"DialogOutOf{ammoType}", "DialogOutOfTurretAmmo").Value, + identifier: "outofammo".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } else if (remainingAmmo < 3) { - character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), - identifier: "outofammo", + character.Speak(TextManager.Get($"DialogLowOn{ammoType}").Value, + identifier: "outofammo".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } } @@ -1264,29 +1265,29 @@ namespace Barotrauma.Items.Components { if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget) { - if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName)) + if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.Get("DialogNewTargetSpotted"), - identifier: "newtargetspotted", + character.Speak(TextManager.Get("DialogNewTargetSpotted").Value, + identifier: "newtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } - else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), - identifier: "identifiedtargetspotted", + character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value, + identifier: "identifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } else { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), - identifier: "unidentifiedtargetspotted", + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, + identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } } - else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName)) { - character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), - identifier: "unidentifiedtargetspotted", + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value, + identifier: "unidentifiedtargetspotted".ToIdentifier(), minDurationBetweenSimilar: 5.0f); } character.AddEncounter(closestEnemy); @@ -1295,8 +1296,8 @@ namespace Barotrauma.Items.Components } else if (closestEnemy == null && character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogIceSpireSpotted"), - identifier: "icespirespotted", + character.Speak(TextManager.Get("DialogIceSpireSpotted").Value, + identifier: "icespirespotted".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } @@ -1339,8 +1340,8 @@ namespace Barotrauma.Items.Components if (!shoot) { return false; } if (character.IsOnPlayerTeam) { - character.Speak(TextManager.Get("DialogFireTurret"), - identifier: "fireturret", + character.Speak(TextManager.Get("DialogFireTurret").Value, + identifier: "fireturret".ToIdentifier(), minDurationBetweenSimilar: 30.0f); } character.SetInput(InputType.Shoot, true, true); @@ -1349,6 +1350,14 @@ namespace Barotrauma.Items.Components return false; } + /// + /// Turret doesn't consume grid power, directly takes from the batteries on its grid instead. + /// + public override float GetCurrentPowerConsumption(Connection conn = null) + { + return 0; + } + private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true) { if (targetBody == null) { return false; } @@ -1372,7 +1381,7 @@ namespace Barotrauma.Items.Components } if (ai != null) { - if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) + if (targetCharacter.Params.Group == ai.Config.Entity) { return false; } @@ -1458,7 +1467,7 @@ namespace Barotrauma.Items.Components for (int j = 0; j < item.linkedTo.Count; j++) { var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count]; - if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } + if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; } if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching); @@ -1602,7 +1611,7 @@ namespace Barotrauma.Items.Components private Vector2? loadedRotationLimits; private float? loadedBaseRotation; - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 93ada4ac4..784669f37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; +using System.Collections.Immutable; using Barotrauma.Abilities; namespace Barotrauma @@ -24,16 +25,16 @@ namespace Barotrauma class WearableSprite { - public string UnassignedSpritePath { get; private set; } + public ContentPath UnassignedSpritePath { get; private set; } public string SpritePath { get; private set; } - public XElement SourceElement { get; private set; } + public ContentXElement SourceElement { get; private set; } public WearableType Type { get; private set; } private Sprite _sprite; public Sprite Sprite { get { return _sprite; } - set + private set { if (value == _sprite) { return; } if (_sprite != null) @@ -85,29 +86,29 @@ namespace Barotrauma public int Variant { get; set; } - private Gender _gender; + private Character _picker; /// /// None = Any/Not Defined -> no effect. /// Changing the gender forces re-initialization, because the textures can be different for male and female characters. /// - public Gender Gender + public Character Picker { - get { return _gender; } + get { return _picker; } set { - if (value == _gender) { return; } - _gender = value; + if (value == _picker) { return; } + _picker = value; IsInitialized = false; - UnassignedSpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty)); - Init(_gender); + UnassignedSpritePath = ParseSpritePath(SourceElement); + Init(_picker); } } - public WearableSprite(XElement subElement, WearableType type) + public WearableSprite(ContentXElement subElement, WearableType type) { Type = type; SourceElement = subElement; - UnassignedSpritePath = subElement.GetAttributeString("texture", string.Empty); + UnassignedSpritePath = subElement.GetAttributeContentPath("texture") ?? ContentPath.Empty; Init(); switch (type) { @@ -131,34 +132,48 @@ namespace Barotrauma /// /// Note: this constructor cannot initialize automatically, because the gender is unknown at this point. We only know it when the item is equipped. /// - public WearableSprite(XElement subElement, Wearable wearable, int variant = 0) + public WearableSprite(ContentXElement subElement, Wearable wearable, int variant = 0) { Type = WearableType.Item; WearableComponent = wearable; Variant = Math.Max(variant, 0); - UnassignedSpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty)); + UnassignedSpritePath = ParseSpritePath(subElement); SourceElement = subElement; } - private string ParseSpritePath(string texturePath) => texturePath.Contains("/") ? texturePath : $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{texturePath}"; + private ContentPath ParseSpritePath(ContentXElement element) + { + if (element.DoesAttributeReferenceFileNameAlone("texture")) + { + string textureName = element.GetAttributeString("texture", ""); + return ContentPath.FromRaw( + element.ContentPackage, + $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}"); + } + else + { + return element.GetAttributeContentPath("texture") ?? ContentPath.Empty; + } + } public void ParsePath(bool parseSpritePath) { - string tempPath = UnassignedSpritePath; - if (_gender != Gender.None) + SpritePath = UnassignedSpritePath.Value; + if (_picker?.Info != null) { - tempPath = tempPath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male"); + SpritePath = _picker.Info.ReplaceVars(SpritePath); } - SpritePath = tempPath.Replace("[VARIANT]", Variant.ToString()); + SpritePath = SpritePath.Replace("[VARIANT]", Variant.ToString()); if (!File.Exists(SpritePath)) { // If the variant does not exist, parse the path so that it uses first variant. - SpritePath = tempPath.Replace("[VARIANT]", "1"); + SpritePath = SpritePath.Replace("[VARIANT]", "1"); } - if (!File.Exists(SpritePath) && _gender == Gender.None) + if (!File.Exists(SpritePath) && _picker?.Info == null) { - // If there's no sprite for Gender.None does not exist, try to use male sprite - SpritePath = tempPath.Replace("[GENDER]", "male"); + // If there's no character info is defined, try to use first tagset from CharacterInfoPrefab + var charInfoPrefab = CharacterPrefab.HumanPrefab.CharacterInfoPrefab; + SpritePath = charInfoPrefab.ReplaceVars(SpritePath, charInfoPrefab.Heads.First()); } if (parseSpritePath) { @@ -167,10 +182,11 @@ namespace Barotrauma } public bool IsInitialized { get; private set; } - public void Init(Gender gender = Gender.None) + public void Init(Character picker = null) { if (IsInitialized) { return; } - _gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None; + + _picker = picker; ParsePath(false); Sprite?.Remove(); Sprite = new Sprite(SourceElement, file: SpritePath); @@ -226,13 +242,13 @@ namespace Barotrauma.Items.Components { partial class Wearable : Pickable, IServerSerializable { - private readonly XElement[] wearableElements; + private readonly ContentXElement[] wearableElements; private readonly WearableSprite[] wearableSprites; private readonly LimbType[] limbType; private readonly Limb[] limb; private readonly List damageModifiers; - public readonly Dictionary SkillModifiers = new Dictionary(); + public readonly Dictionary SkillModifiers; public readonly Dictionary WearableStatValues = new Dictionary(); @@ -285,23 +301,24 @@ namespace Barotrauma.Items.Components } } - public Wearable(Item item, XElement element) : base(item, element) + public Wearable(Item item, ContentXElement element) : base(item, element) { this.item = item; damageModifiers = new List(); + SkillModifiers = new Dictionary(); int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); - variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server); + variant = Rand.Range(1, Variants + 1, Rand.RandSync.ServerAndClient); wearableSprites = new WearableSprite[spriteCount]; - wearableElements = new XElement[spriteCount]; + wearableElements = new ContentXElement[spriteCount]; limbType = new LimbType[spriteCount]; limb = new Limb[spriteCount]; AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false); int i = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -318,7 +335,7 @@ namespace Barotrauma.Items.Components wearableSprites[i] = new WearableSprite(subElement, this, variant); wearableElements[i] = subElement; - foreach (XElement lightElement in subElement.Elements()) + foreach (var lightElement in subElement.Elements()) { if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } wearableSprites[i].LightComponent = new LightComponent(item, lightElement) @@ -334,7 +351,7 @@ namespace Barotrauma.Items.Components damageModifiers.Add(new DamageModifier(subElement, item.Name + ", Wearable")); break; case "skillmodifier": - string skillIdentifier = subElement.GetAttributeString("skillidentifier", string.Empty); + Identifier skillIdentifier = subElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); float skillValue = subElement.GetAttributeFloat("skillvalue", 0f); if (SkillModifiers.ContainsKey(skillIdentifier)) { @@ -382,12 +399,9 @@ namespace Barotrauma.Items.Components for (int i = 0; i < wearableSprites.Length; i++ ) { var wearableSprite = wearableSprites[i]; - if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker.Info?.Gender ?? Gender.None); } - if (picker.Info != null && picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None)) - { - // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. - wearableSprite.Gender = picker.Info.Gender; - } + if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker); } + // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. + wearableSprite.Picker = picker; Limb equipLimb = character.AnimController.GetLimb(limbType[i]); if (equipLimb == null) { continue; } @@ -512,7 +526,7 @@ namespace Barotrauma.Items.Components } private int loadedVariant = -1; - public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); loadedVariant = componentElement.GetAttributeInt("variant", -1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 69f16d500..4348caf46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -287,7 +287,7 @@ namespace Barotrauma if (DraggableIndicator == null) { - DraggableIndicator = GUI.Style.GetComponentStyle("GUIDragIndicator").GetDefaultSprite(); + DraggableIndicator = GUIStyle.GetComponentStyle("GUIDragIndicator").GetDefaultSprite(); slotHotkeySprite = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(258, 7, 120, 120), null, 0); @@ -479,15 +479,15 @@ namespace Barotrauma { if (i < 0 || i >= slots.Length) { - string thisItemStr = item?.prefab.Identifier ?? "null"; + string thisItemStr = item?.Prefab.Identifier.Value ?? "null"; string ownerStr = "null"; if (Owner is Item ownerItem) { - ownerStr = ownerItem.prefab.Identifier; + ownerStr = ownerItem.Prefab.Identifier.Value; } else if (Owner is Character ownerCharacter) { - ownerStr = ownerCharacter.SpeciesName; + ownerStr = ownerCharacter.SpeciesName.Value; } string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {thisItemStr}, inventory: {ownerStr})."; GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); @@ -533,7 +533,7 @@ namespace Barotrauma else { #if CLIENT - if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } #endif return false; } @@ -766,11 +766,11 @@ namespace Barotrauma { for (int j = 0; j < capacity; j++) { - if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); } + if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } } for (int j = 0; j < otherInventory.capacity; j++) { - if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); } + if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.9f); } } } #endif @@ -831,7 +831,7 @@ namespace Barotrauma { if (slots[j].Contains(existingItems.FirstOrDefault())) { - visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + visualSlots[j].ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.9f); } } } @@ -890,15 +890,15 @@ namespace Barotrauma return list; } - public Item FindItemByTag(string tag, bool recursive = false) + public Item FindItemByTag(Identifier tag, bool recursive = false) { - if (tag == null) { return null; } + if (tag.IsEmpty) { return null; } return FindItem(i => i.HasTag(tag), recursive); } - public Item FindItemByIdentifier(string identifier, bool recursive = false) + public Item FindItemByIdentifier(Identifier identifier, bool recursive = false) { - if (identifier == null) { return null; } + if (identifier.IsEmpty) { return null; } return FindItem(i => i.Prefab.Identifier == identifier, recursive); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index cfe23ac81..e892953f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; +using System.Collections.Immutable; using Barotrauma.Abilities; #if CLIENT @@ -22,11 +23,11 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable { public static List ItemList = new List(); - public ItemPrefab Prefab => prefab as ItemPrefab; + public new ItemPrefab Prefab => base.Prefab as ItemPrefab; public static bool ShowLinks = true; - - private readonly HashSet tags; + + private readonly HashSet tags; private bool isWire, isLogic; @@ -119,7 +120,7 @@ namespace Barotrauma private readonly bool[] hasStatusEffectsOfType; private readonly Dictionary> statusEffectLists; - public Dictionary SerializableProperties { get; protected set; } + public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; bool HasInGameEditableProperties @@ -186,17 +187,17 @@ namespace Barotrauma public override string Name { - get { return prefab.Name; } + get { return base.Prefab.Name.Value; } } private string description; public string Description { - get { return description ?? prefab.Description; } + get { return description ?? base.Prefab.Description.Value; } set { description = value; } } - [Editable, Serialize(false, true, alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool NonInteractable { get; @@ -206,21 +207,21 @@ namespace Barotrauma /// /// Use to also check /// - [Editable, Serialize(false, true, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)] public bool NonPlayerTeamInteractable { get; set; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, true, alwaysUseInstanceValues: true)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.IsSwappableItem), Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool AllowSwapping { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool PurchasedNewSwap { get; @@ -262,7 +263,7 @@ namespace Barotrauma private float rotationRad; - [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)] + [ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)] public float Rotation { get @@ -331,7 +332,7 @@ namespace Barotrauma if (scale == value) { return; } scale = MathHelper.Clamp(value, 0.01f, 10.0f); - float relativeScale = scale / prefab.Scale; + float relativeScale = scale / base.Prefab.Scale; if (!ResizeHorizontal || !ResizeVertical) { @@ -357,21 +358,21 @@ namespace Barotrauma } = float.PositiveInfinity; protected Color spriteColor; - [Editable, Serialize("1.0,1.0,1.0,1.0", true)] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor { get { return spriteColor; } set { spriteColor = value; } } - [Serialize("1.0,1.0,1.0,1.0", true), Editable] + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable] public Color InventoryIconColor { get; protected set; } - - [Editable, Serialize("1.0,1.0,1.0,1.0", true, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] + + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] public Color ContainerColor { get; @@ -381,26 +382,26 @@ namespace Barotrauma /// /// Can be used by status effects or conditionals to check what item this item is contained inside /// - public string ContainerIdentifier + public Identifier ContainerIdentifier { get { return - Container?.prefab.Identifier ?? - ParentInventory?.Owner?.ToString() ?? - ""; + Container?.Prefab.Identifier ?? + ParentInventory?.Owner?.ToIdentifier() ?? + Identifier.Empty; } } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] /// /// Can be used to modify the AITarget's label using status effects /// public string SonarLabel { - get { return AiTarget?.SonarLabel ?? ""; } + get { return AiTarget?.SonarLabel?.Value ?? ""; } set { if (AiTarget != null) @@ -421,7 +422,7 @@ namespace Barotrauma } } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sound range /// @@ -431,7 +432,7 @@ namespace Barotrauma set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } } } - [Serialize(0.0f, false)] + [Serialize(0.0f, IsPropertySaveable.No)] /// /// Can be used by status effects or conditionals to modify the sound range /// @@ -444,13 +445,13 @@ namespace Barotrauma /// /// Should the item's Use method be called with the "Use" or with the "Shoot" key? /// - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool IsShootable { get; set; } /// /// If true, the user has to hold the "aim" key before use is registered. False by default. /// - [Serialize(false, false)] + [Serialize(false, IsPropertySaveable.No)] public bool RequireAimToUse { get; set; @@ -459,7 +460,7 @@ namespace Barotrauma /// /// If true, the user has to hold the "aim" key before secondary use is registered. True by default. /// - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool RequireAimToSecondaryUse { get; set; @@ -475,8 +476,8 @@ namespace Barotrauma public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition); private float offsetOnSelectedMultiplier = 1.0f; - - [Serialize(1.0f, false)] + + [Serialize(1.0f, IsPropertySaveable.No)] public float OffsetOnSelectedMultiplier { get => offsetOnSelectedMultiplier; @@ -485,7 +486,7 @@ namespace Barotrauma private float healthMultiplier = 1.0f; - [Serialize(1.0f, true, "Multiply the maximum condition by this value")] + [Serialize(1.0f, IsPropertySaveable.Yes, "Multiply the maximum condition by this value")] public float HealthMultiplier { get => healthMultiplier; @@ -499,7 +500,7 @@ namespace Barotrauma private float maxRepairConditionMultiplier = 1.0f; - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float MaxRepairConditionMultiplier { get => maxRepairConditionMultiplier; @@ -508,7 +509,7 @@ namespace Barotrauma //the default value should be Prefab.Health, but because we can't use it in the attribute, //we'll just use NaN (which does nothing) and set the default value in the constructor/load - [Serialize(float.NaN, false), Editable] + [Serialize(float.NaN, IsPropertySaveable.No), Editable] public float Condition { get { return condition; } @@ -517,7 +518,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (!MathUtils.IsValid(value)) { return; } if (Indestructible) { return; } - if (InvulnerableToDamage && value <= condition) { return;} + if (InvulnerableToDamage && value <= condition) { return; } float prev = condition; bool wasInFullCondition = IsFullCondition; @@ -525,6 +526,8 @@ namespace Barotrauma condition = MathHelper.Clamp(value, 0.0f, MaxCondition); if (condition == 0.0f && prev > 0.0f) { + //Flag connections to be updated as device is broken + flagChangedConnections(connections); #if CLIENT foreach (ItemComponent ic in components) { @@ -534,6 +537,11 @@ namespace Barotrauma #endif ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } + else if (condition > 0.0f && prev <= 0.0f) + { + //Flag connections to be updated as device is now working again + flagChangedConnections(connections); + } SetActiveSprite(); @@ -559,6 +567,22 @@ namespace Barotrauma LastConditionChange = condition - prev; ConditionLastUpdated = Timing.TotalTime; + + static void flagChangedConnections(Dictionary connections) + { + if (connections == null) { return; } + foreach (Connection c in connections.Values) + { + if (c.IsPower && c.Grid != null) + { + Powered.ChangedConnections.Add(c); + foreach (Connection conn in c.Recipients) + { + Powered.ChangedConnections.Add(conn); + } + } + } + } } } @@ -590,7 +614,7 @@ namespace Barotrauma set; } - [Editable, Serialize(false, isSaveable: true, "When enabled will prevent the item from taking damage from all sources")] + [Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")] public bool InvulnerableToDamage { get; set; } public bool StolenDuringRound; @@ -609,7 +633,7 @@ namespace Barotrauma } } - [Serialize(true, true, alwaysUseInstanceValues: true)] + [Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public bool AllowStealing { get; @@ -617,7 +641,7 @@ namespace Barotrauma } private string originalOutpost; - [Serialize("", true, alwaysUseInstanceValues: true)] + [Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public string OriginalOutpost { get { return originalOutpost; } @@ -631,7 +655,7 @@ namespace Barotrauma } } - [Editable, Serialize("", true)] + [Editable, Serialize("", IsPropertySaveable.Yes)] public string Tags { get { return string.Join(",", tags); } @@ -639,7 +663,7 @@ namespace Barotrauma { tags.Clear(); // Always add prefab tags - prefab.Tags.ForEach(t => tags.Add(t)); + base.Prefab.Tags.ForEach(t => tags.Add(t)); if (!string.IsNullOrWhiteSpace(value)) { string[] splitTags = value.Split(','); @@ -647,7 +671,7 @@ namespace Barotrauma { string[] splitTag = tag.Trim().Split(':'); splitTag[0] = splitTag[0].ToLowerInvariant(); - tags.Add(string.Join(":", splitTag)); + tags.Add(string.Join(":", splitTag).ToIdentifier()); } } } @@ -705,10 +729,7 @@ namespace Barotrauma private set; } = new List(20); - public string ConfigFile - { - get { return Prefab.FilePath; } - } + public ContentPath ConfigFilePath => Prefab.ContentFile.Path; //which type of inventory slots (head, torso, any, etc) the item can be placed in private readonly HashSet allowedSlots = new HashSet(); @@ -743,7 +764,7 @@ namespace Barotrauma get { return ownInventory; } } - [Editable, Serialize(false, true, description: + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " + "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] public bool DisplaySideBySideWhenLinked { get; set; } @@ -806,13 +827,13 @@ namespace Barotrauma public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } - public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID) + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( - (int)(position.X - itemPrefab.sprite.size.X / 2 * itemPrefab.Scale), - (int)(position.Y + itemPrefab.sprite.size.Y / 2 * itemPrefab.Scale), - (int)(itemPrefab.sprite.size.X * itemPrefab.Scale), - (int)(itemPrefab.sprite.size.Y * itemPrefab.Scale)), - itemPrefab, submarine, id: id) + (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), + (int)(position.Y + itemPrefab.Sprite.size.Y / 2 * itemPrefab.Scale), + (int)(itemPrefab.Sprite.size.X * itemPrefab.Scale), + (int)(itemPrefab.Sprite.size.Y * itemPrefab.Scale)), + itemPrefab, submarine, callOnItemLoaded, id: id) { } @@ -824,11 +845,11 @@ namespace Barotrauma public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true, ushort id = Entity.NullEntityID) : base(itemPrefab, submarine, id) { - spriteColor = prefab.SpriteColor; + spriteColor = base.Prefab.SpriteColor; components = new List(); drawableComponents = new List(); hasComponentsToDraw = false; - tags = new HashSet(); + tags = new HashSet(); repairables = new List(); defaultRect = newRect; @@ -841,7 +862,7 @@ namespace Barotrauma allPropertyObjects.Add(this); - XElement element = itemPrefab.ConfigElement; + ContentXElement element = itemPrefab.ConfigElement; if (element == null) return; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -850,7 +871,7 @@ namespace Barotrauma SetActiveSprite(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -924,7 +945,7 @@ namespace Barotrauma aiTarget = new AITarget(this, subElement); break; default: - ItemComponent ic = ItemComponent.Load(subElement, this, itemPrefab.FilePath); + ItemComponent ic = ItemComponent.Load(subElement, this); if (ic == null) break; AddComponent(ic); @@ -1041,7 +1062,7 @@ namespace Barotrauma { defaultRect = defaultRect }; - foreach (KeyValuePair property in SerializableProperties) + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); @@ -1058,7 +1079,7 @@ namespace Barotrauma for (int i = 0; i < components.Count && i < clone.components.Count; i++) { - foreach (KeyValuePair property in components[i].SerializableProperties) + foreach (KeyValuePair property in components[i].SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); @@ -1269,9 +1290,9 @@ namespace Barotrauma if (!Prefab.AllowDroppingOnSwap || otherItem == null) { return false; } if (Prefab.AllowDroppingOnSwapWith.Any()) { - foreach (string tagOrIdentifier in Prefab.AllowDroppingOnSwapWith) + foreach (Identifier tagOrIdentifier in Prefab.AllowDroppingOnSwapWith) { - if (otherItem.prefab.Identifier.Equals(tagOrIdentifier, StringComparison.OrdinalIgnoreCase)) { return true; } + if (otherItem.Prefab.Identifier == tagOrIdentifier) { return true; } if (otherItem.HasTag(tagOrIdentifier)) { return true; } } return false; @@ -1386,31 +1407,22 @@ namespace Barotrauma public Item GetRootContainer() { if (Container == null) { return null; } - Item rootContainer = Container; while (rootContainer.Container != null) { rootContainer = rootContainer.Container; } - return rootContainer; } - /// - /// Should this item or any of its containers be ignored by the AI? - /// - public bool IsThisOrAnyContainerIgnoredByAI(Character character) + public bool HasAccess(Character character) { - if (IgnoreByAI(character)) { return true; } - if (Container == null) { return false; } - if (Container.IgnoreByAI(character)) { return true; } - var container = Container; - while (container.Container != null) - { - container = container.Container; - if (container.IgnoreByAI(character)) { return true; } - } - return false; + if (character.IsBot && IgnoreByAI(character)) { return false; } + if (!IsInteractable(character)) { return false; } + var itemContainer = GetComponent(); + if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; } + if (Container != null && !Container.HasAccess(character)) { return false; } + return true; } public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null; @@ -1449,33 +1461,49 @@ namespace Barotrauma } public void AddTag(string tag) + { + AddTag(tag.ToIdentifier()); + } + + public void AddTag(Identifier tag) { if (tags.Contains(tag)) { return; } tags.Add(tag); } + public bool HasTag(string tag) + { + return HasTag(tag.ToIdentifier()); + } + + public bool HasTag(Identifier tag) { if (tag == null) { return true; } - return tags.Contains(tag) || prefab.Tags.Contains(tag); + return tags.Contains(tag) || base.Prefab.Tags.Contains(tag); } public void ReplaceTag(string tag, string newTag) + { + ReplaceTag(tag.ToIdentifier(), newTag.ToIdentifier()); + } + + public void ReplaceTag(Identifier tag, Identifier newTag) { if (!tags.Contains(tag)) { return; } tags.Remove(tag); tags.Add(newTag); } - public IEnumerable GetTags() + public IEnumerable GetTags() { return tags; } - public bool HasTag(IEnumerable allowedTags) + public bool HasTag(IEnumerable allowedTags) { if (allowedTags == null) return true; - foreach (string tag in allowedTags) + foreach (Identifier tag in allowedTags) { if (tags.Contains(tag)) return true; } @@ -1528,7 +1556,7 @@ namespace Barotrauma foreach (Item containedItem in ContainedItems) { if (effect.TargetIdentifiers != null && - !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) && + !effect.TargetIdentifiers.Contains(((MapEntity)containedItem).Prefab.Identifier) && !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id))) { continue; @@ -1731,7 +1759,7 @@ namespace Barotrauma UpdateTransform(); if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { - Spawner?.AddToRemoveQueue(this); + Spawner?.AddItemToRemoveQueue(this); return; } } @@ -1860,7 +1888,7 @@ namespace Barotrauma //apply simple angular drag body.ApplyTorque(body.AngularVelocity * volume * -0.05f); - } + } private bool OnCollision(Fixture f1, Fixture f2, Contact contact) @@ -1871,14 +1899,9 @@ namespace Barotrauma if (projectile != null) { //ignore character colliders (a projectile only hits limbs) - if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) - { - return false; - } - if (projectile.IgnoredBodies != null) - { - if (projectile.IgnoredBodies.Contains(f2.Body)) { return false; } - } + if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; } + if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; } + if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; } } contact.GetWorldManifold(out Vector2 normal, out _); @@ -2027,17 +2050,17 @@ namespace Barotrauma return connectedComponents; } - public static readonly (string input, string output)[] connectionPairs = new (string input, string output)[] + public static readonly ImmutableArray<(Identifier Input, Identifier Output)> connectionPairs = new (Identifier, Identifier)[] { - ("power_in", "power_out"), - ("signal_in1", "signal_out1"), - ("signal_in2", "signal_out2"), - ("signal_in3", "signal_out3"), - ("signal_in4", "signal_out4"), - ("signal_in", "signal_out"), - ("signal_in1", "signal_out"), - ("signal_in2", "signal_out") - }; + ("power_in".ToIdentifier(), "power_out".ToIdentifier()), + ("signal_in1".ToIdentifier(), "signal_out1".ToIdentifier()), + ("signal_in2".ToIdentifier(), "signal_out2".ToIdentifier()), + ("signal_in3".ToIdentifier(), "signal_out3".ToIdentifier()), + ("signal_in4".ToIdentifier(), "signal_out4".ToIdentifier()), + ("signal_in".ToIdentifier(), "signal_out".ToIdentifier()), + ("signal_in1".ToIdentifier(), "signal_out".ToIdentifier()), + ("signal_in2".ToIdentifier(), "signal_out".ToIdentifier()) + }.ToImmutableArray(); private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent { @@ -2078,34 +2101,30 @@ namespace Barotrauma if (relay != null && !relay.IsOn) { return; } } - foreach ((string input, string output) in connectionPairs) + foreach ((Identifier input, Identifier output) in connectionPairs) { - if (input == c.Name) + void searchFromAToB(Identifier connectionEndA, Identifier connectionEndB) { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == output); - if (pairedConnection != null) + if (connectionEndA == c.Name) { - if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); - } - } - else if (output == c.Name) - { - var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == input); - if (pairedConnection != null) - { - if (alreadySearched.Contains(pairedConnection)) { continue; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); + var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionEndB); + if (pairedConnection != null) + { + if (alreadySearched.Contains(pairedConnection)) { return; } + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); + } } } + searchFromAToB(input, output); + searchFromAToB(output, input); } } - public Controller FindController(string[] tags = null) + public Controller FindController(ImmutableArray? tags = null) { //try finding the controller with the simpler non-recursive method first var controllers = GetConnectedComponents(); - bool needsTag = tags != null && tags.Length > 0; + bool needsTag = tags != null && tags.Value.Length > 0; if (controllers.None() || (needsTag && controllers.None(c => c.Item.HasTag(tags)))) { controllers = GetConnectedComponents(recursive: true); @@ -2119,7 +2138,7 @@ namespace Barotrauma controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault(); } - public bool TryFindController(out Controller controller, string[] tags = null) + public bool TryFindController(out Controller controller, ImmutableArray? tags = null) { controller = FindController(tags: tags); return controller != null; @@ -2291,11 +2310,12 @@ namespace Barotrauma //to prevent accidentally selecting items when clicking UI elements if (user == Character.Controlled && GUI.MouseOn != null) { - if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) + if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.PickKey].MouseButton == 0) { pickHit = false; } - if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) + + if (GameSettings.CurrentConfig.KeyMap.Bindings[ic.SelectKey].MouseButton == 0) { selectHit = false; } @@ -2308,7 +2328,7 @@ namespace Barotrauma //LMB is used to manipulate wires, so using E to select connection panels is much easier if (Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode) { - pickHit = selectHit = GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None ? + pickHit = selectHit = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Use].MouseButton == MouseButton.None ? user.IsKeyHit(InputType.Use) : user.IsKeyHit(InputType.Select); } @@ -2356,8 +2376,9 @@ namespace Barotrauma { if (requiredSkill != null) { - GUI.AddMessage(TextManager.GetWithVariables("InsufficientSkills", new string[2] { "[requiredskill]", "[requiredlevel]" }, - new string[2] { TextManager.Get("SkillName." + requiredSkill.Identifier), ((int)(requiredSkill.Level * skillMultiplier)).ToString() }, new bool[2] { true, false }), GUI.Style.Red); + GUI.AddMessage(TextManager.GetWithVariables("InsufficientSkills", + ("[requiredskill]", TextManager.Get("SkillName." + requiredSkill.Identifier), FormatCapitals.Yes), + ("[requiredlevel]", ((int)(requiredSkill.Level * skillMultiplier)).ToString(), FormatCapitals.No)), GUIStyle.Red); } } #endif @@ -2423,7 +2444,7 @@ namespace Barotrauma if (remove) { - Spawner.AddToRemoveQueue(this); + Spawner.AddItemToRemoveQueue(this); } } @@ -2456,7 +2477,7 @@ namespace Barotrauma if (remove) { - Spawner.AddToRemoveQueue(this); + Spawner.AddItemToRemoveQueue(this); } } @@ -2520,7 +2541,7 @@ namespace Barotrauma } - if (remove) { Spawner?.AddToRemoveQueue(this); } + if (remove) { Spawner?.AddItemToRemoveQueue(this); } } public bool Combine(Item item, Character user) @@ -2644,6 +2665,10 @@ namespace Barotrauma { msg.Write(stringVal); } + else if (value is Identifier idValue) + { + msg.Write(idValue); + } else if (value is float floatVal) { msg.Write(floatVal); @@ -2770,6 +2795,12 @@ namespace Barotrauma property.TrySetValue(parentObject, val); } } + else if (type == typeof(Identifier)) + { + Identifier val = msg.ReadIdentifier(); + logValue = val.Value; + if (allowEditing) { property.TrySetValue(parentObject, val); } + } else if (type == typeof(float)) { float val = msg.ReadSingle(); @@ -2887,7 +2918,7 @@ namespace Barotrauma partial void UpdateNetPosition(float deltaTime); - public static Item Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Item Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { return Load(element, submarine, createNetworkEvent: false, idRemap: idRemap); } @@ -2899,12 +2930,12 @@ namespace Barotrauma /// The submarine to spawn the item in (can be null) /// Should an EntitySpawner event be created to notify clients about the item being created. /// - public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) + public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) { string name = element.Attribute("name").Value; - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); - if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(identifier)) + if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty) { string errorMessage = "Failed to load an item (both name and identifier were null):\n"+element.ToString(); DebugConsole.ThrowError(errorMessage); @@ -2912,15 +2943,15 @@ namespace Barotrauma return null; } - string pendingSwap = element.GetAttributeString("pendingswap", ""); + Identifier pendingSwap = element.GetAttributeIdentifier("pendingswap", Identifier.Empty); ItemPrefab appliedSwap = null; ItemPrefab oldPrefab = null; - if (!string.IsNullOrEmpty(pendingSwap) && Level.Loaded?.Type != LevelData.LevelType.Outpost) + if (!pendingSwap.IsEmpty && Level.Loaded?.Type != LevelData.LevelType.Outpost) { oldPrefab = ItemPrefab.Find(name, identifier); appliedSwap = ItemPrefab.Find(string.Empty, pendingSwap); identifier = pendingSwap; - pendingSwap = null; + pendingSwap = Identifier.Empty; } ItemPrefab prefab = ItemPrefab.Find(name, identifier); @@ -2930,8 +2961,8 @@ namespace Barotrauma Vector2 centerPos = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2); if (appliedSwap != null) { - rect.Width = (int)(prefab.sprite.size.X * prefab.Scale); - rect.Height = (int)(prefab.sprite.size.Y * prefab.Scale); + rect.Width = (int)(prefab.Sprite.size.X * prefab.Scale); + rect.Height = (int)(prefab.Sprite.size.Y * prefab.Scale); } else if (rect.Width == 0 && rect.Height == 0) { @@ -2943,7 +2974,7 @@ namespace Barotrauma { Submarine = submarine, linkedToID = new List(), - PendingItemSwap = string.IsNullOrEmpty(pendingSwap) ? null : MapEntityPrefab.Find(pendingSwap) as ItemPrefab + PendingItemSwap = pendingSwap.IsEmpty ? null : MapEntityPrefab.Find(pendingSwap.Value) as ItemPrefab }; #if SERVER @@ -2955,11 +2986,11 @@ namespace Barotrauma foreach (XAttribute attribute in (appliedSwap?.ConfigElement ?? element).Attributes()) { - if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) { continue; } + if (!item.SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } bool shouldBeLoaded = false; foreach (var propertyAttribute in property.Attributes.OfType()) { - if (propertyAttribute.isSaveable) + if (propertyAttribute.IsSaveable == IsPropertySaveable.Yes) { shouldBeLoaded = true; break; @@ -2975,19 +3006,18 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType().Any() && (submarine == null || !submarine.Loading)) { - switch (property.Name) + if (property.Name == "Tags" || + property.Name == "Condition" || + property.Name == "Description") { - case "Tags": - case "Condition": - case "Description": - //these can be ignored, they're always written in the spawn data - break; - default: - if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) - { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property }); - } - break; + //these can be ignored, they're always written in the spawn data + } + else + { + if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) + { + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property }); + } } } } @@ -2999,15 +3029,15 @@ namespace Barotrauma //if we're overriding a non-overridden item in a sub/assembly xml or vice versa, //use the values from the prefab instead of loading them from the sub/assembly xml - bool usePrefabValues = thisIsOverride != prefab.IsOverride || appliedSwap != null; + bool usePrefabValues = thisIsOverride != ItemPrefab.Prefabs.IsOverride(prefab) || appliedSwap != null; List unloadedComponents = new List(item.components); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "upgrade": { - var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty); + var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier); int level = subElement.GetAttributeInt("level", 1); if (upgradePrefab != null) @@ -3039,8 +3069,8 @@ namespace Barotrauma item.Upgrades.ForEach(upgrade => upgrade.ApplyUpgrade()); - var availableSwapIds = element.GetAttributeStringArray("availableswaps", new string[0]); - foreach (string swapId in availableSwapIds) + var availableSwapIds = element.GetAttributeIdentifierArray("availableswaps", Array.Empty()); + foreach (Identifier swapId in availableSwapIds) { ItemPrefab swapPrefab = ItemPrefab.Find(string.Empty, swapId); if (swapPrefab != null) @@ -3140,7 +3170,7 @@ namespace Barotrauma if (Rotation != 0f) { element.Add(new XAttribute("rotation", Rotation)); } - if (Prefab.IsOverride) { element.Add(new XAttribute("isoverride", "true")); } + if (ItemPrefab.Prefabs.IsOverride(Prefab)) { element.Add(new XAttribute("isoverride", "true")); } if (FlippedX) { element.Add(new XAttribute("flippedx", true)); } if (FlippedY) { element.Add(new XAttribute("flippedy", true)); } @@ -3332,7 +3362,7 @@ namespace Barotrauma List list = new List(ItemList); foreach (Item item in list) { - if (item.prefab == prefab) + if (((MapEntity)item).Prefab == prefab) { item.Remove(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 40cfa040f..23137fab7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -4,15 +4,16 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.Linq; +using Barotrauma.Extensions; +using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma { - struct DeconstructItem + readonly struct DeconstructItem { - public readonly string ItemIdentifier; + public readonly Identifier ItemIdentifier; //minCondition does <= check, meaning that below or equal to min condition will be skipped. public readonly float MinCondition; //maxCondition does > check, meaning that above this max the deconstruct item will be skipped. @@ -21,6 +22,7 @@ namespace Barotrauma public readonly float OutConditionMin, OutConditionMax; //should the condition of the deconstructed item be copied to the output items public readonly bool CopyCondition; + //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this public readonly string[] RequiredDeconstructor; //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this @@ -30,11 +32,11 @@ namespace Barotrauma public readonly string InfoText; public readonly string InfoTextOnOtherItemMissing; - public float Commonness { get; } + public readonly float Commonness; - public DeconstructItem(XElement element, string parentDebugName) + public DeconstructItem(XElement element, Identifier parentDebugName) { - ItemIdentifier = element.GetAttributeString("identifier", "notfound"); + ItemIdentifier = element.GetAttributeIdentifier("identifier", ""); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f)); @@ -56,85 +58,113 @@ namespace Barotrauma class FabricationRecipe { - public class RequiredItem + public abstract class RequiredItem { - public readonly List ItemPrefabs; - public int Amount; + public abstract IEnumerable ItemPrefabs { get; } + public abstract UInt32 UintIdentifier { get; } + + public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition) + { + Amount = amount; + MinCondition = minCondition; + MaxCondition = maxCondition; + UseCondition = useCondition; + } + public readonly int Amount; public readonly float MinCondition; public readonly float MaxCondition; public readonly bool UseCondition; + } - public RequiredItem(ItemPrefab itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) - { - ItemPrefabs = new List() { itemPrefab }; - Amount = amount; - MinCondition = minCondition; - MaxCondition = maxCondition; - UseCondition = useCondition; - } + public class RequiredItemByIdentifier : RequiredItem + { + public readonly Identifier ItemPrefabIdentifier; + public ItemPrefab ItemPrefab => ItemPrefab.Prefabs[ItemPrefabIdentifier]; + public override UInt32 UintIdentifier { get; } - public RequiredItem(IEnumerable itemPrefabs, int amount, float minCondition, float maxCondition, bool useCondition) + public override IEnumerable ItemPrefabs => ItemPrefab.ToEnumerable(); + + public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) { - ItemPrefabs = new List(itemPrefabs); - Amount = amount; - MinCondition = minCondition; - MaxCondition = maxCondition; - UseCondition = useCondition; + ItemPrefabIdentifier = itemPrefab; + using MD5 md5 = MD5.Create(); + UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); } } - public readonly ItemPrefab TargetItem; - public readonly string DisplayName; - public readonly List RequiredItems; - public readonly string[] SuitableFabricatorIdentifiers; + public class RequiredItemByTag : RequiredItem + { + public readonly Identifier Tag; + public override UInt32 UintIdentifier { get; } + + public override IEnumerable ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)); + + public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition) + { + Tag = tag; + using MD5 md5 = MD5.Create(); + UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); + } + } + + public readonly Identifier TargetItemPrefabIdentifier; + public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; + + private Lazy displayName; + public LocalizedString DisplayName + => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; + public readonly ImmutableArray RequiredItems; + public readonly ImmutableArray SuitableFabricatorIdentifiers; public readonly float RequiredTime; public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 - public readonly List RequiredSkills; + public readonly ImmutableArray RequiredSkills; + public readonly uint RecipeHash; + public readonly int Amount; - public int Amount { get; } - - public FabricationRecipe(XElement element, ItemPrefab itemPrefab) + public FabricationRecipe(XElement element, Identifier itemPrefab) { - TargetItem = itemPrefab; - string displayName = element.GetAttributeString("displayname", ""); - DisplayName = string.IsNullOrEmpty(displayName) ? itemPrefab.Name : TextManager.GetWithVariable($"DisplayName.{displayName}", "[itemname]", itemPrefab.Name); + TargetItemPrefabIdentifier = itemPrefab; + var displayNameIdentifier = element.GetAttributeIdentifier("displayname", ""); + displayName = new Lazy(() => displayNameIdentifier.IsEmpty + ? TargetItem.Name + : TextManager.GetWithVariable($"DisplayName.{displayNameIdentifier}", "[itemname]", TargetItem.Name)); - SuitableFabricatorIdentifiers = element.GetAttributeStringArray("suitablefabricators", new string[0]); + SuitableFabricatorIdentifiers = element.GetAttributeIdentifierArray("suitablefabricators", Array.Empty()).ToImmutableArray(); - RequiredSkills = new List(); + var requiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { - DebugConsole.AddWarning($"Error in \"{itemPrefab.Name}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); + DebugConsole.AddWarning($"Error in \"{itemPrefab}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); } - RequiredItems = new List(); + var requiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "requiredskill": if (subElement.Attribute("name") != null) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Use skill identifiers instead of names."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! Use skill identifiers instead of names."); continue; } - RequiredSkills.Add(new Skill( - subElement.GetAttributeString("identifier", ""), + requiredSkills.Add(new Skill( + subElement.GetAttributeIdentifier("identifier", ""), subElement.GetAttributeInt("level", 0))); break; case "item": case "requireditem": - string requiredItemIdentifier = subElement.GetAttributeString("identifier", ""); - string requiredItemTag = subElement.GetAttributeString("tag", ""); - if (string.IsNullOrWhiteSpace(requiredItemIdentifier) && string.IsNullOrEmpty(requiredItemTag)) + Identifier requiredItemIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + Identifier requiredItemTag = subElement.GetAttributeIdentifier("tag", Identifier.Empty); + if (requiredItemIdentifier == Identifier.Empty && requiredItemTag == Identifier.Empty) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! One of the required items has no identifier or tag."); + DebugConsole.ThrowError("Error in fabricable item " + itemPrefab + "! One of the required items has no identifier or tag."); continue; } @@ -144,69 +174,76 @@ namespace Barotrauma bool useCondition = subElement.GetAttributeBool("usecondition", true); int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1)); - if (!string.IsNullOrEmpty(requiredItemIdentifier)) + if (requiredItemIdentifier != Identifier.Empty) { - if (!(MapEntityPrefab.Find(null, requiredItemIdentifier.Trim()) is ItemPrefab requiredItem)) + var existing = requiredItems.FindIndex(r => + r is RequiredItemByIdentifier ri && + ri.ItemPrefabIdentifier == requiredItemIdentifier && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); + if (existing >= 0) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Required item \"" + requiredItemIdentifier + "\" not found."); - continue; - } - - var existing = RequiredItems.Find(r => - r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && - MathUtils.NearlyEqual(r.MinCondition, minCondition) && MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); - if (existing == null) - { - RequiredItems.Add(new RequiredItem(requiredItem, amount, minCondition, maxCondition, useCondition)); - } - else - { - existing.Amount += amount; + amount += requiredItems[existing].Amount; + requiredItems.RemoveAt(existing); } + requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition)); } else { - var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Any(t => t.Equals(requiredItemTag, StringComparison.OrdinalIgnoreCase))); - if (!matchingItems.Any()) + var existing = requiredItems.FindIndex(r => + r is RequiredItemByTag rt && + rt.Tag == requiredItemTag && + MathUtils.NearlyEqual(r.MinCondition, minCondition) && + MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); + if (existing >= 0) { - DebugConsole.ThrowError("Error in fabricable item " + itemPrefab.Name + "! Could not find any items with the tag \"" + requiredItemTag + "\"."); - continue; - } - - var existing = RequiredItems.Find(r => - r.ItemPrefabs.SequenceEqual(matchingItems) && - MathUtils.NearlyEqual(r.MinCondition, minCondition) && - MathUtils.NearlyEqual(r.MaxCondition, maxCondition)); - if (existing == null) - { - RequiredItems.Add(new RequiredItem(matchingItems, amount, minCondition, maxCondition, useCondition)); - } - else - { - existing.Amount += amount; + amount += requiredItems[existing].Amount; + requiredItems.RemoveAt(existing); } + requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition)); } break; } } + + this.RequiredSkills = requiredSkills.ToImmutableArray(); + this.RequiredItems = requiredItems.ToImmutableArray(); + + RecipeHash = GenerateHash(); + } + + private uint GenerateHash() + { + using var md5 = MD5.Create(); + uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5); + + var requiredItems = string.Join(':', RequiredItems + .Select(i => i.UintIdentifier) + .Select(i => string.Join(',', i))); + + var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}")); + + uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5); + if (retVal == 0) { retVal = 1; } + return retVal; } } class PreferredContainer { - public readonly HashSet Primary = new HashSet(); - public readonly HashSet Secondary = new HashSet(); + public readonly ImmutableHashSet Primary; + public readonly ImmutableHashSet Secondary; - public float SpawnProbability { get; private set; } - public float MaxCondition { get; private set; } - public float MinCondition { get; private set; } - public int MinAmount { get; private set; } - public int MaxAmount { get; private set; } + public readonly float SpawnProbability; + public readonly float MaxCondition; + public readonly float MinCondition; + public readonly int MinAmount; + public readonly int MaxAmount; public PreferredContainer(XElement element) { - Primary = XMLExtensions.GetAttributeStringArray(element, "primary", new string[0]).ToHashSet(); - Secondary = XMLExtensions.GetAttributeStringArray(element, "secondary", new string[0]).ToHashSet(); + Primary = XMLExtensions.GetAttributeIdentifierArray(element, "primary", Array.Empty()).ToImmutableHashSet(); + Secondary = XMLExtensions.GetAttributeIdentifierArray(element, "secondary", Array.Empty()).ToImmutableHashSet(); SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); @@ -236,7 +273,7 @@ namespace Barotrauma public readonly bool CanBeBought; - public readonly string ReplacementOnUninstall; + public readonly Identifier ReplacementOnUninstall; public string SpawnWithId; @@ -244,7 +281,7 @@ namespace Barotrauma public readonly Vector2 SwapOrigin; - public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>(); + public List<(Identifier requiredTag, Identifier swapTo)> ConnectedItemsToSwap = new List<(Identifier requiredTag, Identifier swapTo)>(); public readonly Sprite SchematicSprite; @@ -254,16 +291,16 @@ namespace Barotrauma return location?.GetAdjustedMechanicalCost(price) ?? price; } - public SwappableItem(XElement element) + public SwappableItem(ContentXElement element) { BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0); SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty); CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); - ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", ""); + ReplacementOnUninstall = element.GetAttributeIdentifier("replacementonuninstall", ""); SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -272,319 +309,53 @@ namespace Barotrauma break; case "swapconnecteditem": ConnectedItemsToSwap.Add( - (subElement.GetAttributeString("tag", ""), - subElement.GetAttributeString("swapto", ""))); + (subElement.GetAttributeIdentifier("tag", ""), + subElement.GetAttributeIdentifier("swapto", ""))); break; } } } } - partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier + partial class ItemPrefab : MapEntityPrefab, IImplementsVariants { - private readonly string name; - public override string Name => name; - public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - Item.RemoveByPrefab(this); - } - //default size - protected Vector2 size; + public Vector2 Size { get; private set; } - private float impactTolerance; - public readonly PriceInfo DefaultPrice; - private readonly Dictionary locationPrices; + private PriceInfo defaultPrice; + public PriceInfo DefaultPrice => defaultPrice; + + private ImmutableDictionary locationPrices; /// /// Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true, the character /// has to be within the trigger to interact. If it's set to false, having the cursor within the trigger is enough. /// - public List Triggers; + public ImmutableArray Triggers { get; private set; } + private ImmutableDictionary treatmentSuitability; private readonly List fabricationRecipeElements = new List(); - private readonly Dictionary treatmentSuitability = new Dictionary(); - /// /// Is this prefab overriding a prefab in another content package /// - public bool IsOverride; + public bool IsOverride => Prefabs.IsOverride(this); - public readonly ItemPrefab VariantOf; + private readonly XElement originalElement; + public ContentXElement ConfigElement { get; private set; } - public XElement ConfigElement - { - get; - private set; - } + public ImmutableArray DeconstructItems { get; private set; } - public List DeconstructItems - { - get; - private set; - } + public ImmutableDictionary FabricationRecipes { get; private set; } - public List FabricationRecipes - { - get; - private set; - } + public float DeconstructTime { get; private set; } - public float DeconstructTime - { - get; - private set; - } - - public bool AllowDeconstruct - { - get; - private set; - } - - //how close the Character has to be to the item to pick it up - [Serialize(120.0f, false)] - public float InteractDistance - { - get; - private set; - } - - // this can be used to allow items which are behind other items tp - [Serialize(0.0f, false)] - public float InteractPriority - { - get; - private set; - } - - [Serialize(false, false)] - public bool InteractThroughWalls - { - get; - private set; - } - - [Serialize(false, false, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] - public bool HideConditionBar { get; set; } - - [Serialize(false, false, description: "Hides the condition displayed in the item's tooltip.")] - public bool HideConditionInTooltip { get; set; } - - //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item - //if false, trigger areas define areas that can be used to highlight the item - [Serialize(true, false)] - public bool RequireBodyInsideTrigger - { - get; - private set; - } - - //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger - [Serialize(false, false)] - public bool RequireCursorInsideTrigger - { - get; - private set; - } - - //if true then players can only highlight the item if its targeted for interaction by a campaign event - [Serialize(false, false)] - public bool RequireCampaignInteract - { - get; - private set; - } - - //should the camera focus on the item when selected - [Serialize(false, false)] - public bool FocusOnSelected - { - get; - private set; - } - - //the amount of "camera offset" when selecting the construction - [Serialize(0.0f, false)] - public float OffsetOnSelected - { - get; - private set; - } - - [Serialize(100.0f, false)] - public float Health - { - get; - private set; - } - - [Serialize(false, false)] - public bool AllowSellingWhenBroken - { - get; - private set; - } - - [Serialize(false, false)] - public bool Indestructible - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByExplosions - { - get; - private set; - } - - [Serialize(1f, false)] - public float ExplosionDamageMultiplier - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByProjectiles - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByMeleeWeapons - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByRepairTools - { - get; - private set; - } - - [Serialize(false, false)] - public bool DamagedByMonsters - { - get; - private set; - } - - [Serialize(false, false)] - public bool FireProof - { - get; - private set; - } - - [Serialize(false, false)] - public bool WaterProof - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float ImpactTolerance - { - get { return impactTolerance; } - set { impactTolerance = Math.Max(value, 0.0f); } - } - - [Serialize(0.0f, false)] - public float OnDamagedThreshold { get; set; } - - [Serialize(0.0f, false)] - public float SonarSize - { - get; - private set; - } - - [Serialize(false, false)] - public bool UseInHealthInterface - { - get; - private set; - } - - [Serialize(false, false)] - public bool DisableItemUsageWhenSelected - { - get; - private set; - } - - [Serialize("", false)] - public string CargoContainerIdentifier - { - get; - private set; - } - - [Serialize(false, false)] - public bool UseContainedSpriteColor - { - get; - private set; - } - - [Serialize(false, false)] - public bool UseContainedInventoryIconColor - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float AddedRepairSpeedMultiplier - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float AddedPickingSpeedMultiplier - { - get; - private set; - } - - [Serialize(false, false)] - public bool CannotRepairFail - { - get; - private set; - } - - [Serialize(null, false)] - public string EquipConfirmationText { get; set; } - - [Serialize(true, false, description: "Can the item be rotated in the submarine editor.")] - public bool AllowRotatingInEditor { get; set; } - - [Serialize(false, false)] - public bool ShowContentsInTooltip { get; private set; } + public bool AllowDeconstruct { get; private set; } //Containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced. - public List PreferredContainers - { - get; - private set; - } = new List(); + public ImmutableArray PreferredContainers { get; private set; } public SwappableItem SwappableItem { @@ -594,344 +365,371 @@ namespace Barotrauma /// /// How likely it is for the item to spawn in a level of a given type. - /// Key = name of the LevelGenerationParameters (empty string = default value) + /// Key = name of the LevelGenerationParameters (empty string = default value) /* TODO: empty string = default value???? */ /// Value = commonness /// - public Dictionary LevelCommonness - { - get; - private set; - } = new Dictionary(); + public ImmutableDictionary LevelCommonness { get; private set; } - public Dictionary LevelQuantity + public readonly struct FixedQuantityResourceInfo { - get; - } = new Dictionary(); - - public struct FixedQuantityResourceInfo - { - public int ClusterQuantity { get; } - public int ClusterSize { get; } - public bool IsIslandSpecifc { get; } - public bool AllowAtStart { get; } + public readonly int ClusterQuantity; + public readonly int ClusterSize; + public readonly bool IsIslandSpecific; + public readonly bool AllowAtStart; public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific, bool allowAtStart) { ClusterQuantity = clusterQuantity; ClusterSize = clusterSize; - IsIslandSpecifc = isIslandSpecific; + IsIslandSpecific = isIslandSpecific; AllowAtStart = allowAtStart; } } - [Serialize(true, false)] - public bool CanFlipX { get; private set; } - - [Serialize(true, false)] - public bool CanFlipY { get; private set; } - - [Serialize(false, false)] - public bool IsDangerous { get; private set; } + public ImmutableDictionary LevelQuantity { get; private set; } public bool CanSpriteFlipX { get; private set; } public bool CanSpriteFlipY { get; private set; } - private int maxStackSize; - [Serialize(1, false)] - public int MaxStackSize - { - get { return maxStackSize; } - set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } - } - - [Serialize(false, false)] - public bool AllowDroppingOnSwap { get; private set; } - - private readonly HashSet allowDroppingOnSwapWith = new HashSet(); - public IEnumerable AllowDroppingOnSwapWith - { - get { return allowDroppingOnSwapWith; } - } - - public Vector2 Size => size; - - public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); - /// /// Can the item be chosen as extra cargo in multiplayer. If not set, the item is available if it can be bought from outposts in the campaign. /// - public bool? AllowAsExtraCargo; + public bool? AllowAsExtraCargo { get; private set; } + + public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); /// /// Any item with a Price element in the definition can be sold everywhere. /// public bool CanBeSold => DefaultPrice != null; - public bool RandomDeconstructionOutput { get; } + public bool RandomDeconstructionOutput { get; private set; } - public int RandomDeconstructionOutputAmount { get; } + public int RandomDeconstructionOutputAmount { get; private set; } - public uint UIntIdentifier { get; set; } + private Sprite sprite; + public override Sprite Sprite => sprite; - public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath); + public override string OriginalName { get; } - public static void LoadFromFile(ContentFile file) + private LocalizedString name; + public override LocalizedString Name => name; + + private ImmutableHashSet tags; + public override ImmutableHashSet Tags => tags; + + private ImmutableHashSet allowedLinks; + public override ImmutableHashSet AllowedLinks => allowedLinks; + + private MapEntityCategory category; + public override MapEntityCategory Category => category; + + private ImmutableHashSet aliases; + public override ImmutableHashSet Aliases => aliases; + + //how close the Character has to be to the item to pick it up + [Serialize(120.0f, IsPropertySaveable.No)] + public float InteractDistance { get; private set; } + + // this can be used to allow items which are behind other items tp + [Serialize(0.0f, IsPropertySaveable.No)] + public float InteractPriority { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool InteractThroughWalls { get; private set; } + + [Serialize(false, IsPropertySaveable.No, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] + public bool HideConditionBar { get; set; } + + [Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")] + public bool HideConditionInTooltip { get; set; } + + //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item + //if false, trigger areas define areas that can be used to highlight the item + [Serialize(true, IsPropertySaveable.No)] + public bool RequireBodyInsideTrigger { get; private set; } + + //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger + [Serialize(false, IsPropertySaveable.No)] + public bool RequireCursorInsideTrigger { get; private set; } + + //if true then players can only highlight the item if its targeted for interaction by a campaign event + [Serialize(false, IsPropertySaveable.No)] + public bool RequireCampaignInteract { - DebugConsole.Log("*** " + file.Path + " ***"); - RemoveByFile(file.Path); - - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - - var rootElement = doc.Root; - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "item": - new ItemPrefab(rootElement, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - break; - case "items": - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var itemElement = element.GetChildElement("item"); - if (itemElement != null) - { - new ItemPrefab(itemElement, file.Path, true) - { - ContentPackage = file.ContentPackage, - IsOverride = true - }; - } - else - { - DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); - } - } - else - { - new ItemPrefab(element, file.Path, false) - { - ContentPackage = file.ContentPackage - }; - } - } - break; - case "override": - var items = rootElement.GetChildElement("items"); - if (items != null) - { - foreach (var element in items.Elements()) - { - new ItemPrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage, - IsOverride = true - }; - } - } - foreach (var element in rootElement.GetChildElements("item")) - { - new ItemPrefab(element, file.Path, true) - { - ContentPackage = file.ContentPackage - }; - } - break; - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); - break; - } + get; + private set; } - public static void LoadAll(IEnumerable files) + //should the camera focus on the item when selected + [Serialize(false, IsPropertySaveable.No)] + public bool FocusOnSelected { get; private set; } + + //the amount of "camera offset" when selecting the construction + [Serialize(0.0f, IsPropertySaveable.No)] + public float OffsetOnSelected { get; private set; } + + [Serialize(100.0f, IsPropertySaveable.No)] + public float Health { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowSellingWhenBroken { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool Indestructible { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByExplosions { get; private set; } + + [Serialize(1f, IsPropertySaveable.No)] + public float ExplosionDamageMultiplier { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByProjectiles { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByMeleeWeapons { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByRepairTools { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DamagedByMonsters { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool FireProof { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool WaterProof { get; private set; } + + private float impactTolerance; + [Serialize(0.0f, IsPropertySaveable.No)] + public float ImpactTolerance { - DebugConsole.Log("Loading item prefabs: "); - - foreach (ContentFile file in files) - { - LoadFromFile(file); - } - - //initialize item requirements for fabrication recipes - //(has to be done after all the item prefabs have been loaded, because the - //recipe ingredients may not have been loaded yet when loading the prefab) - InitFabricationRecipes(); + get { return impactTolerance; } + set { impactTolerance = Math.Max(value, 0.0f); } } - public static void InitFabricationRecipes() + [Serialize(0.0f, IsPropertySaveable.No)] + public float OnDamagedThreshold { get; set; } + + [Serialize(0.0f, IsPropertySaveable.No)] + public float SonarSize { - foreach (ItemPrefab itemPrefab in Prefabs) - { - itemPrefab.FabricationRecipes.Clear(); - foreach (XElement fabricationRecipe in itemPrefab.fabricationRecipeElements) - { - itemPrefab.FabricationRecipes.Add(new FabricationRecipe(fabricationRecipe, itemPrefab)); - } - } + get; + private set; } - public static string GenerateLegacyIdentifier(string name) + [Serialize(false, IsPropertySaveable.No)] + public bool UseInHealthInterface { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool DisableItemUsageWhenSelected { get; private set; } + + [Serialize("", IsPropertySaveable.No)] + public string CargoContainerIdentifier { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool UseContainedSpriteColor { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool UseContainedInventoryIconColor { get; private set; } + + [Serialize(0.0f, IsPropertySaveable.No)] + public float AddedRepairSpeedMultiplier { - return "legacyitem_" + name.ToLowerInvariant().Replace(" ", ""); + get; + private set; } - public ItemPrefab(XElement element, string filePath, bool allowOverriding) + [Serialize(0.0f, IsPropertySaveable.No)] + public float AddedPickingSpeedMultiplier { - FilePath = filePath; - ConfigElement = element; + get; + private set; + } - originalName = element.GetAttributeString("name", ""); - name = originalName; - identifier = element.GetAttributeString("identifier", ""); + [Serialize(false, IsPropertySaveable.No)] + public bool CannotRepairFail + { + get; + private set; + } - string variantOf = element.GetAttributeString("variantof", ""); - if (!string.IsNullOrEmpty(variantOf)) + [Serialize(null, IsPropertySaveable.No)] + public string EquipConfirmationText { get; set; } + + [Serialize(true, IsPropertySaveable.No, description: "Can the item be rotated in the submarine editor.")] + public bool AllowRotatingInEditor { get; set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool ShowContentsInTooltip { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool CanFlipX { get; private set; } + + [Serialize(true, IsPropertySaveable.No)] + public bool CanFlipY { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool IsDangerous { get; private set; } + + private int maxStackSize; + [Serialize(1, IsPropertySaveable.No)] + public int MaxStackSize + { + get { return maxStackSize; } + private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } + } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowDroppingOnSwap { get; private set; } + + public ImmutableHashSet AllowDroppingOnSwapWith { get; private set; } + + protected override Identifier DetermineIdentifier(XElement element) + { + Identifier identifier = base.DetermineIdentifier(element); + string originalName = element.GetAttributeString("name", ""); + if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName)) { - ItemPrefab basePrefab = Find(null, variantOf); - if (basePrefab == null) - { - DebugConsole.ThrowError($"Failed to load the item variant \"{identifier}\" - could not find the base prefab \"{variantOf}\""); - } - else - { - VariantOf = basePrefab; - ConfigElement = element = CreateVariantXML(element, basePrefab); - } - } - - string categoryStr = element.GetAttributeString("category", "Misc"); - if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) - { - category = MapEntityCategory.Misc; - } - Category = category; - - var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty; - - //nameidentifier can be used to make multiple items use the same names and descriptions - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); - - //only used if the item doesn't have a name/description defined in the currently selected language - string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); - - //works the same as nameIdentifier, but just replaces the description - string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); - - if (string.IsNullOrEmpty(originalName)) - { - if (string.IsNullOrEmpty(nameIdentifier)) - { - name = TextManager.Get("EntityName." + identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; - } - else - { - name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; - } - } - else if (Category.HasFlag(MapEntityCategory.Legacy)) - { - // Legacy items use names as identifiers, so we have to define them in the xml. But we also want to support the translations. Therefore - if (string.IsNullOrEmpty(nameIdentifier)) - { - name = TextManager.Get("EntityName." + identifier, true) ?? originalName; - } - else - { - name = TextManager.Get("EntityName." + nameIdentifier, true) ?? originalName; - } - - if (string.IsNullOrWhiteSpace(identifier)) + string categoryStr = element.GetAttributeString("category", "Misc"); + if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy)) { identifier = GenerateLegacyIdentifier(originalName); } } + return identifier; + } - if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) + public static Identifier GenerateLegacyIdentifier(string name) + { + return ($"legacyitem_{name.Replace(" ", "")}").ToIdentifier(); + } + + public ItemPrefab(ContentXElement element, ItemFile file) : base(element, file) + { + originalElement = element; + ConfigElement = element; + + OriginalName = element.GetAttributeString("name", ""); + name = OriginalName; + + VariantOf = element.VariantOf(); + + if (!VariantOf.IsEmpty) { return; } //don't even attempt to read the XML until the PrefabCollection readies up the parent to inherit from + + ParseConfigElement(variantOf: null); + } + + private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf) + => subElement.DoesAttributeReferenceFileNameAlone("texture") + ? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path) + : ""; + + private void ParseConfigElement(ItemPrefab variantOf) + { + string categoryStr = ConfigElement.GetAttributeString("category", "Misc"); + this.category = Enum.TryParse(categoryStr, true, out MapEntityCategory category) + ? category + : MapEntityCategory.Misc; + + var parentType = ConfigElement.Parent?.GetAttributeIdentifier("itemtype", ""); + + //nameidentifier can be used to make multiple items use the same names and descriptions + Identifier nameIdentifier = ConfigElement.GetAttributeIdentifier("nameidentifier", ""); + + //only used if the item doesn't have a name/description defined in the currently selected language + string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", ""); + + //works the same as nameIdentifier, but just replaces the description + Identifier descriptionIdentifier = ConfigElement.GetAttributeIdentifier("descriptionidentifier", ""); + + if (string.IsNullOrEmpty(OriginalName)) { - if (!string.IsNullOrEmpty(name)) - { - name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); - } + name = TextManager.Get(nameIdentifier.IsEmpty + ? $"EntityName.{Identifier}" + : $"EntityName.{nameIdentifier}", + $"EntityName.{fallbackNameIdentifier}"); + } + else if (Category.HasFlag(MapEntityCategory.Legacy)) + { + // Legacy items use names as identifiers, so we have to define them in the xml. But we also want to support the translations. Therefore + name = TextManager.Get(nameIdentifier.IsEmpty + ? $"EntityName.{Identifier}" + : $"EntityName.{nameIdentifier}"); + name = name.Fallback(OriginalName); } - name = GeneticMaterial.TryCreateName(this, element); - - if (string.IsNullOrEmpty(name)) + if (parentType == "wrecked") { - DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); + name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); } - DebugConsole.Log(" " + name); + name = GeneticMaterial.TryCreateName(this, ConfigElement); - Aliases = new HashSet - (element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? - element.GetAttributeStringArray("Aliases", new string[0], convertToLowerInvariant: true)); - Aliases.Add(originalName.ToLowerInvariant()); + this.aliases = + (ConfigElement.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? + ConfigElement.GetAttributeStringArray("Aliases", Array.Empty(), convertToLowerInvariant: true)) + .ToImmutableHashSet() + .Add(OriginalName.ToLowerInvariant()); - Triggers = new List(); - DeconstructItems = new List(); - FabricationRecipes = new List(); + var triggers = new List(); + var deconstructItems = new List(); + var fabricationRecipes = new Dictionary(); + var treatmentSuitability = new Dictionary(); + var locationPrices = new Dictionary(); + var preferredContainers = new List(); DeconstructTime = 1.0f; - if (element.Attribute("allowasextracargo") != null) + if (ConfigElement.Attribute("allowasextracargo") != null) { - AllowAsExtraCargo = element.GetAttributeBool("allowasextracargo", false); + AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false); } - Tags = new HashSet(element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true)); + this.tags = ConfigElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet(); if (!Tags.Any()) { - Tags = new HashSet(element.GetAttributeStringArray("Tags", new string[0], convertToLowerInvariant: true)); + this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty()).ToImmutableHashSet(); } - if (element.Attribute("cargocontainername") != null) + if (ConfigElement.Attribute("cargocontainername") != null) { - DebugConsole.ThrowError("Error in item prefab \"" + name + "\" - cargo container should be configured using the item's identifier, not the name."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); } - SerializableProperty.DeserializeProperties(this, element); + SerializableProperty.DeserializeProperties(this, ConfigElement); - if (string.IsNullOrEmpty(Description)) + if (Description.IsNullOrEmpty()) { - if (!string.IsNullOrEmpty(descriptionIdentifier)) + if (descriptionIdentifier != Identifier.Empty) { - Description = TextManager.Get("EntityDescription." + descriptionIdentifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); } - else if (string.IsNullOrEmpty(nameIdentifier)) + else if (nameIdentifier == Identifier.Empty) { - Description = TextManager.Get("EntityDescription." + identifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{Identifier}"); } else { - Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); } } - var allowDroppingOnSwapWith = element.GetAttributeStringArray("allowdroppingonswapwith", new string[0]); - if (allowDroppingOnSwapWith.Any()) - { - AllowDroppingOnSwap = true; - foreach (string tag in allowDroppingOnSwapWith) - { - this.allowDroppingOnSwapWith.Add(tag); - } - } + var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty()); + AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet(); + AllowDroppingOnSwap = allowDroppingOnSwapWith.Any(); - foreach (XElement subElement in element.Elements()) + var levelCommonness = new Dictionary(); + var levelQuantity = new Dictionary(); + + foreach (ContentXElement subElement in ConfigElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - string spriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - spriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } + string spriteFolder = GetTexturePath(subElement, variantOf); CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); @@ -940,21 +738,20 @@ namespace Barotrauma if (subElement.Attribute("sourcerect") == null && subElement.Attribute("sheetindex") == null) { - DebugConsole.ThrowError("Warning - sprite sourcerect not configured for item \"" + Name + "\"!"); + DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); } - size = sprite.size; + Size = Sprite.size; - if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(Name)) + if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) { - sprite.Name = Name; + Sprite.Name = Name.Value; } - sprite.EntityID = identifier; + Sprite.EntityIdentifier = Identifier; break; case "price": - if (locationPrices == null) { locationPrices = new Dictionary(); } if (subElement.Attribute("baseprice") != null) { - foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out DefaultPrice)) + foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out this.defaultPrice)) { if (priceInfo == null) { continue; } locationPrices.Add(priceInfo.Item1, priceInfo.Item2); @@ -962,138 +759,18 @@ namespace Barotrauma } else if (subElement.Attribute("buyprice") != null) { - string locationType = subElement.GetAttributeString("locationtype", "").ToLowerInvariant(); - locationPrices.Add(locationType, new PriceInfo(subElement)); - } - break; - case "upgradeoverride": - { -#if CLIENT - var sprites = new List(); - foreach (XElement decorSprite in subElement.Elements()) - { - if (decorSprite.Name.ToString().Equals("decorativesprite", StringComparison.OrdinalIgnoreCase)) + Identifier locationType = subElement.GetAttributeIdentifier("locationtype", ""); + if (locationPrices.ContainsKey(locationType)) { - sprites.Add(new DecorativeSprite(decorSprite)); + DebugConsole.AddWarning($"Error in item prefab \"{ToString()}\": price for the location type \"{locationType}\" defined more than once."); + locationPrices[locationType] = new PriceInfo(subElement); + } + else + { + locationPrices.Add(locationType, new PriceInfo(subElement)); } } - UpgradeOverrideSprites.Add(subElement.GetAttributeString("identifier", ""), sprites); -#endif break; - } -#if CLIENT - case "upgradepreviewsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f); - } - break; - case "inventoryicon": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - InventoryIcon = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "minimapicon": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "infectedsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "damagedinfectedsprite": - { - string iconFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - iconFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); - } - break; - case "brokensprite": - string brokenSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - brokenSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - var brokenSprite = new BrokenItemSprite( - new Sprite(subElement, brokenSpriteFolder, lazyLoad: true), - subElement.GetAttributeFloat("maxcondition", 0.0f), - subElement.GetAttributeBool("fadein", false), - subElement.GetAttributePoint("offset", Point.Zero)); - - int spriteIndex = 0; - for (int i = 0; i < BrokenSprites.Count && BrokenSprites[i].MaxConditionPercentage < brokenSprite.MaxConditionPercentage; i++) - { - spriteIndex = i; - } - BrokenSprites.Insert(spriteIndex, brokenSprite); - break; - case "decorativesprite": - string decorativeSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - decorativeSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - - int groupID = 0; - DecorativeSprite decorativeSprite = null; - if (subElement.Attribute("texture") == null) - { - groupID = subElement.GetAttributeInt("randomgroupid", 0); - } - else - { - decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); - DecorativeSprites.Add(decorativeSprite); - groupID = decorativeSprite.RandomGroupID; - } - if (!DecorativeSpriteGroups.ContainsKey(groupID)) - { - DecorativeSpriteGroups.Add(groupID, new List()); - } - DecorativeSpriteGroups[groupID].Add(decorativeSprite); - - break; - case "containedsprite": - string containedSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) - { - containedSpriteFolder = Path.GetDirectoryName(VariantOf?.FilePath ?? filePath); - } - var containedSprite = new ContainedItemSprite(subElement, containedSpriteFolder, lazyLoad: true); - if (containedSprite.Sprite != null) - { - ContainedSprites.Add(containedSprite); - } - break; -#endif case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); AllowDeconstruct = true; @@ -1103,27 +780,39 @@ namespace Barotrauma { if (deconstructItem.Attribute("name") != null) { - DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items."); + DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - DeconstructItems.Add(new DeconstructItem(deconstructItem, identifier)); + deconstructItems.Add(new DeconstructItem(deconstructItem, Identifier)); } - RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count); + RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count); break; case "fabricate": case "fabricable": case "fabricableitem": - fabricationRecipeElements.Add(subElement); + var newRecipe = new FabricationRecipe(subElement, Identifier); + if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe)) + { + DebugConsole.ThrowError( + $"Error in item prefab \"{ToString()}\": " + + $"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " + + $"This will cause issues with fabrication." + ); + } + else + { + fabricationRecipes.Add(newRecipe.RecipeHash, newRecipe); + } break; case "preferredcontainer": var preferredContainer = new PreferredContainer(subElement); if (preferredContainer.Primary.Count == 0 && preferredContainer.Secondary.Count == 0) { - DebugConsole.ThrowError($"Error in item prefab {Name}: preferred container has no preferences defined ({subElement})."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\": preferred container has no preferences defined ({subElement})."); } else { - PreferredContainers.Add(preferredContainer); + preferredContainers.Add(preferredContainer); } break; case "swappableitem": @@ -1138,25 +827,25 @@ namespace Barotrauma Height = subElement.GetAttributeInt("height", 0) }; - Triggers.Add(trigger); + triggers.Add(trigger); break; case "levelresource": foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness")) { - string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelName = levelCommonnessElement.GetAttributeIdentifier("leveltype", ""); if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false)) { - if (!LevelCommonness.ContainsKey(levelName)) + if (!levelCommonness.ContainsKey(levelName)) { - LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); + levelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); } } else { - if (!LevelQuantity.ContainsKey(levelName)) + if (!levelQuantity.ContainsKey(levelName)) { - LevelQuantity.Add(levelName, new FixedQuantityResourceInfo( + levelQuantity.Add(levelName, new FixedQuantityResourceInfo( levelCommonnessElement.GetAttributeInt("clusterquantity", 0), levelCommonnessElement.GetAttributeInt("clustersize", 0), levelCommonnessElement.GetAttributeBool("isislandspecific", false), @@ -1168,70 +857,79 @@ namespace Barotrauma case "suitabletreatment": if (subElement.Attribute("name") != null) { - DebugConsole.ThrowError("Error in item prefab \"" + Name + "\" - suitable treatments should be defined using item identifiers, not item names."); + DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); } - string treatmentIdentifier = (subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", string.Empty)).ToLowerInvariant(); + Identifier treatmentIdentifier = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); float suitability = subElement.GetAttributeFloat("suitability", 0.0f); treatmentSuitability.Add(treatmentIdentifier, suitability); break; } } - // Set the default price in case the prices are defined in the old way - // with separate Price elements and there is no default price explicitly set +#if CLIENT + ParseSubElementsClient(ConfigElement, variantOf); +#endif + + this.Triggers = triggers.ToImmutableArray(); + this.DeconstructItems = deconstructItems.ToImmutableArray(); + this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary(); + this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary(); + this.locationPrices = locationPrices.ToImmutableDictionary(); + this.PreferredContainers = preferredContainers.ToImmutableArray(); + this.LevelCommonness = levelCommonness.ToImmutableDictionary(); + this.LevelQuantity = levelQuantity.ToImmutableDictionary(); + + // Backwards compatibility if (locationPrices != null && locationPrices.Any()) { - DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + this.defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } - HideConditionInTooltip = element.GetAttributeBool("hideconditionintooltip", HideConditionBar); + HideConditionInTooltip = ConfigElement.GetAttributeBool("hideconditionintooltip", HideConditionBar); //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { - Category = MapEntityCategory.Wrecked; + this.category = MapEntityCategory.Wrecked; Subcategory = "Thalamus"; } - if (sprite == null) + if (Sprite == null) { - DebugConsole.ThrowError("Item \"" + Name + "\" has no sprite!"); + DebugConsole.ThrowError($"Item \"{ToString()}\" has no sprite!"); #if SERVER - sprite = new Sprite("", Vector2.Zero); - sprite.SourceRect = new Rectangle(0, 0, 32, 32); + this.sprite = new Sprite("", Vector2.Zero); + this.sprite.SourceRect = new Rectangle(0, 0, 32, 32); #else - sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null) + this.sprite = new Sprite(TextureLoader.PlaceHolderTexture, null, null) { Origin = TextureLoader.PlaceHolderTexture.Bounds.Size.ToVector2() / 2 }; #endif - size = sprite.size; - sprite.EntityID = identifier; + Size = Sprite.size; + Sprite.EntityIdentifier = Identifier; } - - if (string.IsNullOrEmpty(identifier)) + + if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - "Item prefab \"" + name + "\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + $"Item prefab \"{ToString()}\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } #if DEBUG if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) { - if (!string.IsNullOrEmpty(originalName)) + if (!string.IsNullOrEmpty(OriginalName)) { - DebugConsole.AddWarning($"Item \"{(string.IsNullOrEmpty(identifier) ? name : identifier)}\" has a hard-coded name, and won't be localized to other languages."); + DebugConsole.AddWarning($"Item \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); } } #endif - AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); - - Prefabs.Add(this, allowOverriding); - this.CalculatePrefabUIntIdentifier(Prefabs); + this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty()).ToImmutableHashSet(); } - public float GetTreatmentSuitability(string treatmentIdentifier) + public float GetTreatmentSuitability(Identifier treatmentIdentifier) { return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; } @@ -1239,7 +937,7 @@ namespace Barotrauma public PriceInfo GetPriceInfo(Location location) { if (location?.Type == null) { return null; } - var locationTypeId = location.Type.Identifier?.ToLowerInvariant(); + var locationTypeId = location.Type.Identifier; if (locationPrices != null && locationPrices.ContainsKey(locationTypeId)) { return locationPrices[locationTypeId]; @@ -1261,15 +959,15 @@ namespace Barotrauma (location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } - public static ItemPrefab Find(string name, string identifier) + public static ItemPrefab Find(string name, Identifier identifier) { - if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(identifier)) + if (string.IsNullOrEmpty(name) && identifier.IsEmpty) { throw new ArgumentException("Both name and identifier cannot be null."); } ItemPrefab prefab; - if (string.IsNullOrEmpty(identifier)) + if (identifier.IsEmpty) { //legacy support identifier = GenerateLegacyIdentifier(name); @@ -1284,12 +982,12 @@ namespace Barotrauma } if (prefab == null) { - prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(identifier)); + prefab = Prefabs.Find(me => me.Aliases != null && me.Aliases.Contains(identifier.Value)); } if (prefab == null) { - DebugConsole.ThrowError("Error loading item - item prefab \"" + name + "\" (identifier \"" + identifier + "\") not found."); + DebugConsole.ThrowError($"Error loading item - item prefab \"{name}\" (identifier \"{identifier}\") not found."); } return prefab; } @@ -1314,12 +1012,12 @@ namespace Barotrauma } } - public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) + public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) { - Dictionary priceLocations = new Dictionary(); + Dictionary priceLocations = new Dictionary(); if (locationPrices != null) { - foreach (KeyValuePair locationPrice in locationPrices) + foreach (KeyValuePair locationPrice in locationPrices) { PriceInfo priceInfo = locationPrice.Value; @@ -1340,16 +1038,16 @@ namespace Barotrauma return priceLocations.ToImmutableDictionary(); } - public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) + public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) { - Dictionary priceLocations = new Dictionary(); + Dictionary priceLocations = new Dictionary(); if (!CanBeSold && sellingImportant) { return priceLocations.ToImmutableDictionary(); } - foreach (KeyValuePair locationPrice in locationPrices) + foreach (KeyValuePair locationPrice in locationPrices) { PriceInfo priceInfo = locationPrice.Value; @@ -1379,7 +1077,7 @@ namespace Barotrauma static bool HasConditionRequirement(PreferredContainer pc) => pc.MinCondition > 0 || pc.MaxCondition < 100; } - public bool IsContainerPreferred(Item item, string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) + public bool IsContainerPreferred(Item item, Identifier[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; @@ -1394,101 +1092,29 @@ namespace Barotrauma private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; - 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)); + 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)); - private XElement CreateVariantXML(XElement variantElement, ItemPrefab basePrefab) + protected override void CreateInstance(Rectangle rect) { - XElement newElement = new XElement(variantElement.Name); - newElement.Add(basePrefab.ConfigElement.Attributes()); - newElement.Add(basePrefab.ConfigElement.Elements()); + throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance"); + } - ReplaceElement(newElement, variantElement); + private bool disposed = false; + public override void Dispose() + { + if (disposed) { return; } + disposed = true; + Prefabs.Remove(this); + Item.RemoveByPrefab(this); + } - void ReplaceElement(XElement element, XElement replacement) - { - List elementsToRemove = new List(); - foreach (XAttribute attribute in replacement.Attributes()) - { - ReplaceAttribute(element, attribute); - } - foreach (XElement replacementSubElement in replacement.Elements()) - { - int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); - System.Diagnostics.Debug.Assert(index > -1); - - int i = 0; - bool matchingElementFound = false; - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } - if (i == index) - { - if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) - { - //if the replacement is empty (no attributes or child elements) - //remove the element from the variant - elementsToRemove.Add(subElement); - } - else - { - ReplaceElement(subElement, replacementSubElement); - } - matchingElementFound = true; - break; - } - i++; - } - if (!matchingElementFound) - { - element.Add(replacementSubElement); - } - } - elementsToRemove.ForEach(e => e.Remove()); - } - - void ReplaceAttribute(XElement element, XAttribute newAttribute) - { - XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); - if (existingAttribute == null) - { - element.Add(newAttribute); - return; - } - float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); - if (newAttribute.Value.StartsWith('*')) - { - string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); - float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); - if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) - { - existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); - } - else - { - existingAttribute.Value = ((int)(value * multiplier)).ToString(); - } - } - else if (newAttribute.Value.StartsWith('+')) - { - string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); - float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); - if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) - { - existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); - } - else - { - existingAttribute.Value = ((int)(value + addition)).ToString(); - } - } - else - { - existingAttribute.Value = newAttribute.Value; - } - } - - return newElement; + public Identifier VariantOf { get; } + + public void InheritFrom(ItemPrefab parent) + { + ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement).FromPackage(ConfigElement.ContentPackage); + ParseConfigElement(parent); } public override string ToString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index a5c4b8974..136178582 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -22,14 +22,14 @@ namespace Barotrauma public bool IgnoreInEditor { get; set; } - private string[] excludedIdentifiers; + private Identifier[] excludedIdentifiers; private RelationType type; public List statusEffects; - public string Msg; - public string MsgTag; + public LocalizedString Msg; + public Identifier MsgTag; /// /// Should broken (0 condition) items be excluded @@ -55,15 +55,11 @@ namespace Barotrauma { if (value == null) return; - Identifiers = value.Split(','); - for (int i = 0; i < Identifiers.Length; i++) - { - Identifiers[i] = Identifiers[i].Trim().ToLowerInvariant(); - } + Identifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); } } - public string[] Identifiers { get; private set; } + public Identifier[] Identifiers { get; private set; } public string JoinedExcludedIdentifiers { @@ -72,11 +68,7 @@ namespace Barotrauma { if (value == null) return; - excludedIdentifiers = value.Split(','); - for (int i = 0; i < excludedIdentifiers.Length; i++) - { - excludedIdentifiers[i] = excludedIdentifiers[i].Trim().ToLowerInvariant(); - } + excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray(); } } @@ -84,28 +76,19 @@ namespace Barotrauma { if (item == null) { return false; } if (excludedIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) { return false; } - return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id) || (AllowVariants && item.Prefab.VariantOf?.Identifier == id)); + return Identifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && item.Prefab.VariantOf == id)); } public bool MatchesItem(ItemPrefab itemPrefab) { if (itemPrefab == null) { return false; } if (excludedIdentifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id))) { return false; } - return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id) || (AllowVariants && itemPrefab.VariantOf?.Identifier == id)); + return Identifiers.Any(id => itemPrefab.Identifier == id || itemPrefab.Tags.Contains(id) || (AllowVariants && !itemPrefab.VariantOf.IsEmpty && itemPrefab.VariantOf == id)); } - public RelatedItem(string[] identifiers, string[] excludedIdentifiers) + public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers) { - for (int i = 0; i < identifiers.Length; i++) - { - identifiers[i] = identifiers[i].Trim().ToLowerInvariant(); - } - this.Identifiers = identifiers; - - for (int i = 0; i < excludedIdentifiers.Length; i++) - { - excludedIdentifiers[i] = excludedIdentifiers[i].Trim().ToLowerInvariant(); - } - this.excludedIdentifiers = excludedIdentifiers; + this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToArray(); + this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToArray(); statusEffects = new List(); } @@ -178,22 +161,22 @@ namespace Barotrauma element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers)); } - if (!string.IsNullOrWhiteSpace(Msg)) { element.Add(new XAttribute("msg", string.IsNullOrEmpty(MsgTag) ? Msg : MsgTag)); } + if (!Msg.IsNullOrWhiteSpace()) { element.Add(new XAttribute("msg", MsgTag.IsEmpty ? Msg : MsgTag.Value)); } } - public static RelatedItem Load(XElement element, bool returnEmpty, string parentDebugName) + public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) { - string[] identifiers; + Identifier[] identifiers; if (element.Attribute("name") != null) { //backwards compatibility + a console warning DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); - string[] itemNames = element.GetAttributeStringArray("name", new string[0]); + Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty()); //attempt to convert to identifiers and tags - List convertedIdentifiers = new List(); - foreach (string itemName in itemNames) + List convertedIdentifiers = new List(); + foreach (Identifier itemName in itemNames) { - var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName); + var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value); if (matchingItem != null) { convertedIdentifiers.Add(matchingItem.Identifier); @@ -208,24 +191,24 @@ namespace Barotrauma } else { - identifiers = element.GetAttributeStringArray("items", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("item", null, convertToLowerInvariant: true); + identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null); if (identifiers == null) { - identifiers = element.GetAttributeStringArray("identifiers", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("tags", null, convertToLowerInvariant: true); + identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null); if (identifiers == null) { - identifiers = element.GetAttributeStringArray("identifier", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("tag", new string[0], convertToLowerInvariant: true); + identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty()); } } } - string[] excludedIdentifiers = element.GetAttributeStringArray("excludeditems", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludeditem", null, convertToLowerInvariant: true); + Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null); if (excludedIdentifiers == null) { - excludedIdentifiers = element.GetAttributeStringArray("excludedidentifiers", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludedtags", null, convertToLowerInvariant: true); + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null); if (excludedIdentifiers == null) { - excludedIdentifiers = element.GetAttributeStringArray("excludedidentifier", null, convertToLowerInvariant: true) ?? element.GetAttributeStringArray("excludedtag", new string[0], convertToLowerInvariant: true); + excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty()); } } @@ -257,24 +240,24 @@ namespace Barotrauma return null; } - ri.MsgTag = element.GetAttributeString("msg", ""); - string msg = TextManager.Get(ri.MsgTag, true); - if (msg == null) + ri.MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty); + LocalizedString msg = TextManager.Get(ri.MsgTag); + if (!msg.Loaded) { - ri.Msg = ri.MsgTag; + ri.Msg = ri.MsgTag.Value; } else { #if CLIENT foreach (InputType inputType in Enum.GetValues(typeof(InputType))) { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBindText(inputType)); + msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType)); } ri.Msg = msg; #endif } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs index da8c59d63..b9fc28d5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CoreEntityPrefab.cs @@ -1,5 +1,10 @@ -using System; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; using System.Text; namespace Barotrauma @@ -8,7 +13,80 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); + private readonly ConstructorInfo constructor; + + private CoreEntityPrefab( + Identifier identifier, + ConstructorInfo constructor, + bool resizeHorizontal = false, + bool resizeVertical = false, + bool linkable = false, + IEnumerable allowedLinks = null, + IEnumerable aliases = null) + : base(identifier) + { + this.constructor = constructor; + this.Name = TextManager.Get($"EntityName.{identifier}"); + this.Description = TextManager.Get($"EntityDescription.{identifier}"); + this.ResizeHorizontal = resizeHorizontal; + this.ResizeVertical = resizeVertical; + this.Linkable = linkable; + this.AllowedLinks = (allowedLinks ?? Enumerable.Empty()).ToImmutableHashSet(); + this.Aliases = (aliases ?? Enumerable.Empty()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet(); + } + + public static void InitCorePrefabs() + { + CoreEntityPrefab ep = new CoreEntityPrefab( + "hull".ToIdentifier(), + typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + resizeHorizontal: true, + resizeVertical: true, + linkable: true, + allowedLinks: new Identifier[] { "hull".ToIdentifier() }); + Prefabs.Add(ep, false); + + ep = new CoreEntityPrefab( + "gap".ToIdentifier(), + typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), + resizeHorizontal: true, + resizeVertical: true); + Prefabs.Add(ep, false); + + ep = new CoreEntityPrefab( + "waypoint".ToIdentifier(), + typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); + Prefabs.Add(ep, false); + + ep = new CoreEntityPrefab( + "spawnpoint".ToIdentifier(), + typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) })); + Prefabs.Add(ep, false); + } + + protected override void CreateInstance(Rectangle rect) + { + if (constructor == null) return; + object[] lobject = new object[] { this, rect }; + constructor.Invoke(lobject); + } + private bool disposed = false; + + public override Sprite Sprite => null; + + public override string OriginalName => Name.Value; + + public override LocalizedString Name { get; } + + public override ImmutableHashSet Tags { get; } = Enumerable.Empty().ToImmutableHashSet(); + + public override ImmutableHashSet AllowedLinks { get; } + + public override MapEntityCategory Category => MapEntityCategory.Structure; + + public override ImmutableHashSet Aliases { get; } + public override void Dispose() { if (disposed) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index de0f565e8..9e02177fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -18,45 +18,72 @@ namespace Barotrauma.MapCreatures.Behavior public readonly BallastFloraBehavior? ParentBallastFlora; public int ID = -1; - public Item ClaimedItem; + public Item? ClaimedItem; public int ClaimedItemId = -1; public float MaxHealth = 100f; - public float Health = 100f; + + private float health = 100; + public float Health + { + get { return health; } + set { health = MathHelper.Clamp(value, 0.0f, MaxHealth); } + } + + public float RemoveTimer = 60.0f; public bool SpawningItem; public Item? AttackItem; public bool IsRoot; + /// + /// Decorative branches that grow around the root + /// + public bool IsRootGrowth; public bool Removed; + public bool DisconnectedFromRoot; + public Hull? CurrentHull; public float Pulse = 1.0f; private bool inflate; private float pulseDelay = Rand.Range(0f, 3f); + public readonly BallastFloraBranch? ParentBranch; + /// + /// How far from the root this branch is + /// + public readonly int BranchDepth; + public float AccumulatedDamage; + public float DamageVisualizationTimer; + public Vector2 ShakeAmount; // Adjacent tiles, used to free up sides when this branch gets removed public readonly Dictionary Connections = new Dictionary(); - public BallastFloraBranch(BallastFloraBehavior? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) + public BallastFloraBranch(BallastFloraBehavior? parent, BallastFloraBranch? parentBranch, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) : base(null, position, type, flowerConfig, leafConfig, rect) { + ParentBranch = parentBranch; ParentBallastFlora = parent; + if (parentBranch != null) + { + BranchDepth = parentBranch.BranchDepth + 1; + } } public void UpdateHealth() { if (MaxHealth <= Health) { return; } Color healthColor = Color.White * (1.0f - Health / MaxHealth); - HealthColor = healthColor; + HealthColor = Color.Lerp(HealthColor, healthColor, 0.05f); } public void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay) { - if (ParentBallastFlora == null) { return; } + if (ParentBallastFlora == null || DisconnectedFromRoot) { return; } if (pulseDelay > 0) { @@ -91,7 +118,7 @@ namespace Barotrauma.MapCreatures.Behavior public List> debugSearchLines = new List>(); #endif - private static List _entityList = new List(); + private readonly static List _entityList = new List(); public static IEnumerable EntityList => _entityList; public enum NetworkHeader @@ -101,119 +128,126 @@ namespace Barotrauma.MapCreatures.Behavior BranchCreate, BranchRemove, BranchDamage, - Infect + Infect, + Remove } public enum AttackType { Fire, Explosives, - Other + Other, + CutFromRoot } public struct AITarget { - public string[] Tags; + public Identifier[] Tags; public int Priority; - public AITarget(XElement element) + public AITarget(ContentXElement element) { - Tags = element.GetAttributeStringArray("tags", new string[0]); + Tags = element.GetAttributeIdentifierArray("tags", Array.Empty())!; Priority = element.GetAttributeInt("priority", 0); } public bool Matches(Item item) - { - foreach (string tag in item.GetTags()) + { + foreach (Identifier targetTag in Tags) { - foreach (string targetTag in Tags) - { - if (tag == targetTag) { return true; } - } + if (item.HasTag(targetTag)) { return true; } } - return false; } } - [Serialize(0.25f, true, "Scale of the branches")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the branches.")] public float BaseBranchScale { get; set; } - [Serialize(0.25f, true, "Scale of the flowers")] + [Serialize(0.25f, IsPropertySaveable.Yes, "Scale of the flowers.")] public float BaseFlowerScale { get; set; } - [Serialize(0.5f, true, "Scale of the leaves")] + [Serialize(0.5f, IsPropertySaveable.Yes, "Scale of the leaves.")] public float BaseLeafScale { get; set; } - [Serialize(0.33f, true, "Chance for a flower to appear on the branch")] + [Serialize(0.33f, IsPropertySaveable.Yes, "Chance for a flower to appear on a branch.")] public float FlowerProbability { get; set; } - [Serialize(0.7f, true, "Change for leaves to appear for the branch")] + [Serialize(0.7f, IsPropertySaveable.Yes, "Chance for leaves to appear on a branch.")] public float LeafProbability { get; set; } - [Serialize(3f, true, "Delay between pulses")] + [Serialize(3f, IsPropertySaveable.Yes, "Delay between pulses.")] public float PulseDelay { get; set; } - [Serialize(3f, true, "How fast the flower inflates during a pulse")] + [Serialize(3f, IsPropertySaveable.Yes, "How fast the flower inflates during a pulse.")] public float PulseInflateSpeed { get; set; } - [Serialize(1f, true, "How fast the flower deflates")] + [Serialize(1f, IsPropertySaveable.Yes, "How fast the flower deflates.")] public float PulseDeflateSpeed { get; set; } - [Serialize(32, true, "How many vines must grow before the plant breaks thru the wall")] + [Serialize(32, IsPropertySaveable.Yes, "How many vines must grow before the plant breaks through the wall.")] public int BreakthroughPoint { get; set; } - [Serialize(false, true, "Has the plant grown large enough to expose itself")] + [Serialize(false, IsPropertySaveable.Yes, "Has the plant grown large enough to expose itself.")] public bool HasBrokenThrough { get; set; } - [Serialize(300, true, "How far the ballast flora can detect items")] + [Serialize(300, IsPropertySaveable.Yes, "How far the ballast flora can detect items from.")] public int Sight { get; set; } - [Serialize(100, true, "How much health the branches have")] + [Serialize(100, IsPropertySaveable.Yes, "How much health the branches have.")] public int BranchHealth { get; set; } - [Serialize(400, true, "How much health the stem has")] - public int StemHealth { get; set; } + [Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")] + public int RootHealth { get; set; } - [Serialize(300f, true, "How much power the ballast flora takes from junction boxes")] + [Serialize(0.0005f, 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.")] + public int MaxBranchHealthRegenDistance { get; set; } + + [Serialize("255,255,255,255", IsPropertySaveable.Yes)] + public Color RootColor { get; set; } + + [Serialize(300f, IsPropertySaveable.Yes, "How much power the ballast flora takes from junction boxes.")] public float PowerConsumptionMin { get; set; } - [Serialize(3000f, true, "How much the power drain spikes")] + [Serialize(3000f, IsPropertySaveable.Yes, "How much the power drain spikes.")] public float PowerConsumptionMax { get; set; } - [Serialize(10f, true, "How long it takes for power drain to wind down")] + [Serialize(10f, IsPropertySaveable.Yes, "How long it takes for power drain to wind down.")] public float PowerConsumptionDuration { get; set; } - [Serialize(250f, true, "How much power does it take to accelerate growth")] + [Serialize(250f, IsPropertySaveable.Yes, "How much power does it take to accelerate growth.")] public float PowerRequirement { get; set; } - [Serialize(5f, true, "Maximum anger, anger increases when the plant gets damaged and increases growth speed")] + [Serialize(5f, IsPropertySaveable.Yes, "Maximum anger, anger increases when the plant gets damaged and increases growth speed.")] public float MaxAnger { get; set; } - [Serialize(10000f, true, "Maximum power buffer")] + [Serialize(10000f, IsPropertySaveable.Yes, "Maximum power buffer.")] public float MaxPowerCapacity { get; set; } - [Serialize("", true, "Item prefab that is spawned when threatened")] - public string AttackItemPrefab { get; set; } = ""; + [Serialize("", IsPropertySaveable.Yes, "Item prefab that is spawned when threatened.")] + public Identifier AttackItemPrefab { get; set; } = Identifier.Empty; - [Serialize(0.8f, true, "How resistant the ballast flora is to exlposives before it blooms")] + [Serialize(0.8f, IsPropertySaveable.Yes, "How resistant the ballast flora is to explosives before it blooms.")] public float ExplosionResistance { get; set; } - [Serialize(5f, true, "How much damage is taken from open fires")] + [Serialize(5f, IsPropertySaveable.Yes, "How much damage is taken from open fires.")] public float FireVulnerability { get; set; } - [Serialize(0.5f, true, "How much resistance against fire is gained while submerged.")] + [Serialize(0.5f, IsPropertySaveable.Yes, "How much resistance against fire is gained while submerged.")] public float SubmergedWaterResistance { get; set; } - [Serialize(0.8f, true, "What depth the branches will be drawn on")] + [Serialize(0.8f, IsPropertySaveable.Yes, "What depth the branches will be drawn on.")] public float BranchDepth { get; set; } - [Serialize("", true, "What sound to play when the ballast flora bursts thru walls")] + [Serialize("", IsPropertySaveable.Yes, "What sound to play when the ballast flora bursts through walls.")] public string BurstSound { get; set; } = ""; private float availablePower; - [Serialize(0f, true, "How much power the ballast flora has stored.")] + [Serialize(0f, IsPropertySaveable.Yes, "How much power the ballast flora has stored.")] public float AvailablePower { get => availablePower; @@ -222,7 +256,7 @@ namespace Barotrauma.MapCreatures.Behavior private float anger; - [Serialize(1f, true, "How enraged the flora is, affects how fast it grows.")] + [Serialize(1f, IsPropertySaveable.Yes, "How enraged the flora is, affects how fast it grows.")] public float Anger { get => anger; @@ -235,13 +269,13 @@ namespace Barotrauma.MapCreatures.Behavior public BallastFloraPrefab Prefab { get; private set; } - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } public Vector2 Offset; - public readonly List ClaimedTargets = new List(); - public readonly List ClaimedJunctionBoxes = new List(); - public readonly List ClaimedBatteries = new List(); + public readonly HashSet ClaimedTargets = new HashSet(); + public readonly HashSet ClaimedJunctionBoxes = new HashSet(); + public readonly HashSet ClaimedBatteries = new HashSet(); public readonly Dictionary IgnoredTargets = new Dictionary(); private readonly List> tempClaimedTargets = new List>(); @@ -252,11 +286,12 @@ namespace Barotrauma.MapCreatures.Behavior public float PowerConsumptionTimer; private float defenseCooldown, toxinsCooldown, fireCheckCooldown; - private float damageIndicatorTimer, selfDamageTimer, toxinsTimer; + private float selfDamageTimer, toxinsTimer; private readonly List branchesVulnerableToFire = new List(); public readonly List Branches = new List(); + private BallastFloraBranch? root; private readonly List bodies = new List(); public readonly BallastFloraStateMachine StateMachine; @@ -319,15 +354,15 @@ namespace Barotrauma.MapCreatures.Behavior SerializableProperties = SerializableProperty.DeserializeProperties(this, prefab.Element); LoadPrefab(prefab.Element); StateMachine = new BallastFloraStateMachine(this); - if (firstGrowth) { GenerateStem(); } + if (firstGrowth) { GenerateRoot(); } _entityList.Add(this); } - partial void LoadPrefab(XElement element); + partial void LoadPrefab(ContentXElement element); - public void LoadTargets(XElement element) + public void LoadTargets(ContentXElement element) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { Targets.Add(new AITarget(subElement)); } @@ -358,6 +393,10 @@ namespace Barotrauma.MapCreatures.Behavior { be.Add(new XAttribute("claimed", (int)(branch.ClaimedItem?.ID ?? -1))); } + if (branch.ParentBranch != null) + { + be.Add(new XAttribute("parentbranch", (int)(branch.ParentBranch?.ID ?? -1))); + } saveElement.Add(be); } @@ -382,7 +421,7 @@ namespace Barotrauma.MapCreatures.Behavior { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); Offset = element.GetAttributeVector2("offset", Vector2.Zero); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -412,8 +451,15 @@ namespace Barotrauma.MapCreatures.Behavior int sides = getInt("sides"); int blockedSides = getInt("blockedsides"); int claimedId = branchElement.GetAttributeInt("claimed", -1); + int parentBranchId = branchElement.GetAttributeInt("parentbranch", -1); - BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig)) + BallastFloraBranch? parentBranch = null; + if (parentBranchId > -1) + { + parentBranch = Branches[parentBranchId]; + } + + BallastFloraBranch newBranch = new BallastFloraBranch(this, parentBranch, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig)) { ID = id, Health = health, @@ -422,6 +468,7 @@ namespace Barotrauma.MapCreatures.Behavior BlockedSides = (TileSide) blockedSides, IsRoot = isRoot }; + if (newBranch.IsRoot) { root = newBranch; } if (claimedId > -1) { @@ -437,6 +484,15 @@ namespace Barotrauma.MapCreatures.Behavior public void Update(float deltaTime) { + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + { + if (Branches.Count == 0) + { + Remove(); + return; + } + } + foreach (BallastFloraBranch branch in Branches) { branch.UpdateScale(deltaTime); @@ -446,38 +502,25 @@ namespace Barotrauma.MapCreatures.Behavior #endif } - if (damageIndicatorTimer <= 0) - { - foreach (BallastFloraBranch branch in Branches) - { - if (branch.AccumulatedDamage > 0) - { - -#if CLIENT - CreateDamageParticle(branch, branch.AccumulatedDamage); - - if (GameMain.DebugDraw) - { - var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position; - GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1); - } -#elif SERVER - SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage); -#endif - } - - branch.AccumulatedDamage = 0f; - } - - damageIndicatorTimer = 1f; - } - - damageIndicatorTimer -= deltaTime; + UpdateDamage(deltaTime); UpdatePowerDrain(deltaTime); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (root != null && HealthRegenPerBranch > 0.0f) + { + float healAmount = Branches.Count(b => !b.IsRoot && !b.IsRootGrowth && !b.DisconnectedFromRoot) * HealthRegenPerBranch; + + foreach (BallastFloraBranch branch in Branches) + { + if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; } + float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount; + if (branchHealAmount <= 0.0f) { continue; } + branch.Health += branchHealAmount; + branch.AccumulatedDamage -= branchHealAmount; + } + } StateMachine.Update(deltaTime); if (HasBrokenThrough) @@ -512,12 +555,12 @@ namespace Barotrauma.MapCreatures.Behavior // This entire scope is probably very heavy for GC, need to experiment if (toxinsTimer > 0.1f) { - if (!string.IsNullOrWhiteSpace(AttackItemPrefab)) + if (!AttackItemPrefab.IsEmpty) { Dictionary> branches = new Dictionary>(); foreach (BallastFloraBranch branch in Branches) { - if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0) { continue; } + if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0 || branch.DisconnectedFromRoot) { continue; } if (branches.TryGetValue(branch.CurrentHull, out List? list)) { @@ -534,11 +577,12 @@ namespace Barotrauma.MapCreatures.Behavior List list = branches[hull]; if (!list.Any(HasAcidEmitter)) { - BallastFloraBranch randomBranch = branches[hull].GetRandom(); + BallastFloraBranch randomBranch = branches[hull].GetRandomUnsynced(); randomBranch.SpawningItem = true; ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab); - Entity.Spawner?.AddToSpawnQueue(prefab, Parent.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item => + #warning TODO: Parent needs a nullability sanity check + Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item => { randomBranch.AttackItem = item; randomBranch.SpawningItem = false; @@ -563,38 +607,49 @@ namespace Barotrauma.MapCreatures.Behavior } } + partial void UpdateDamage(float deltaTime); + + private readonly List toBeRemoved = new List(); private void UpdateSelfDamage(float deltaTime) { if (selfDamageTimer <= 0) { - bool hasRoot = false; - foreach (BallastFloraBranch branch in Branches) - { - if (branch.IsRoot) - { - hasRoot = true; - break; - } - } - - if (!hasRoot) - { - Kill(); - return; - } - if (!HasBrokenThrough && !CanGrowMore()) { Branches.ForEachMod(branch => { - float maxHealth = branch.IsRoot ? StemHealth : BranchHealth; + float maxHealth = branch.IsRoot ? RootHealth : BranchHealth; DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other); }); } - selfDamageTimer = 1f; } + toBeRemoved.Clear(); + foreach (BallastFloraBranch branch in Branches) + { + if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) + { + DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot); + } + if (branch.Health <= 0.0f) + { + if (branch.ClaimedItem != null) + { + RemoveClaim(branch.ClaimedItem); + branch.ClaimedItem = null; + } + branch.RemoveTimer -= deltaTime; + if (branch.RemoveTimer <= 0.0f) + { + toBeRemoved.Add(branch); + } + } + } + foreach (BallastFloraBranch branch in toBeRemoved) + { + RemoveBranch(branch); + } selfDamageTimer -= deltaTime; } @@ -675,20 +730,25 @@ namespace Barotrauma.MapCreatures.Behavior branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent, true); } - private void GenerateStem() + private void GenerateRoot() { - BallastFloraBranch stem = new BallastFloraBranch(this, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig) + if (root != null) + { + DebugConsole.ThrowError("Error in ballast flora: tried to grow a root even though root has already been created.\n" + Environment.StackTrace); + } + + root = new BallastFloraBranch(this, null, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right, GrowthStep = 1f, - Health = StemHealth, - MaxHealth = StemHealth, + MaxHealth = RootHealth, + Health = RootHealth, IsRoot = true, CurrentHull = Parent }; - - Branches.Add(stem); - CreateBody(stem); + + Branches.Add(root); + CreateBody(root); } public float GetGrowthSpeed(float deltaTime) @@ -704,19 +764,19 @@ namespace Barotrauma.MapCreatures.Behavior return deltaTime; } - public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List result) + public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List result, bool isRootGrowth = false, Vector2? forcePosition = null) { result = new List(); - if (parent.IsSideBlocked(side)) { return false; } + if (!isRootGrowth && parent.IsSideBlocked(side)) { return false; } - Vector2 pos = parent.AdjacentPositions[side]; + Vector2 pos = forcePosition ?? parent.AdjacentPositions[side]; Rectangle rect = VineTile.CreatePlantRect(pos); - if (CollidesWithWorld(rect)) + if (CollidesWithWorld(rect, checkOtherBranches: !isRootGrowth)) { parent.BlockedSides |= side; parent.FailedGrowthAttempts++; - return false; + return false; } FoliageConfig flowerConfig = FoliageConfig.EmptyConfig; @@ -732,21 +792,22 @@ namespace Barotrauma.MapCreatures.Behavior leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f); } - BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect) + BallastFloraBranch newBranch = new BallastFloraBranch(this, parent, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect) { ID = CreateID(), + MaxHealth = BranchHealth, Health = BranchHealth, - MaxHealth = BranchHealth + IsRootGrowth = isRootGrowth }; SetHull(newBranch); if (newBranch.CurrentHull == null || newBranch.CurrentHull.Submarine != Parent.Submarine) { - parent.BlockedSides |= side; + if (!isRootGrowth) { parent.BlockedSides |= side; } parent.FailedGrowthAttempts++; return false; - } + } UpdateConnections(newBranch, parent); @@ -760,12 +821,28 @@ namespace Barotrauma.MapCreatures.Behavior GrowthWarps--; } + int rootGrowthCount = Branches.Count(b => b.IsRootGrowth); + if (rootGrowthCount < GetDesiredRootGrowthAmount()) + { + if (root != null) + { + Vector2 rootGrowthPos = Rand.Vector(rootGrowthCount * Rand.Range(3.0f, 5.0f)); + TryGrowBranch(root, TileSide.None, out List newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos); + } + } + #if SERVER SendNetworkMessage(this, NetworkHeader.BranchCreate, newBranch, parent.ID); #endif return true; } + private int GetDesiredRootGrowthAmount() + { + if (root == null) { return 0; } + return MathHelper.Clamp(Branches.Count(b => !b.IsRootGrowth && b.Health > 0) / 20, 3, 30); + } + public bool BranchContainsTarget(BallastFloraBranch branch, Item target) { Rectangle worldRect = branch.Rect; @@ -894,6 +971,14 @@ namespace Barotrauma.MapCreatures.Behavior public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) { float damage = amount; + + if (type != AttackType.Other && type != AttackType.CutFromRoot) + { + branch.DamageVisualizationTimer = 1.0f; + } + + if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } + // damage is handled server side currently if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -922,12 +1007,10 @@ namespace Barotrauma.MapCreatures.Behavior } } - branch.AccumulatedDamage += damage; - branch.Health -= damage; - - if (type != AttackType.Other) + if (type != AttackType.Other && type != AttackType.CutFromRoot) { + branch.AccumulatedDamage += damage; Anger += damage * 0.001f; } @@ -935,30 +1018,13 @@ namespace Barotrauma.MapCreatures.Behavior GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); #endif - if (branch.Health < 0) + if (branch.Health <= 0 && type != AttackType.CutFromRoot) { RemoveBranch(branch); + if (branch.IsRoot) { Kill(); } } } - public void Remove() - { - foreach (Body body in bodies) - { - GameMain.World.Remove(body); - } - - Parent.BallastFlora = null; - Branches.Clear(); - - foreach (Item target in ClaimedTargets) - { - target.Infector = null; - } - - _entityList.Remove(this); - } - public void RemoveBranch(BallastFloraBranch branch) { bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; @@ -969,6 +1035,21 @@ 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); + bodies.ForEachMod(body => { if (body.UserData == branch) @@ -995,11 +1076,21 @@ namespace Barotrauma.MapCreatures.Behavior }); #if CLIENT - CreateDeathParticle(branch); + CreateDeathParticle(branch, 1.0f); #endif if (isClient) { return; } + int rootGrowthCount = Branches.Count(b => b.IsRootGrowth); + if (rootGrowthCount > GetDesiredRootGrowthAmount()) + { + var rootGrowth = Branches.LastOrDefault(b => b.IsRootGrowth); + if (rootGrowth != null) + { + RemoveBranch(rootGrowth); + } + } + if (branch.ClaimedItem != null) { RemoveClaim(branch.ClaimedItem); @@ -1050,8 +1141,10 @@ namespace Barotrauma.MapCreatures.Behavior public void Kill() { - Branches.ForEachMod(RemoveBranch); - Parent.BallastFlora = null; + foreach (var branch in Branches) + { + branch.DisconnectedFromRoot = true; + } foreach (Item target in ClaimedTargets) { @@ -1059,6 +1152,19 @@ namespace Barotrauma.MapCreatures.Behavior } StateMachine?.State?.Exit(); +#if SERVER + SendNetworkMessage(this, NetworkHeader.Kill); +#endif + } + + public void Remove() + { + Kill(); + + Branches.ForEachMod(RemoveBranch); + Branches.Clear(); + toBeRemoved.Clear(); + Parent.BallastFlora = null; // clean up leftover (can probably be removed) foreach (Body body in bodies) @@ -1067,8 +1173,9 @@ namespace Barotrauma.MapCreatures.Behavior GameMain.World.Remove(body); } + _entityList.Remove(this); #if SERVER - SendNetworkMessage(this, NetworkHeader.Kill); + SendNetworkMessage(this, NetworkHeader.Remove); #endif } @@ -1093,9 +1200,9 @@ namespace Barotrauma.MapCreatures.Behavior private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore()); - private bool CollidesWithWorld(Rectangle rect) + private bool CollidesWithWorld(Rectangle rect, bool checkOtherBranches = true) { - if (Branches.Any(g => g.Rect.Contains(rect))) { return true; } + if (checkOtherBranches && Branches.Any(g => g.Rect.Contains(rect))) { return true; } Rectangle worldRect = rect; worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs index e053c3104..232a7563b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs @@ -4,125 +4,28 @@ using System.Xml.Linq; namespace Barotrauma { - class BallastFloraPrefab : IPrefab, IDisposable + class BallastFloraPrefab : Prefab { public string OriginalName { get; } - public string Identifier { get; } - public string FilePath { get; } - public XElement Element { get; } - - public ContentPackage ContentPackage { get; private set; } + public LocalizedString DisplayName { get; } + public ContentXElement Element { get; } public bool Disposed; public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private BallastFloraPrefab(XElement element, string filePath, bool isOverride) + public BallastFloraPrefab(ContentXElement element, BallastFloraFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Identifier = element.GetAttributeString("identifier", ""); OriginalName = element.GetAttributeString("name", ""); + DisplayName = TextManager.Get(Identifier).Fallback(OriginalName); Element = element; - FilePath = filePath; - Prefabs.Add(this, isOverride); } - public static BallastFloraPrefab Find(string idenfitier) + public static BallastFloraPrefab Find(Identifier identifier) { - return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null; + return Prefabs.ContainsKey(identifier) ? Prefabs[identifier] : null; } - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading map creature prefabs: "); - - foreach (ContentFile file in files) { LoadFromFile(file); } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - - var rootElement = doc?.Root; - if (rootElement == null) { return; } - - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "ballastflorabehavior": - { - new BallastFloraPrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; - break; - } - case "ballastflorabehaviors": - { - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - XElement upgradeElement = element.GetChildElement("mapcreature"); - if (upgradeElement != null) - { - new BallastFloraPrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage }; - } - else - { - DebugConsole.ThrowError($"Cannot find a map creature element from the children of the override element defined in {file.Path}"); - } - } - else - { - if (element.Name.ToString().Equals("mapcreature", StringComparison.OrdinalIgnoreCase)) - { - new BallastFloraPrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; - } - } - } - - break; - } - case "override": - { - XElement mapCreatures = rootElement.GetChildElement("ballastflorabehaviors"); - if (mapCreatures != null) - { - foreach (XElement element in mapCreatures.Elements()) - { - new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - } - - foreach (XElement element in rootElement.GetChildElements("ballastflorabehavior")) - { - new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - - break; - } - default: - { - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " + - "Valid elements are: \"MapCreature\", \"MapCreatures\" and \"Override\"."); - break; - } - } - } - - private void Dispose(bool disposing) - { - if (!Disposed) - { - if (disposing) - { - Prefabs.Remove(this); - } - } - - Disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public override void Dispose() { } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs index ab939cc7e..f09f2a98b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs @@ -18,7 +18,7 @@ namespace Barotrauma.MapCreatures.Behavior private bool tryDrown; private readonly Character attacker; - public DefendWithPumpState(BallastFloraBranch branch, List items, Character attacker) + public DefendWithPumpState(BallastFloraBranch branch, IEnumerable items, Character attacker) { targetBranch = branch; this.attacker = attacker; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs index ff3ac93bb..9246cb7e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs @@ -59,11 +59,14 @@ namespace Barotrauma.MapCreatures.Behavior protected virtual void Grow() { - List newTiles = GrowRandomly(); + List newBranches = GrowRandomly(); #if DEBUG Behavior.debugSearchLines.Clear(); #endif - if (newTiles.Any(TryScanTargets)) { return; } + foreach (var branch in newBranches) + { + TryScanTargets(branch); + } } public void UpdateIgnoredTargets() @@ -85,23 +88,20 @@ namespace Barotrauma.MapCreatures.Behavior private List GrowRandomly() { - List newBranches = new List(); - List newList = new List(Behavior.Branches); - foreach (BallastFloraBranch branch in newList) - { - if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; } + List availableBranches = Behavior.Branches.Where(b => !b.DisconnectedFromRoot && b.FailedGrowthAttempts <= 8 && b.CanGrowMore()).ToList(); + if (availableBranches.Count == 0) { return availableBranches; } - if (Rand.Range(0, Behavior.Branches.Count(tile => tile.CanGrowMore())) != 0) { continue; } + //prefer growing from the branches furthest from the root (ones with the largest branch depth) + var branch = ToolBox.SelectWeightedRandom(availableBranches, b => (float)b.BranchDepth, Rand.RandSync.Unsynced); - TileSide side = branch.GetRandomFreeSide(); + TileSide side = branch.GetRandomFreeSide(); + if (side == TileSide.None) { return availableBranches; } - if (side == TileSide.None) { continue; } + Behavior.TryGrowBranch(branch, side, out List result); + availableBranches.Clear(); + availableBranches.Add(branch); - Behavior.TryGrowBranch(branch, side, out List result); - newBranches.AddRange(result); - } - - return newBranches; + return availableBranches; } private Item? ScanForTargets(VineTile branch) @@ -117,22 +117,22 @@ namespace Barotrauma.MapCreatures.Behavior int highestPriority = 0; Item? currentItem = null; - foreach (Item item in Item.ItemList.Where(it => !Behavior.ClaimedTargets.Contains(it))) + foreach (Item item in Item.ItemList) { + if (item.Submarine != parent.Submarine || Vector2.DistanceSquared(worldPos, item.WorldPosition) > Behavior.Sight * Behavior.Sight) { continue; } + if (Behavior.ClaimedTargets.Contains(item)) { continue; } if (Behavior.IgnoredTargets.ContainsKey(item)) { continue; } int priority = 0; foreach (BallastFloraBehavior.AITarget target in Behavior.Targets) { - if (!target.Matches(item) || target.Priority <= highestPriority) { continue; } + if (target.Priority <= highestPriority || !target.Matches(item)) { continue; } priority = target.Priority; break; } if (priority == 0) { continue; } - if (item.Submarine != parent.Submarine || Vector2.Distance(worldPos, item.WorldPosition) > Behavior.Sight) { continue; } - Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position); #if DEBUG @@ -156,6 +156,7 @@ namespace Barotrauma.MapCreatures.Behavior { foreach (BallastFloraBranch existingBranch in Behavior.Branches) { + if (existingBranch.Health <= 0 || existingBranch.IsRootGrowth) { continue; } if (Behavior.BranchContainsTarget(existingBranch, currentItem)) { Behavior.ClaimTarget(currentItem, existingBranch); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs index 4d30f0f94..82254622b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs @@ -53,7 +53,7 @@ namespace Barotrauma.MapCreatures.Behavior List newList = new List(TargetBranches); foreach (BallastFloraBranch branch in newList) { - if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; } + if (branch.FailedGrowthAttempts > 8 || branch.DisconnectedFromRoot || !branch.CanGrowMore()) { continue; } // Get what side gets us closest to the target TileSide side = GetClosestSide(branch, Target.WorldPosition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 4c356b657..ab74f2a8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; @@ -70,6 +71,14 @@ namespace Barotrauma public double SpawnTime => spawnTime; private readonly double spawnTime; + private static UInt64 creationCounter = 0; + private readonly static object creationCounterMutex = new object(); + + public readonly string CreationStackTrace; + public readonly UInt64 CreationIndex; + public string ErrorLine + => $"- {ID}: {this} ({Submarine?.Info?.Name ?? "[null]"} {Submarine?.ID ?? 0}) {CreationStackTrace}"; + public Entity(Submarine submarine, ushort id) { this.Submarine = submarine; @@ -84,6 +93,31 @@ namespace Barotrauma } dictionary.Add(ID, this); + + CreationStackTrace = ""; +#if DEBUG + var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); + var frames = st.GetFrames(); + int frameCount = 0; + foreach (var frame in frames) + { + string fileName = frame.GetFileName(); + if ((fileName?.Contains("BarotraumaClient") ?? false) || (fileName?.Contains("BarotraumaServer") ?? false)) { break; } + + fileName = Path.GetFileNameWithoutExtension(fileName); + int fileLineNumber = frame.GetFileLineNumber(); + + if (fileName.IsNullOrEmpty() || fileLineNumber <= 0) { continue; } + + CreationStackTrace += $"{fileName}@{fileLineNumber}; "; + } +#endif + #warning TODO: consider removing this mutex, entity creation probably shouldn't be multithreaded + lock (creationCounterMutex) + { + CreationIndex = creationCounter; + creationCounter++; + } } protected virtual ushort DetermineID(ushort id, Submarine submarine) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 1c0e18576..442917563 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -59,10 +59,10 @@ namespace Barotrauma smoke = true; flames = true; underwaterBubble = true; - ignoreFireEffectsForTags = new string[0]; + ignoreFireEffectsForTags = Array.Empty(); } - public Explosion(XElement element, string parentDebugName) + public Explosion(ContentXElement element, string parentDebugName) { Attack = new Attack(element, parentDebugName + ", Explosion"); @@ -80,7 +80,7 @@ namespace Barotrauma playTinnitus = element.GetAttributeBool("playtinnitus", showEffects); applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects); - ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", new string[0], convertToLowerInvariant: true); + ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty(), convertToLowerInvariant: true); ignoreCover = element.GetAttributeBool("ignorecover", false); onlyInside = element.GetAttributeBool("onlyinside", false); @@ -482,7 +482,7 @@ namespace Barotrauma { List ballastFlorae = new List(); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.BallastFlora != null) { ballastFlorae.Add(hull.BallastFlora); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 3840832c6..bc8844700 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -136,7 +136,7 @@ namespace Barotrauma { } public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID) - : base(MapEntityPrefab.Find(null, "gap"), submarine, id) + : base(MapEntityPrefab.FindByIdentifier("gap".ToIdentifier()), submarine, id) { this.rect = rect; flowForce = Vector2.Zero; @@ -706,7 +706,7 @@ namespace Barotrauma base.ShallowRemove(); GapList.Remove(this); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); } @@ -717,7 +717,7 @@ namespace Barotrauma base.Remove(); GapList.Remove(this); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.ConnectedGaps.Remove(this); } @@ -734,7 +734,7 @@ namespace Barotrauma if (!DisableHullRechecks) FindHulls(); } - public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Gap Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = Rectangle.Empty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 869d1fe35..d86671e8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -101,8 +101,8 @@ namespace Barotrauma partial class Hull : MapEntity, ISerializableEntity, IServerSerializable { - public static List hullList = new List(); - public static List EntityGrids { get; } = new List(); + public readonly static List HullList = new List(); + public readonly static List EntityGrids = new List(); public static bool ShowHulls = true; @@ -124,8 +124,8 @@ namespace Barotrauma public const int BackgroundSectionsPerNetworkEvent = 16; - public readonly Dictionary properties; - public Dictionary SerializableProperties + public readonly Dictionary properties; + public Dictionary SerializableProperties { get { return properties; } } @@ -155,32 +155,23 @@ namespace Barotrauma public readonly List ConnectedGaps = new List(); - public override string Name - { - get - { - return "Hull"; - } - } + public override string Name => "Hull"; - public string DisplayName + public LocalizedString DisplayName { get; private set; } - private readonly HashSet moduleTags = new HashSet(); + private readonly HashSet moduleTags = new HashSet(); /// /// Inherited flags from outpost generation. /// - public IEnumerable OutpostModuleTags - { - get { return moduleTags; } - } + public IEnumerable OutpostModuleTags => moduleTags; private string roomName; - [Editable, Serialize("", true, translationTextTag: "RoomName.")] + [Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "RoomName.")] public string RoomName { get { return roomName; } @@ -188,7 +179,7 @@ namespace Barotrauma { if (roomName == value) { return; } roomName = value; - DisplayName = TextManager.Get(roomName, returnNull: true) ?? roomName; + DisplayName = TextManager.Get(roomName).Fallback(roomName); if (!IsWetRoom && ForceAsWetRoom) { IsWetRoom = true; @@ -200,7 +191,7 @@ namespace Barotrauma private Color ambientLight; - [Editable, Serialize("0,0,0,0", true)] + [Editable, Serialize("0,0,0,0", IsPropertySaveable.Yes)] public Color AmbientLight { get { return ambientLight; } @@ -314,7 +305,7 @@ namespace Barotrauma } } - [Serialize(100000.0f, true)] + [Serialize(100000.0f, IsPropertySaveable.Yes)] public float Oxygen { get { return oxygen; } @@ -332,7 +323,7 @@ namespace Barotrauma roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)); private bool isWetRoom; - [Editable, Serialize(false, true, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] public bool IsWetRoom { get { return isWetRoom; } @@ -347,7 +338,7 @@ namespace Barotrauma } private bool avoidStaying; - [Editable, Serialize(false, true, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Bots avoid staying here, but they are still allowed to access the room when needed and go through it. Forced true for wet rooms.")] public bool AvoidStaying { get { return avoidStaying || IsWetRoom; } @@ -469,7 +460,7 @@ namespace Barotrauma }; } - hullList.Add(this); + HullList.Add(this); if (submarine == null || !submarine.Loading) { @@ -488,11 +479,11 @@ namespace Barotrauma public static Rectangle GetBorders() { - if (!hullList.Any()) return Rectangle.Empty; + if (!HullList.Any()) return Rectangle.Empty; - Rectangle rect = hullList[0].rect; + Rectangle rect = HullList[0].rect; - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { if (hull.Rect.X < rect.X) { @@ -516,12 +507,15 @@ namespace Barotrauma public override MapEntity Clone() { - var clone = new Hull(MapEntityPrefab.Find(null, "hull"), rect, Submarine); - foreach (KeyValuePair property in SerializableProperties) + var clone = new Hull(MapEntityPrefab.FindByIdentifier("hull".ToIdentifier()), rect, Submarine); + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) { continue; } clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); } +#if CLIENT + clone.lastAmbientLightEditTime = 0.0; +#endif return clone; } @@ -536,17 +530,17 @@ namespace Barotrauma { var newGrid = new EntityGrid(submarine, 200.0f); EntityGrids.Add(newGrid); - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { if (hull.Submarine == submarine && !hull.IdFreed) { newGrid.InsertEntity(hull); } } return newGrid; } - public void SetModuleTags(IEnumerable tags) + public void SetModuleTags(IEnumerable tags) { moduleTags.Clear(); - foreach (string tag in tags) + foreach (Identifier tag in tags) { moduleTags.Add(tag); } @@ -630,7 +624,7 @@ namespace Barotrauma public override void ShallowRemove() { base.Remove(); - hullList.Remove(this); + HullList.Remove(this); if (Submarine == null || (!Submarine.Loading && !Submarine.Unloading)) { @@ -659,7 +653,7 @@ namespace Barotrauma public override void Remove() { base.Remove(); - hullList.Remove(this); + HullList.Remove(this); BallastFlora?.Remove(); if (Submarine != null && !Submarine.Loading && !Submarine.Unloading) @@ -712,7 +706,7 @@ namespace Barotrauma return null; } - var decal = GameMain.DecalManager.Prefabs.Find(p => p.UIntIdentifier == decalId); + var decal = DecalManager.Prefabs.Find(p => p.UintIdentifier == decalId); if (decal == null) { DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!"); @@ -732,7 +726,7 @@ namespace Barotrauma if (decals.Count >= MaxDecalsPerHull) { return null; } - var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); + var decal = DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); if (decal != null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -1110,12 +1104,12 @@ namespace Barotrauma /// public static Hull FindHullUnoptimized(Vector2 position, Hull guess = null, bool useWorldCoordinates = true, bool inclusive = true) { - if (guess != null && hullList.Contains(guess)) + if (guess != null && HullList.Contains(guess)) { if (Submarine.RectContains(useWorldCoordinates ? guess.WorldRect : guess.rect, position, inclusive)) return guess; } - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { if (Submarine.RectContains(useWorldCoordinates ? hull.WorldRect : hull.rect, position, inclusive)) return hull; } @@ -1135,15 +1129,15 @@ namespace Barotrauma else { Hull h = c.CurrentHull; - hullList.ForEach(j => j.Visible = false); + HullList.ForEach(j => j.Visible = false); List visibleHulls; if (h == null || c.Submarine == null) { - visibleHulls = hullList.FindAll(j => j.CanSeeOther(null, false)); + visibleHulls = HullList.FindAll(j => j.CanSeeOther(null, false)); } else { - visibleHulls = hullList.FindAll(j => h.CanSeeOther(j, true)); + visibleHulls = HullList.FindAll(j => h.CanSeeOther(j, true)); } visibleHulls.ForEach(j => j.Visible = true); foreach (Item it in Item.ItemList) @@ -1164,7 +1158,7 @@ namespace Barotrauma foreach (Gap g in ConnectedGaps) { if (g.ConnectedWall != null && g.ConnectedWall.CastShadow) continue; - List otherHulls = hullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this); + List otherHulls = HullList.FindAll(h => h.ConnectedGaps.Contains(g) && h != this); retVal = otherHulls.Any(h => h == other); if (!retVal && allowIndirect) retVal = otherHulls.Any(h => h.CanSeeOther(other, false)); if (retVal) return true; @@ -1174,7 +1168,7 @@ namespace Barotrauma { foreach (Gap g in ConnectedGaps) { - if (g.ConnectedDoor != null && !hullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true; + if (g.ConnectedDoor != null && !HullList.Any(h => h.ConnectedGaps.Contains(g) && h != this)) return true; } List structures = mapEntityList.FindAll(me => me is Structure && me.Rect.Intersects(Rect)); return structures.Any(st => !(st as Structure).CastShadow); @@ -1209,7 +1203,7 @@ namespace Barotrauma if (moduleFlags != null && moduleFlags.Any() && (Submarine.Info.Type == SubmarineType.OutpostModule || Submarine.Info.Type == SubmarineType.Outpost)) { - if (moduleFlags.Contains("airlock") && + if (moduleFlags.Contains("airlock".ToIdentifier()) && ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor != null)) { return "RoomName.Airlock"; @@ -1313,7 +1307,7 @@ namespace Barotrauma public static Hull GetCleanTarget(Vector2 worldPosition) { - foreach (Hull hull in hullList) + foreach (Hull hull in HullList) { Rectangle worldRect = hull.WorldRect; if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) { continue; } @@ -1476,7 +1470,7 @@ namespace Barotrauma } #endregion - public static Hull Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect; if (element.Attribute("rect") != null) @@ -1507,7 +1501,7 @@ namespace Barotrauma hull.OriginalAmbientLight = XMLExtensions.ParseColor(originalAmbientLight, false); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1525,7 +1519,7 @@ namespace Barotrauma } break; case "ballastflorabehavior": - string identifier = subElement.GetAttributeString("identifier", string.Empty); + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); BallastFloraPrefab prefab = BallastFloraPrefab.Find(identifier); if (prefab != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 5793f5b9c..13d72bc6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -5,64 +5,57 @@ using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; +using System.Net; namespace Barotrauma { partial class ItemAssemblyPrefab : MapEntityPrefab { - private readonly string name; - public override string Name { get { return name; } } - public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); public static readonly string SaveFolder = "ItemAssemblies"; - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } - private readonly XElement configElement; - - public List> DisplayEntities + + public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities; + + public readonly Rectangle Bounds; + + public override LocalizedString Name { get; } + + public override Sprite Sprite => null; + + public override string OriginalName => Name.Value; + + public override ImmutableHashSet Tags { get; } + + public override ImmutableHashSet AllowedLinks => null; + + public override MapEntityCategory Category => MapEntityCategory.ItemAssembly; + + public override ImmutableHashSet Aliases => null; + + protected override Identifier DetermineIdentifier(XElement element) { - get; - private set; + return element.GetAttributeIdentifier("identifier", element.GetAttributeIdentifier("name", "")); } - public Rectangle Bounds; - - public ItemAssemblyPrefab(string filePath, bool allowOverwrite = false) + public ItemAssemblyPrefab(ContentXElement element, ItemAssemblyFile file) : base(element, file) { - FilePath = filePath; - XDocument doc = XMLExtensions.TryLoadXml(filePath); - if (doc == null) { return; } - - XElement element = doc.Root; - if (element.IsOverride()) - { - element = element.Elements().First(); - } - - originalName = element.GetAttributeString("name", ""); - identifier = element.GetAttributeString("identifier", null) ?? originalName.ToLowerInvariant().Replace(" ", ""); configElement = element; - Category = MapEntityCategory.ItemAssembly; - SerializableProperty.DeserializeProperties(this, configElement); - name = TextManager.Get("EntityName." + identifier, returnNull: true) ?? originalName; - Description = TextManager.Get("EntityDescription." + identifier, returnNull: true) ?? Description; + Name = TextManager.Get($"EntityName.{Identifier}").Fallback(element.GetAttributeString("name", "")); + Description = TextManager.Get($"EntityDescription.{Identifier}"); + Tags = Enumerable.Empty().ToImmutableHashSet(); List containedItemIDs = new List(); foreach (XElement entityElement in element.Elements()) { - var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase)); + var containerElement = entityElement.GetChildElement("itemcontainer"); if (containerElement == null) { continue; } string containedString = containerElement.GetAttributeString("contained", ""); @@ -84,52 +77,36 @@ namespace Barotrauma int minX = int.MaxValue, minY = int.MaxValue; int maxX = int.MinValue, maxY = int.MinValue; - DisplayEntities = new List>(); + var displayEntities = new List<(Identifier, Rectangle)>(); foreach (XElement entityElement in element.Elements()) { ushort id = (ushort)entityElement.GetAttributeInt("ID", 0); if (id > 0 && containedItemIDs.Contains(id)) { continue; } - string identifier = entityElement.GetAttributeString("identifier", entityElement.Name.ToString().ToLowerInvariant()); - MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier); - if (mapEntity == null) - { - string entityName = entityElement.GetAttributeString("name", ""); - mapEntity = List.FirstOrDefault(p => p.Name == entityName); - } + Identifier identifier = entityElement.GetAttributeIdentifier("identifier", entityElement.Name.ToString().ToLowerInvariant()); Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty); - if (mapEntity != null && !entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) + if (!entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { - if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { DisplayEntities.Add(new Pair(mapEntity, rect)); } + if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { displayEntities.Add((identifier, rect)); } minX = Math.Min(minX, rect.X); minY = Math.Min(minY, rect.Y - rect.Height); maxX = Math.Max(maxX, rect.Right); maxY = Math.Max(maxY, rect.Y); } } + DisplayEntities = displayEntities.ToImmutableArray(); Bounds = minX == int.MaxValue ? new Rectangle(0, 0, 1, 1) : new Rectangle(minX, minY, maxX - minX, maxY - minY); - - if (allowOverwrite && Prefabs.ContainsKey(identifier)) - { - Prefabs.Remove(Prefabs[identifier]); - } - Prefabs.Add(this, doc.Root.IsOverride()); } - public static void Remove(string filePath) - { - Prefabs.RemoveByFile(filePath); - } - protected override void CreateInstance(Rectangle rect) { #if CLIENT var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub, selectInstance: Screen.Selected == GameMain.SubEditorScreen); - if (Screen.Selected is SubEditorScreen) + if (Screen.Selected is SubEditorScreen && loaded.Any()) { SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false)); } @@ -140,7 +117,7 @@ namespace Barotrauma public List CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false) { - return PasteEntities(position, sub, configElement, FilePath, selectInstance); + return PasteEntities(position, sub, configElement, ContentFile.Path.Value, selectInstance); } public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) @@ -183,53 +160,19 @@ namespace Barotrauma public void Delete() { Dispose(); - if (File.Exists(FilePath)) + if (File.Exists(ContentFile.Path)) { try { - File.Delete(FilePath); + File.Delete(ContentFile.Path); } catch (Exception e) { - DebugConsole.ThrowError("Deleting item assembly \"" + name + "\" failed.", e); + DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); } } } - public static void LoadAll() - { - if (GameSettings.VerboseLogging) - { - DebugConsole.Log("Loading item assembly prefabs: "); - } - - List itemAssemblyFiles = new List(); - - //find assembly files in the item assembly folders - if (Directory.Exists(VanillaSaveFolder)) - { - itemAssemblyFiles.AddRange(Directory.GetFiles(VanillaSaveFolder)); - } - if (Directory.Exists(SaveFolder)) - { - itemAssemblyFiles.AddRange(Directory.GetFiles(SaveFolder)); - } - - //find assembly files in selected content packages - foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages) - { - foreach (string filePath in cp.GetFilesOfType(ContentType.ItemAssembly)) - { - //ignore files that have already been added (= file saved to item assembly folder) - if (itemAssemblyFiles.Any(f => Path.GetFullPath(f) == Path.GetFullPath(filePath))) { continue; } - itemAssemblyFiles.Add(filePath); - } - } - - foreach (string file in itemAssemblyFiles) - { - new ItemAssemblyPrefab(file); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs new file mode 100644 index 000000000..1205fa136 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Biome.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Xml.Linq; + +namespace Barotrauma +{ + class Biome : PrefabWithUintIdentifier + { + public readonly static PrefabCollection Prefabs = new PrefabCollection(); + + public readonly Identifier OldIdentifier; + public readonly LocalizedString DisplayName; + public readonly LocalizedString Description; + + public readonly bool IsEndBiome; + + public readonly ImmutableHashSet AllowedZones; + + public Biome(ContentXElement element, LevelGenerationParametersFile file) : base(file, ParseIdentifier(element)) + { + OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); + + DisplayName = + TextManager.Get("biomename." + Identifier).Fallback( + element.GetAttributeString("name", "Biome")); + + Description = + TextManager.Get("biomedescription." + Identifier).Fallback( + element.GetAttributeString("description", "")); + + IsEndBiome = element.GetAttributeBool("endbiome", false); + + AllowedZones = element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }).ToImmutableHashSet(); + } + + public static Identifier ParseIdentifier(ContentXElement element) + { + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + if (identifier.IsEmpty) + { + identifier = element.GetAttributeIdentifier("name", ""); + DebugConsole.ThrowError("Error in biome \"" + identifier + "\": identifier missing, using name as the identifier."); + } + return identifier; + } + + public override void Dispose() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index e365e2c66..4adb2834f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -7,23 +7,18 @@ using System.Xml.Linq; namespace Barotrauma { - class CaveGenerationParams : ISerializableEntity + class CaveGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { - public static List CaveParams { get; private set; } + public readonly static PrefabCollection CaveParams = new PrefabCollection(); - public string Name - { - get { return Identifier; } - } - - public readonly string Identifier; + public string Name => Identifier.Value; private int minWidth, maxWidth; private int minHeight, maxHeight; private int minBranchCount, maxBranchCount; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; @@ -33,100 +28,100 @@ namespace Barotrauma /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// - public readonly Dictionary OverrideCommonness = new Dictionary(); + public readonly Dictionary OverrideCommonness = new Dictionary(); - [Editable, Serialize(1.0f, true)] + [Editable, Serialize(1.0f, IsPropertySaveable.Yes)] public float Commonness { get; private set; } - [Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)] + [Serialize(8000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 100000)] public int MinWidth { get { return minWidth; } set { minWidth = Math.Max(value, 1000); } } - [Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] + [Serialize(10000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] public int MaxWidth { get { return maxWidth; } set { maxWidth = Math.Max(value, minWidth); } } - [Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)] + [Serialize(8000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 100000)] public int MinHeight { get { return minHeight; } set { minHeight = Math.Max(value, 1000); } } - [Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] + [Serialize(10000, IsPropertySaveable.Yes), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] public int MaxHeight { get { return maxHeight; } set { maxHeight = Math.Max(value, minHeight); } } - [Serialize(2, true), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(2, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinBranchCount { get { return minBranchCount; } set { minBranchCount = Math.Max(value, 0); } } - [Serialize(4, true), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(4, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxBranchCount { get { return maxBranchCount; } set { maxBranchCount = Math.Max(value, minBranchCount); } } - [Serialize(50, true), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(50, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000)] public int LevelObjectAmount { get; set; } - [Serialize(0.1f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] + [Serialize(0.1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] public float DestructibleWallRatio { get; set; } - public Sprite WallSprite { get; private set; } - public Sprite WallEdgeSprite { get; private set; } + public readonly Sprite WallSprite; + public readonly Sprite WallEdgeSprite; public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand) { - if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) + var caveParams = CaveParams.OrderBy(p => p.UintIdentifier).ToList(); + if (caveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) { - return CaveParams.First(); + return caveParams.First(); } - return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); + return ToolBox.SelectWeightedRandom(caveParams.ToList(), caveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); } public float GetCommonness(LevelGenerationParams generationParams, bool abyss) { - if (generationParams?.Identifier != null && - OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness)) + if (generationParams != null && + generationParams.Identifier != Identifier.Empty && + OverrideCommonness.TryGetValue(abyss ? "abyss".ToIdentifier() : generationParams.Identifier, out float commonness)) { return commonness; } return Commonness; } - private CaveGenerationParams(XElement element) + public CaveGenerationParams(ContentXElement element, CaveGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Identifier = element == null ? "default" : element.GetAttributeString("identifier", null) ?? element.Name.ToString(); - Identifier = Identifier.ToLowerInvariant(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -137,7 +132,7 @@ namespace Barotrauma WallEdgeSprite = new Sprite(subElement); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelType = subElement.GetAttributeIdentifier("leveltype", ""); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -147,71 +142,16 @@ namespace Barotrauma } } - public static void LoadPresets() - { - CaveParams = new List(); - - var files = GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters); - if (!files.Any()) - { - files = new List() { new ContentFile("Content/Map/CaveGenerationParameters.xml", ContentType.CaveGenerationParameters) }; - } - - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - CaveParams.Clear(); - DebugConsole.NewMessage($"Overriding cave generation parameters with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - if (isOverride) - { - string identifier = element.FirstElement().GetAttributeString("identifier", ""); - var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (existingParams != null) - { - DebugConsole.NewMessage($"Overriding the cave generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow); - CaveParams.Remove(existingParams); - } - CaveParams.Add(new CaveGenerationParams(element.FirstElement())); - - } - else - { - string identifier = element.FirstElement().GetAttributeString("identifier", ""); - var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (existingParams != null) - { - DebugConsole.ThrowError($"Duplicate cave generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use tags to override the generation parameters."); - continue; - } - else - { - CaveParams.Add(new CaveGenerationParams(element)); - } - } - } - } - } - public void Save(XElement element) { SerializableProperty.SerializeProperties(this, element, true); - foreach (KeyValuePair overrideCommonness in OverrideCommonness) + foreach (KeyValuePair overrideCommonness in OverrideCommonness) { bool elementFound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { - if (subElement.Name.ToString().Equals("overridecommonness", StringComparison.OrdinalIgnoreCase) - && subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, StringComparison.OrdinalIgnoreCase)) + if (subElement.NameAsIdentifier() == "overridecommonness" + && subElement.GetAttributeIdentifier("leveltype", "") == overrideCommonness.Key) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); elementFound = true; @@ -226,5 +166,10 @@ namespace Barotrauma } } } + + public override void Dispose() + { + WallSprite?.Remove(); WallEdgeSprite?.Remove(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 8d1c11c4b..735066d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -279,7 +279,7 @@ namespace Barotrauma { float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount)); - float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server); + float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient); Vector2 extrudedPoint = edge.Point1 + edgeDir * (i / (float)pointCount) + @@ -469,7 +469,7 @@ namespace Barotrauma for (int i = 0; i < vertexCount; i++) { Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); - verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)); + verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.ServerAndClient)); angle += angleStep; } return verts; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index bd22ea0ce..5fac8f4b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -11,6 +11,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.IO; using Voronoi2; namespace Barotrauma @@ -400,6 +401,8 @@ namespace Barotrauma Loaded = this; Generating = true; + Rand.Tracker.Reset(); + Rand.Tracker.Active = true; EqualityCheckValues.Clear(); EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); @@ -410,7 +413,7 @@ namespace Barotrauma EndLocation = GameMain.GameSession?.EndLocation; } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); LevelObjectManager = new LevelObjectManager(); @@ -420,11 +423,8 @@ namespace Barotrauma #if CLIENT if (backgroundCreatureManager == null) { - var files = GameMain.Instance.GetFilesOfType(ContentType.BackgroundCreaturePrefabs); - if (files.Count() > 0) - backgroundCreatureManager = new BackgroundCreatureManager(files); - else - backgroundCreatureManager = new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml"); + var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()).ToArray(); + backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml"); } #endif Stopwatch sw = new Stopwatch(); @@ -476,7 +476,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.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- //generate the initial nodes for the main path and smaller tunnels @@ -550,20 +550,20 @@ namespace Barotrauma Tunnels.Add(abyssTunnel); } - int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.Server); + 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.Server)]; + Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.ServerAndClient)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } - Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.Server)]; - Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.Server)]; + Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.ServerAndClient)]; + Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.ServerAndClient)]; var sidePathNodes = GeneratePathNodes(branchStart, branchEnd, pathBorders, tunnelToBranchOff, GenerationParams.SideTunnelVariance); //make sure the path is wide enough to pass through - int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.Server); + int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.ServerAndClient); Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff)); } @@ -572,7 +572,7 @@ namespace Barotrauma GenerateAbyssArea(); GenerateCaves(mainPath); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- //generate voronoi sites @@ -583,21 +583,21 @@ namespace Barotrauma Point siteVariance = GenerationParams.VoronoiSiteVariance; siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - int caveSiteInterval = 500; + const int caveSiteInterval = 500; for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) { for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) { - int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.Server); - int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.Server); + int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X + 1, Rand.RandSync.ServerAndClient); + int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y + 1, Rand.RandSync.ServerAndClient); bool closeToTunnel = false; bool closeToCave = false; foreach (Tunnel tunnel in Tunnels) { + float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); for (int i = 1; i < tunnel.Nodes.Count; i++) { - float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } @@ -607,7 +607,7 @@ namespace Barotrauma if (Math.Sqrt(tunnelDistSqr) < minDist) { closeToTunnel = true; - tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + //tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); if (tunnel.Type == TunnelType.Cave) { closeToCave = true; @@ -620,7 +620,7 @@ namespace Barotrauma if (!closeToTunnel) { //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry - if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; } + if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; } } if (!TooClose(siteX, siteY)) @@ -635,8 +635,8 @@ namespace Barotrauma { for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) { - int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); - int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); + int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); + int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient); if (!TooClose(caveSiteX, caveSiteY)) { @@ -677,7 +677,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // construct the voronoi graph and cells @@ -778,7 +778,7 @@ namespace Barotrauma for (int i = 0; i < GenerationParams.IslandCount; i++) { if (potentialIslands.Count == 0) { break; } - var island = potentialIslands.GetRandom(Rand.RandSync.Server); + var island = potentialIslands.GetRandom(Rand.RandSync.ServerAndClient); island.CellType = CellType.Solid; island.Island = true; pathCells.Remove(island); @@ -795,7 +795,7 @@ namespace Barotrauma startPosition.X = (int)pathCells[0].Site.Coord.X; startExitPosition.X = startPosition.X; - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level @@ -862,8 +862,6 @@ namespace Barotrauma // mirror if needed //---------------------------------------------------------------------------------- - int asdfasdf = Rand.Int(int.MaxValue, Rand.RandSync.Server); - if (mirror) { HashSet mirroredEdges = new HashSet(); @@ -1008,7 +1006,7 @@ namespace Barotrauma caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells)); foreach (var caveCell in caveCells) { - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) { var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f); if (chunk != null) @@ -1020,7 +1018,7 @@ namespace Barotrauma } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // create some ruins @@ -1033,7 +1031,7 @@ namespace Barotrauma GenerateRuin(ruinPositions[i], mirror); } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // create floating ice chunks @@ -1054,18 +1052,18 @@ namespace Barotrauma for (int i = 0; i < GenerationParams.FloatingIceChunkCount; i++) { if (iceChunkPositions.Count == 0) { break; } - Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.Server)]; - float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server); + Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.ServerAndClient)]; + float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.ServerAndClient); var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f); var chunk = CreateIceChunk(vertices, selectedPos.ToVector2()); chunk.MoveAmount = new Vector2(0.0f, minMainPathWidth * 0.7f); - chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server); + chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.ServerAndClient); ExtraWalls.Add(chunk); iceChunkPositions.Remove(selectedPos); } } - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // generate the bodies and rendered triangles of the cells @@ -1170,7 +1168,7 @@ namespace Barotrauma } #endif - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // create ice spires @@ -1205,7 +1203,7 @@ namespace Barotrauma CreateOutposts(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); //---------------------------------------------------------------------------------- // top barrier & sea floor @@ -1247,15 +1245,15 @@ namespace Barotrauma CreateWrecks(); CreateBeaconStation(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); GenerateItems(); - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient)); #if CLIENT backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); @@ -1283,7 +1281,7 @@ namespace Barotrauma Debug.WriteLine("Seed: " + Seed); Debug.WriteLine("**********************************************************************************"); - if (GameSettings.VerboseLogging) + if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.NewMessage("Generated level with the seed " + Seed + " (type: " + GenerationParams.Identifier + ")", Color.White); } @@ -1301,6 +1299,8 @@ namespace Barotrauma //assign an ID to make entity events work //ID = FindFreeID(); Generating = false; + Rand.Tracker.Active = false; + File.WriteAllLines(GameMain.NetworkMember is { IsServer: true } ? "serverrng.txt" : "clientrng.txt", Rand.Tracker.LogMsgs); } private List GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance) @@ -1311,9 +1311,9 @@ namespace Barotrauma for (int x = startPosition.X + nodeInterval.X; x < endPosition.X - nodeInterval.X; - x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) + x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.ServerAndClient)) { - Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server)); + Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.ServerAndClient)); //allow placing the 2nd main path node at any height regardless of variance //(otherwise low variance will always make the main path go through the upper part of the level) @@ -1378,7 +1378,7 @@ namespace Barotrauma foreach (VoronoiCell cell in cells) { if (cell.Edges.Any(e => e.NextToCave)) { continue; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) { continue; } + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > holeProbability) { continue; } if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; } float closestDist = 0.0f; @@ -1620,13 +1620,13 @@ namespace Barotrauma //above the bottom of the level = can't place a point here if (seaFloorPos > AbyssStart) { continue; } - float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.Server)); + float yPos = MathHelper.Lerp(AbyssStart, Math.Max(seaFloorPos, AbyssArea.Y), Rand.Range(0.2f, 1.0f, Rand.RandSync.ServerAndClient)); foreach (var abyssIsland in AbyssIslands) { if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos))) { - xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.Server) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f); + xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1, Rand.RandSync.ServerAndClient) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f); } } @@ -1681,7 +1681,7 @@ namespace Barotrauma Point islandSize = Vector2.Lerp( GenerationParams.AbyssIslandSizeMin.ToVector2(), GenerationParams.AbyssIslandSizeMax.ToVector2(), - Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)).ToPoint(); + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient)).ToPoint(); if (AbyssArea.Height < islandSize.Y) { return; } @@ -1697,8 +1697,8 @@ namespace Barotrauma do { islandPosition = new Point( - Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.Server), - Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.Server)); + Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.ServerAndClient), + Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.ServerAndClient)); //move the island above the sea floor geometry islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500); @@ -1714,7 +1714,7 @@ namespace Barotrauma break; } - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > GenerationParams.AbyssIslandCaveProbability) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) > GenerationParams.AbyssIslandCaveProbability) { float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); @@ -1735,8 +1735,8 @@ namespace Barotrauma { for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y) { - siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server)); - siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server)); + siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.ServerAndClient)); + siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.ServerAndClient)); } } @@ -1765,7 +1765,7 @@ namespace Barotrauma } } - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.Server); + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.ServerAndClient); float caveScaleRelativeToIsland = 0.7f; GenerateCave( @@ -1786,12 +1786,12 @@ namespace Barotrauma new Point(0, BottomPos) }; - int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.Server); + int mountainCount = Rand.Range(GenerationParams.MountainCountMin, GenerationParams.MountainCountMax + 1, Rand.RandSync.ServerAndClient); for (int i = 0; i < mountainCount; i++) { bottomPositions.Add( new Point(Size.X / (mountainCount + 1) * (i + 1), - BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.Server))); + BottomPos + Rand.Range(GenerationParams.MountainHeightMin, GenerationParams.MountainHeightMax + 1, Rand.RandSync.ServerAndClient))); } bottomPositions.Add(new Point(Size.X, BottomPos)); @@ -1804,8 +1804,8 @@ namespace Barotrauma bottomPositions.Insert(i + 1, new Point( (bottomPositions[i].X + bottomPositions[i + 1].X) / 2, - (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.Server))); - i++; + (bottomPositions[i].Y + bottomPositions[i + 1].Y) / 2 + Rand.Range(0, GenerationParams.SeaFloorVariance + 1, Rand.RandSync.ServerAndClient))); + i++; } currInverval /= 2; @@ -1834,10 +1834,10 @@ namespace Barotrauma { for (int i = 0; i < GenerationParams.CaveCount; i++) { - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.Server); + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.ServerAndClient); Point caveSize = new Point( - Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.Server), - Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.Server)); + Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.ServerAndClient), + Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.ServerAndClient)); int padding = (int)(caveSize.X * 1.2f); Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2); @@ -1891,12 +1891,12 @@ namespace Barotrauma Tunnels.Add(tunnel); caveBranches.Add(tunnel); - int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.Server); + int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount + 1, Rand.RandSync.ServerAndClient); for (int j = 0; j < branches; j++) { - Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); - Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2(); - Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2(); + Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.ServerAndClient); + Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.ServerAndClient)].ToVector2(); + Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.ServerAndClient)].ToVector2(); var branchSegments = MathUtils.GenerateJaggedLine( branchStartPos, branchEndPos, iterations: 3, @@ -1930,17 +1930,17 @@ namespace Barotrauma private void GenerateRuin(Point ruinPos, bool mirror) { - var ruinGenerationParams = RuinGenerationParams.GetRandom(Rand.RandSync.Server); + var ruinGenerationParams = RuinGenerationParams.RuinParams.GetRandom(Rand.RandSync.ServerAndClient); LocationType locationType = StartLocation?.Type; if (locationType == null) { - locationType = LocationType.List.GetRandom(Rand.RandSync.Server); + locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); if (ruinGenerationParams.AllowedLocationTypes.Any()) { - locationType = LocationType.List.Where(lt => + locationType = LocationType.Prefabs.Where(lt => ruinGenerationParams.AllowedLocationTypes.Any(allowedType => - allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server); + allowedType == "any" || lt.Identifier == allowedType)).GetRandom(Rand.RandSync.ServerAndClient); } } @@ -2111,7 +2111,7 @@ namespace Barotrauma if (pointsAboveBottom.Count == 0) { DebugConsole.ThrowError("Error in FindPosAwayFromMainPath: no valid positions above the bottom of the sea floor. Has the position of the sea floor been set too high up?"); - return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.Server)].point; + return distanceField[Rand.Int(distanceField.Count, Rand.RandSync.ServerAndClient)].point; } var validPoints = pointsAboveBottom.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point))); @@ -2154,7 +2154,7 @@ namespace Barotrauma } else { - return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].point; + return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.ServerAndClient)].point; } } @@ -2270,7 +2270,7 @@ namespace Barotrauma { const float maxLength = 15000.0f; float minEdgeLength = 100.0f; - var mainPathPos = PositionsOfInterest.Where(pos => pos.PositionType == PositionType.MainPath).GetRandom(Rand.RandSync.Server); + var mainPathPos = PositionsOfInterest.GetRandom(pos => pos.PositionType == PositionType.MainPath, Rand.RandSync.ServerAndClient); double closestDistSqr = double.PositiveInfinity; GraphEdge closestEdge = null; VoronoiCell closestCell = null; @@ -2307,13 +2307,13 @@ namespace Barotrauma float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength); spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f); - Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); - Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); + Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient); + Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.ServerAndClient); List vertices = new List() { closestEdge.Point1, - extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server), - extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server), + extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient), + extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.ServerAndClient), closestEdge.Point2, }; Vector2 center = Vector2.Zero; @@ -2364,8 +2364,8 @@ namespace Barotrauma }; } } - public List ResourceTags { get; } - public List ResourceIds { get; } + public List ResourceTags { get; } + public List ResourceIds { get; } public List ClusterLocations { get; } public TunnelType TunnelType { get; } @@ -2374,8 +2374,8 @@ namespace Barotrauma Id = id; Position = position; ShouldContainResources = shouldContainResources; - ResourceTags = new List(); - ResourceIds = new List(); + ResourceTags = new List(); + ResourceIds = new List(); ClusterLocations = new List(); TunnelType = tunnelType; } @@ -2417,14 +2417,14 @@ namespace Barotrauma // Such as the exploding crystals in The Great Sea private void GenerateItems() { - string levelName = GenerationParams.Identifier.ToLowerInvariant(); + Identifier levelName = GenerationParams.Identifier; float minCommonness = float.MaxValue, maxCommonness = float.MinValue; List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>(); var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>(); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier)) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || - itemPrefab.LevelCommonness.TryGetValue("", out commonness)) + itemPrefab.LevelCommonness.TryGetValue(Identifier.Empty, out commonness)) { if (commonness <= 0.0f) { continue; } if (commonness < minCommonness) { minCommonness = commonness; } @@ -2432,7 +2432,7 @@ namespace Barotrauma levelResources.Add((itemPrefab, commonness)); } else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) || - itemPrefab.LevelQuantity.TryGetValue("", out fixedQuantityResourceInfo)) + itemPrefab.LevelQuantity.TryGetValue(Identifier.Empty, out fixedQuantityResourceInfo)) { fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); } @@ -2450,12 +2450,12 @@ namespace Barotrauma var location = allValidLocations.GetRandom(l => { if (l.Cell == null || l.Edge == null) { return false; } - if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; } + if (resourceInfo.IsIslandSpecific && !l.Cell.Island) { return false; } if (!resourceInfo.AllowAtStart && l.EdgeCenter.Y > startPosition.Y && l.EdgeCenter.X < Size.X * 0.25f) { return false; } if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; } return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _); - }, randSync: Rand.RandSync.Server); + }, randSync: Rand.RandSync.ServerAndClient); if (location.Cell == null || location.Edge == null) { break; } @@ -2478,10 +2478,10 @@ namespace Barotrauma if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } l.InitializeResources(); return l.Resources.Count <= GetMaxResourcesOnEdge(itemPrefab, l, out _); - }, randSync: Rand.RandSync.Server); + }, randSync: Rand.RandSync.ServerAndClient); if (location.Cell == null || location.Edge == null) { break; } - int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.Server); + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient); PlaceResources(itemPrefab, clusterSize, location, out var abyssResources); var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); abyssClusterLocation.Resources.AddRange(abyssResources); @@ -2509,7 +2509,7 @@ namespace Barotrauma var intervalRange = tunnel.Type != TunnelType.Cave ? GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange; do { - var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.Server); + var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.ServerAndClient); reachedLastNode = !CalculatePositionOnPath(); var id = Tunnels.IndexOf(tunnel) + ":" + nextPathPointId++; var spawnChance = tunnel.Type == TunnelType.Cave || tunnel.ParentTunnel?.Type == TunnelType.Cave ? @@ -2517,7 +2517,7 @@ namespace Barotrauma var containsResources = true; if (spawnChance < 1.0f) { - var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.Server); + var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.ServerAndClient); containsResources = spawnPointRoll <= spawnChance; } var tunnelType = tunnel.Type; @@ -2544,7 +2544,7 @@ namespace Barotrauma } int itemCount = 0; - string[] exclusiveResourceTags = new string[2] { "ore", "plant" }; + Identifier[] exclusiveResourceTags = new Identifier[2] { "ore".ToIdentifier(), "plant".ToIdentifier() }; // Create first cluster for each spawn point foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources)) @@ -2563,14 +2563,14 @@ namespace Barotrauma { var availablePathPoints = PathPoints.Where(p => p.ShouldContainResources && p.NextClusterProbability > 0 && - !excludedPathPointIds.Contains(p.Id)); + !excludedPathPointIds.Contains(p.Id)).ToList(); if (availablePathPoints.None()) { break; } var pathPoint = ToolBox.SelectWeightedRandom( - availablePathPoints.ToList(), + availablePathPoints, availablePathPoints.Select(p => p.NextClusterProbability).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); GenerateAdditionalCluster(pathPoint); } @@ -2580,9 +2580,9 @@ namespace Barotrauma while (itemCount < GenerationParams.ItemCount) { // We need to start filling some of the path points previously set to not contain resources - var availablePathPoints = PathPoints.Where(p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None()); - if (availablePathPoints.None()) { break; } - var pathPoint = availablePathPoints.GetRandom(randSync: Rand.RandSync.Server); + Func availablePathPoints = p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None(); + if (PathPoints.None(availablePathPoints)) { break; } + var pathPoint = PathPoints.GetRandom(availablePathPoints, randSync: Rand.RandSync.ServerAndClient); if (!GenerateFirstCluster(pathPoint)) { excludedPathPointIds.Add(pathPoint.Id); @@ -2716,7 +2716,7 @@ namespace Barotrauma if (validLocations.Any()) { - var location = validLocations.GetRandom(randSync: Rand.RandSync.Server); + var location = validLocations.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (CreateResourceCluster(pathPoint, location)) { var i = allValidLocations.FindIndex(l => l.Equals(location)); @@ -2764,7 +2764,7 @@ namespace Barotrauma selectedPrefab = ToolBox.SelectWeightedRandom( levelResources.Select(it => it.itemPrefab).ToList(), levelResources.Select(it => it.commonness).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); selectedPrefab.Tags.ForEach(t => { if (exclusiveResourceTags.Contains(t)) @@ -2781,7 +2781,7 @@ namespace Barotrauma selectedPrefab = ToolBox.SelectWeightedRandom( filteredResources.Select(it => it.itemPrefab).ToList(), filteredResources.Select(it => it.commonness).ToList(), - Rand.RandSync.Server); + Rand.RandSync.ServerAndClient); } if (selectedPrefab == null) { return false; } @@ -2800,7 +2800,7 @@ namespace Barotrauma if (maxClusterSize < 1) { return false; } var minClusterSize = Math.Min(GenerationParams.ResourceClusterSizeRange.X, maxClusterSize); - var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.Server); + var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.ServerAndClient); if (resourcesInCluster < 1) { return false; } @@ -2863,7 +2863,7 @@ namespace Barotrauma } } - var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.Server); + var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.ServerAndClient); var poiPos = poi.Position.ToVector2(); allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter) .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter))); @@ -2969,11 +2969,11 @@ namespace Barotrauma var lerpAmount = 0.0f; for (int i = 1; i < resourceCount; i++) { - var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.Server); + var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.ServerAndClient); lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value; lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f); } - var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.Server); + var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.ServerAndClient); placedResources = new List(); for (int i = 0; i < resourceCount; i++) { @@ -2981,7 +2981,7 @@ namespace Barotrauma var item = new Item(resourcePrefab, selectedPos, submarine: null); Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); - moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server); + moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient); item.Move(edgeNormal * moveAmount, ignoreContacts: true); if (item.GetComponent() is Holdable h) { @@ -3012,7 +3012,7 @@ namespace Barotrauma { TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); - Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); + Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient); if (!cells.Any(c => c.IsPointInside(startPos + offset))) { startPos += offset; @@ -3081,7 +3081,7 @@ namespace Barotrauma #if DEBUG DebugConsole.ThrowError(errorMsg); #endif - position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position; + position = PositionsOfInterest[Rand.Int(PositionsOfInterest.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; return false; } @@ -3122,7 +3122,7 @@ namespace Barotrauma return false; } - position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))].Position; + position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position; return true; } @@ -3366,9 +3366,9 @@ namespace Barotrauma private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type) { var tempSW = new Stopwatch(); - + // Min distance between a sub and the start/end/other sub. - float minDistance = Sonar.DefaultSonarRange; + const float minDistance = Sonar.DefaultSonarRange; var waypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == null && wp.SpawnType == SpawnType.Path && @@ -3376,7 +3376,7 @@ namespace Barotrauma !IsCloseToStart(wp.WorldPosition, minDistance) && !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList(); - var subDoc = SubmarineInfo.OpenFile(contentFile.Path); + var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. @@ -3419,7 +3419,7 @@ namespace Barotrauma { Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); tempSW.Restart(); - SubmarineInfo info = new SubmarineInfo(contentFile.Path) + SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value) { Type = type }; @@ -3431,13 +3431,13 @@ namespace Barotrauma PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub)); foreach (Hull hull in sub.GetHulls(false)) { - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance) + if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.WreckHullFloodingChance) { - hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); + hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.ServerAndClient); } } // Only spawn thalamus when the wreck has some thalamus items defined. - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) + if (Rand.Value(Rand.RandSync.ServerAndClient) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) { if (!sub.CreateWreckAI()) { @@ -3550,7 +3550,7 @@ namespace Barotrauma spawnPoint = Vector2.Zero; while (waypoints.Any()) { - var wp = waypoints.GetRandom(Rand.RandSync.Server); + var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient); waypoints.Remove(wp); if (!IsBlocked(wp.WorldPosition, paddedDimensions)) { @@ -3665,17 +3665,19 @@ namespace Barotrauma { var totalSW = new Stopwatch(); totalSW.Start(); - var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); + var wreckFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToList(); if (wreckFiles.None()) { DebugConsole.ThrowError("No wreck files found in the selected content packages!"); return; } - wreckFiles.Shuffle(Rand.RandSync.Server); + wreckFiles.Shuffle(Rand.RandSync.ServerAndClient); int minWreckCount = Math.Min(Loaded.GenerationParams.MinWreckCount, wreckFiles.Count); int maxWreckCount = Math.Min(Loaded.GenerationParams.MaxWreckCount, wreckFiles.Count); - int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.Server); + int wreckCount = Rand.Range(minWreckCount, maxWreckCount + 1, Rand.RandSync.ServerAndClient); if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireWreck) ?? false) { @@ -3693,7 +3695,7 @@ namespace Barotrauma ContentFile contentFile = wreckFiles.First(); wreckFiles.RemoveAt(0); if (contentFile == null) { continue; } - string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null) { //placed successfully @@ -3701,7 +3703,7 @@ namespace Barotrauma } attempts++; } - + } totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); @@ -3743,8 +3745,10 @@ namespace Barotrauma private void CreateOutposts() { - var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost).ToList(); - if (!outpostFiles.Any() && !OutpostGenerationParams.Params.Any() && LevelData.ForceOutpostGenerationParams == null) + var outpostFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToList(); + if (!outpostFiles.Any() && !OutpostGenerationParams.OutpostParams.Any() && LevelData.ForceOutpostGenerationParams == null) { DebugConsole.ThrowError("No outpost files found in the selected content packages"); return; @@ -3768,7 +3772,7 @@ namespace Barotrauma Submarine outpost; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { - if (OutpostGenerationParams.Params.Any() || LevelData.ForceOutpostGenerationParams != null) + if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null) { Location location = i == 0 ? StartLocation : EndLocation; @@ -3779,31 +3783,29 @@ namespace Barotrauma } else { - var suitableParams = OutpostGenerationParams.Params - .Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); + var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier)); if (!suitableParams.Any()) { - suitableParams = OutpostGenerationParams.Params - .Where(p => location == null || !p.AllowedLocationTypes.Any()); + suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any()); + if (!suitableParams.Any()) + { + DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters."); + suitableParams = OutpostGenerationParams.OutpostParams; + } } - if (!suitableParams.Any()) - { - DebugConsole.ThrowError("No suitable outpost generation parameters found for the location type \"" + location.Type.Identifier + "\". Selecting random parameters."); - suitableParams = OutpostGenerationParams.Params; - } - outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.Server); + outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.ServerAndClient); } LocationType locationType = location?.Type; if (locationType == null) { - locationType = LocationType.List.GetRandom(Rand.RandSync.Server); + locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient); if (outpostGenerationParams.AllowedLocationTypes.Any()) { - locationType = LocationType.List.Where(lt => - outpostGenerationParams.AllowedLocationTypes.Any(allowedType => - allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server); + locationType = LocationType.Prefabs.GetRandom(lt => + outpostGenerationParams.AllowedLocationTypes.Any(allowedType => + allowedType == "any" || lt.Identifier == allowedType), Rand.RandSync.ServerAndClient); } } @@ -3820,7 +3822,7 @@ namespace Barotrauma foreach (string categoryToHide in locationType.HideEntitySubcategories) { - foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.prefab?.HasSubCategory(categoryToHide) ?? false))) + foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && (me.Prefab?.HasSubCategory(categoryToHide) ?? false))) { entityToHide.HiddenInGame = true; } @@ -3830,8 +3832,8 @@ namespace Barotrauma { DebugConsole.NewMessage($"Loading a pre-built outpost for the {(isStart ? "start" : "end")} of the level..."); //backwards compatibility: if there are no generation params available, try to load an outpost file saved as a sub - ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server); - outpostInfo = new SubmarineInfo(outpostFile.Path) + ContentFile outpostFile = outpostFiles.GetRandom(Rand.RandSync.ServerAndClient); + outpostInfo = new SubmarineInfo(outpostFile.Path.Value) { Type = SubmarineType.Outpost }; @@ -3937,14 +3939,16 @@ namespace Barotrauma private void CreateBeaconStation() { if (!LevelData.HasBeaconStation) { return; } - var beaconStationFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.BeaconStation).ToList(); + var beaconStationFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToList(); if (beaconStationFiles.None()) { DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!"); return; } - var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.Server); - string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient); + string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); if (BeaconStation == null) { return; } @@ -3976,10 +3980,7 @@ namespace Barotrauma Repairable repairable = reactorItem.GetComponent(); if (repairable != null) { - if (repairable != null) - { - repairable.DeteriorationSpeed = 0.0f; - } + repairable.DeteriorationSpeed = 0.0f; } } if (LevelData.IsBeaconActive) @@ -3988,7 +3989,7 @@ namespace Barotrauma reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault())) { ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()]; - Spawner.AddToSpawnQueue( + Spawner.AddItemToSpawnQueue( fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); } @@ -4007,7 +4008,7 @@ namespace Barotrauma foreach (Item item in reactorContainer.Inventory.AllItems) { if (item.NonInteractable) { continue; } - Spawner.AddToRemoveQueue(item); + Spawner.AddItemToRemoveQueue(item); } } @@ -4141,8 +4142,8 @@ namespace Barotrauma job ??= selectedPrefab.GetJobPrefab(); if (job == null) { continue; } - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server); - var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + 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; @@ -4163,7 +4164,7 @@ namespace Barotrauma bool TryGetExtraSpawnPoint(out Vector2 point) { point = Vector2.Zero; - var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(Rand.RandSync.Unsynced); + var hull = Hull.HullList.FindAll(h => h.Submarine == wreck).GetRandomUnsynced(); if (hull != null) { point = hull.WorldPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index eb59a650b..4fd6d7166 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -92,7 +92,7 @@ namespace Barotrauma OriginallyHadHuntingGrounds = element.GetAttributeBool("originallyhadhuntinggrounds", HasHuntingGrounds); string generationParamsId = element.GetAttributeString("generationparams", ""); - GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId); + GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || (!l.OldIdentifier.IsEmpty && l.OldIdentifier == generationParamsId)); if (GenerationParams == null) { DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\"."); @@ -106,18 +106,18 @@ namespace Barotrauma InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin); string biomeIdentifier = element.GetAttributeString("biome", ""); - Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier || b.OldIdentifier == biomeIdentifier); + Biome = Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeIdentifier || (!b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeIdentifier)); if (Biome == null) { DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\"."); - Biome = LevelGenerationParams.GetBiomes().First(); + Biome = Biome.Prefabs.First(); } string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { }); - EventHistory.AddRange(EventSet.PrefabList.Where(p => prefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); + EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n))); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); - NonRepeatableEvents.AddRange(EventSet.PrefabList.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier.Equals(n, StringComparison.InvariantCultureIgnoreCase)))); + NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); } @@ -129,7 +129,7 @@ namespace Barotrauma Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName; Biome = locationConnection.Biome; Type = LevelType.LocationConnection; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome); + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier); Difficulty = locationConnection.Difficulty; float sizeFactor = MathUtils.InverseLerp( @@ -168,7 +168,7 @@ namespace Barotrauma Seed = location.BaseName; Biome = location.Biome; Type = LevelType.Outpost; - GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome); + GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier); Difficulty = 0.0f; var rand = new MTRandom(ToolBox.StringToInt(Seed)); @@ -183,7 +183,7 @@ namespace Barotrauma { if (string.IsNullOrEmpty(seed)) { - seed = Rand.Range(0, int.MaxValue, Rand.RandSync.Server).ToString(); + seed = Rand.Range(0, int.MaxValue, Rand.RandSync.ServerAndClient).ToString(); } Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); @@ -194,18 +194,18 @@ namespace Barotrauma if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } var biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? - LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server); + 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.Server), - Rand.Range(0.0f, 1.0f, Rand.RandSync.Server), + difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient), + Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient), generationParams, biome); if (type == LevelType.LocationConnection) { - float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); + float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient); levelData.HasBeaconStation = beaconRng < 0.5f; levelData.IsBeaconActive = beaconRng > 0.25f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 4aec34f52..3ee01232f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -1,68 +1,20 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class Biome + class LevelGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { - public readonly string Identifier; - public readonly string OldIdentifier; - public readonly string DisplayName; - public readonly string Description; + public readonly static PrefabCollection LevelParams = new PrefabCollection(); - public readonly bool IsEndBiome; + public string Name => Identifier.Value; - public readonly List AllowedZones = new List(); - - public Biome(string name, string description) - { - Identifier = name; - Description = description; - } - - public Biome(XElement element) - { - Identifier = element.GetAttributeString("identifier", ""); - OldIdentifier = element.GetAttributeString("oldidentifier", null); - if (string.IsNullOrEmpty(Identifier)) - { - Identifier = element.GetAttributeString("name", ""); - DebugConsole.ThrowError("Error in biome \"" + Identifier + "\": identifier missing, using name as the identifier."); - } - - DisplayName = - TextManager.Get("biomename." + Identifier, returnNull: true) ?? - element.GetAttributeString("name", "Biome") ?? - TextManager.Get("biomename." + Identifier); - - Description = - TextManager.Get("biomedescription." + Identifier, returnNull: true) ?? - element.GetAttributeString("description", "") ?? - TextManager.Get("biomedescription." + Identifier); - - IsEndBiome = element.GetAttributeBool("endbiome", false); - - AllowedZones.AddRange(element.GetAttributeIntArray("AllowedZones", new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })); - } - } - - class LevelGenerationParams : ISerializableEntity - { - public static List LevelParams { get; private set; } - - private static List biomes; - - public string Name - { - get { return Identifier; } - } - - public readonly string Identifier; - - public readonly string OldIdentifier; + public Identifier OldIdentifier { get; } private int minWidth, maxWidth, height; @@ -100,48 +52,44 @@ namespace Barotrauma private int initialDepthMin, initialDepthMax; //which biomes can this type of level appear in - private readonly List allowedBiomes = new List(); + public readonly ImmutableHashSet AllowedBiomeIdentifiers; + public readonly bool AnyBiomeAllowed; - public IEnumerable AllowedBiomes - { - get { return allowedBiomes; } - } - - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; set; } - [Serialize(LevelData.LevelType.LocationConnection, true), Editable] + [Serialize(LevelData.LevelType.LocationConnection, IsPropertySaveable.Yes), Editable] public LevelData.LevelType Type { get; set; } - [Serialize("27,30,36", true), Editable] + [Serialize("27,30,36", IsPropertySaveable.Yes), Editable] public Color AmbientLightColor { get; set; } - [Serialize("20,40,50", true), Editable()] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable()] public Color BackgroundTextureColor { get; set; } - [Serialize("20,40,50", true), Editable] + [Serialize("20,40,50", IsPropertySaveable.Yes), Editable] public Color BackgroundColor { get; set; } - [Serialize("255,255,255", true), Editable] + [Serialize("255,255,255", IsPropertySaveable.Yes), Editable] public Color WallColor { get; @@ -149,7 +97,7 @@ namespace Barotrauma } private Vector2 startPosition; - [Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] + [Serialize("0,0", IsPropertySaveable.Yes, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition { get { return startPosition; } @@ -162,7 +110,7 @@ namespace Barotrauma } private Vector2 endPosition; - [Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] + [Serialize("1,0", IsPropertySaveable.Yes, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 EndPosition { get { return endPosition; } @@ -174,70 +122,70 @@ namespace Barotrauma } } - [Serialize(true, true, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] + [Serialize(true, IsPropertySaveable.Yes, "Should there be a hole in the wall next to the end outpost (can be used to prevent players from having to backtrack if they approach the outpost from the wrong side of the main path's walls)."), Editable] public bool CreateHoleNextToEnd { get; set; } - [Serialize(true, true, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable] + [Serialize(true, IsPropertySaveable.Yes, "Should the generator force a hole to the bottom of the level to ensure there's a way to the abyss."), Editable] public bool CreateHoleToAbyss { get; set; } - [Serialize(1000, true, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] + [Serialize(1000, IsPropertySaveable.Yes, description: "The total number of level objects (vegetation, vents, etc) in the level."), Editable(MinValueInt = 0, MaxValueInt = 100000)] public int LevelObjectAmount { get; set; } - [Serialize(80, true, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] + [Serialize(80, IsPropertySaveable.Yes, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] public int BackgroundCreatureAmount { get; set; } - [Serialize(100000, true), Editable] + [Serialize(100000, IsPropertySaveable.Yes), Editable] public int MinWidth { get { return minWidth; } set { minWidth = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(100000, true), Editable] + [Serialize(100000, IsPropertySaveable.Yes), Editable] public int MaxWidth { get { return maxWidth; } set { maxWidth = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(50000, true), Editable] + [Serialize(50000, IsPropertySaveable.Yes), Editable] public int Height { get { return height; } set { height = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMin { get { return initialDepthMin; } set { initialDepthMin = Math.Max(value, 0); } } - [Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(80000, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int InitialDepthMax { get { return initialDepthMax; } set { initialDepthMax = Math.Max(value, initialDepthMin); } } - [Serialize(6500, true), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] + [Serialize(6500, IsPropertySaveable.Yes), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] public int MinTunnelRadius { get; @@ -245,7 +193,7 @@ namespace Barotrauma } - [Serialize("0,1", true), Editable] + [Serialize("0,1", IsPropertySaveable.Yes), Editable] public Point SideTunnelCount { get; @@ -253,21 +201,21 @@ namespace Barotrauma } - [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float SideTunnelVariance { get; set; } - [Serialize("2000,6000", true), Editable] + [Serialize("2000,6000", IsPropertySaveable.Yes), Editable] public Point MinSideTunnelRadius { get; set; } - [Editable, Serialize("3000, 3000", true, description: "How far from each other voronoi sites are placed. " + + [Editable, Serialize("3000, 3000", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " + "Sites determine shape of the voronoi graph which the level walls are generated from. " + "(Decreasing this value causes the number of sites, and the complexity of the level, to increase exponentially - be careful when adjusting)")] public Point VoronoiSiteInterval @@ -280,7 +228,7 @@ namespace Barotrauma } } - [Editable, Serialize("700,700", true, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " + + [Editable, Serialize("700,700", IsPropertySaveable.Yes, description: "How much random variation to apply to the positions of the voronoi sites on each axis. " + "Small values produce roughly rectangular level walls. The larger the values are, the less uniform the shapes get.")] public Point VoronoiSiteVariance { @@ -293,7 +241,7 @@ namespace Barotrauma } } - [Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. " + [Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, IsPropertySaveable.Yes, description: "The edges of the individual wall cells are subdivided into edges of this size. " + "Can be used in conjunction with the rounding values to make the cells rounder. Smaller values will make the cells look smoother, " + "but make the level more performance-intensive as the number of polygons used in rendering and physics calculations increases.")] public int CellSubdivisionLength @@ -306,7 +254,7 @@ namespace Barotrauma } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, true, description: "How much the individual wall cells are rounded. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.5f, IsPropertySaveable.Yes, description: "How much the individual wall cells are rounded. " + "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] public float CellRoundingAmount { @@ -317,7 +265,7 @@ namespace Barotrauma } } - [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, true, description: "How much random variance is applied to the edges of the cells. " + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), Serialize(0.1f, IsPropertySaveable.Yes, description: "How much random variance is applied to the edges of the cells. " + "Note that the final shape of the cells is also affected by the CellSubdivisionLength parameter.")] public float CellIrregularity { @@ -330,7 +278,7 @@ namespace Barotrauma [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), - Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] + Serialize("5000, 10000", IsPropertySaveable.Yes, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] public Point MainPathNodeIntervalRange { get { return mainPathNodeIntervalRange; } @@ -341,42 +289,42 @@ namespace Barotrauma } } - [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float MainPathVariance { get; set; } - [Editable, Serialize(5, true, description: "The number of caves placed along the main path.")] + [Editable, Serialize(5, IsPropertySaveable.Yes, description: "The number of caves placed along the main path.")] public int CaveCount { get { return caveCount; } set { caveCount = MathHelper.Clamp(value, 0, 100); } } - [Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)] + [Serialize(100, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 10000)] public int ItemCount { get; set; } - [Serialize("19200,38400", true, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)] + [Serialize("19200,38400", IsPropertySaveable.Yes, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)] public Point ResourceIntervalRange { get; set; } - [Serialize("9600,19200", true, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)] + [Serialize("9600,19200", IsPropertySaveable.Yes, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)] public Point CaveResourceIntervalRange { get; set; } - [Serialize("2,8", true, description: "The minimum and maximum amount of resources in a single cluster. " + + [Serialize("2,8", IsPropertySaveable.Yes, description: "The minimum and maximum amount of resources in a single cluster. " + "In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)] public Point ResourceClusterSizeRange { @@ -384,76 +332,76 @@ namespace Barotrauma set; } - [Serialize(0.3f, true, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.3f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float ResourceSpawnChance { get; set; } - [Serialize(1.0f, true, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float CaveResourceSpawnChance { get; set; } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] public int FloatingIceChunkCount { get; set; } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 100)] + [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 100)] public int IslandCount { get; set; } - [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] public int IceSpireCount { get; set; } - [Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes), Editable(MinValueInt = 0, MaxValueInt = 20)] public int AbyssIslandCount { get; set; } - [Serialize("4000,7000", true), Editable] + [Serialize("4000,7000", IsPropertySaveable.Yes), Editable] public Point AbyssIslandSizeMin { get; set; } - [Serialize("8000,10000", true), Editable] + [Serialize("8000,10000", IsPropertySaveable.Yes), Editable] public Point AbyssIslandSizeMax { get; set; } - [Serialize(0.5f, true), Editable()] + [Serialize(0.5f, IsPropertySaveable.Yes), Editable()] public float AbyssIslandCaveProbability { get; set; } - [Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] + [Serialize(-300000, IsPropertySaveable.Yes, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] public int SeaFloorDepth { get { return seaFloorBaseDepth; } set { seaFloorBaseDepth = MathHelper.Clamp(value, Level.MaxEntityDepth, 0); } } - [Serialize(1000, true, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)] + [Serialize(1000, IsPropertySaveable.Yes, description: "Variance of the depth of the sea floor. Smaller values produce a smoother sea floor."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100000.0f)] public int SeaFloorVariance { get { return seaFloorVariance; } set { seaFloorVariance = value; } } - [Serialize(0, true, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "The minimum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MountainCountMin { get { return mountainCountMin; } @@ -463,7 +411,7 @@ namespace Barotrauma } } - [Serialize(0, true, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(0, IsPropertySaveable.Yes, description: "The maximum number of mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MountainCountMax { get { return mountainCountMax; } @@ -473,7 +421,7 @@ namespace Barotrauma } } - [Serialize(1000, true, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(1000, IsPropertySaveable.Yes, description: "The minimum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int MountainHeightMin { get { return mountainHeightMin; } @@ -483,7 +431,7 @@ namespace Barotrauma } } - [Serialize(5000, true, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + [Serialize(5000, IsPropertySaveable.Yes, description: "The maximum height of the mountains on the sea floor."), Editable(MinValueInt = 0, MaxValueInt = 1000000)] public int MountainHeightMax { get { return mountainHeightMax; } @@ -493,72 +441,72 @@ namespace Barotrauma } } - [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(1, IsPropertySaveable.Yes, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int RuinCount { get; set; } - [Serialize(1, true, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MinWreckCount { get; set; } - [Serialize(1, true, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] + [Serialize(1, IsPropertySaveable.Yes, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] public int MaxWreckCount { get; set; } // TODO: Move the wreck parameters under a separate class? #region Wreck parameters - [Serialize(1, true, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(1, IsPropertySaveable.Yes, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MinCorpseCount { get; set; } - [Serialize(5, true, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + [Serialize(5, IsPropertySaveable.Yes, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MaxCorpseCount { get; set; } - [Serialize(0.0f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float ThalamusProbability { get; set; } - [Serialize(0.5f, true, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.5f, IsPropertySaveable.Yes, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckHullFloodingChance { get; set; } - [Serialize(0.1f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMinWaterPercentage { get; set; } - [Serialize(1.0f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float WreckFloodingHullMaxWaterPercentage { get; set; } #endregion - [Serialize(0.4f, true, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] + [Serialize(0.4f, IsPropertySaveable.Yes, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability { get { return bottomHoleProbability; } set { bottomHoleProbability = MathHelper.Clamp(value, 0.0f, 1.0f); } } - [Serialize(1.0f, true, description: "Scale of the water particle texture."), Editable] + [Serialize(1.0f, IsPropertySaveable.Yes, description: "Scale of the water particle texture."), Editable] public float WaterParticleScale { get { return waterParticleScale; } private set { waterParticleScale = Math.Max(value, 0.01f); } } - [Serialize(2048.0f, true, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] + [Serialize(2048.0f, IsPropertySaveable.Yes, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallTextureSize { get; private set; } - [Serialize(2048.0f, true), Editable(minValue: 10.0f, maxValue: 10000.0f)] + [Serialize(2048.0f, IsPropertySaveable.Yes), Editable(minValue: 10.0f, maxValue: 10000.0f)] public float WallEdgeTextureWidth { get; private set; } - [Serialize(120.0f, true, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] + [Serialize(120.0f, IsPropertySaveable.Yes, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] public float WallEdgeExpandOutwardsAmount { get; private set; } - [Serialize(1000.0f, true, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] + [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] public float WallEdgeExpandInwardsAmount { get; @@ -574,38 +522,31 @@ namespace Barotrauma public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } - public static IEnumerable GetBiomes() - { - return biomes; - } - - public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Biome biome = null) + public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, Identifier biome = default) { Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); - if (LevelParams == null || !LevelParams.Any()) + if (!LevelParams.Any()) { - DebugConsole.ThrowError("Level generation presets not found - using default presets"); - return new LevelGenerationParams(null); + throw new InvalidOperationException("Level generation presets not found - using default presets"); } - var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any()); - if (biome == null) + var matchingLevelParams = LevelParams.Where(lp => + lp.Type == type && + (lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) && + !lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier())); + matchingLevelParams = biome.IsEmpty + ? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome)) + : matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome)); + + if (!matchingLevelParams.Any()) { - matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome)); - } - else - { - matchingLevelParams = matchingLevelParams.FindAll(lp => lp.allowedBiomes.Contains(biome)); - } - if (matchingLevelParams.Count == 0) - { - DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{(biome?.Identifier ?? "null")}\", type: \"{type}\"!"); - if (biome != null) + DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")"); + if (!biome.IsEmpty) { //try to find params that at least have a suitable type - matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type); - if (matchingLevelParams.Count == 0) + matchingLevelParams = LevelParams.Where(lp => lp.Type == type); + if (!matchingLevelParams.Any()) { //still not found, give up and choose some params randomly matchingLevelParams = LevelParams; @@ -613,53 +554,22 @@ namespace Barotrauma } } - return matchingLevelParams[Rand.Range(0, matchingLevelParams.Count, Rand.RandSync.Server)]; + return matchingLevelParams.GetRandom(Rand.RandSync.ServerAndClient); } - private LevelGenerationParams(XElement element) + public LevelGenerationParams(ContentXElement element, LevelGenerationParametersFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { - Identifier = element == null ? "default" : - element.GetAttributeString("identifier", null) ?? element.Name.ToString(); - OldIdentifier = element?.GetAttributeString("oldidentifier", null)?.ToLowerInvariant(); - Identifier = Identifier.ToLowerInvariant(); + OldIdentifier = element.GetAttributeIdentifier("oldidentifier", Identifier.Empty); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - if (element == null) { return; } + if (element is null) { throw new ArgumentNullException($"{nameof(element)} is null"); } - string biomeStr = element.GetAttributeString("biomes", ""); - if (string.IsNullOrWhiteSpace(biomeStr) || biomeStr.Equals("any", StringComparison.OrdinalIgnoreCase)) - { - allowedBiomes = new List(biomes); - } - else - { - string[] biomeNames = biomeStr.Split(','); - for (int i = 0; i < biomeNames.Length; i++) - { - string biomeName = biomeNames[i].Trim().ToLowerInvariant(); - if (biomeName == "none") { continue; } + var allowedBiomeIdentifiers = element.GetAttributeIdentifierArray("biomes", Array.Empty()).ToHashSet(); + AnyBiomeAllowed = allowedBiomeIdentifiers.Contains("any".ToIdentifier()); + allowedBiomeIdentifiers.Remove("any".ToIdentifier()); + AllowedBiomeIdentifiers = allowedBiomeIdentifiers.ToImmutableHashSet(); - Biome matchingBiome = biomes.Find(b => - b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase) || (b.OldIdentifier?.Equals(biomeName, StringComparison.OrdinalIgnoreCase) ?? false)); - if (matchingBiome == null) - { - matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); - if (matchingBiome == null) - { - DebugConsole.ThrowError("Error in level generation parameters: biome \"" + biomeName + "\" not found."); - continue; - } - else - { - DebugConsole.NewMessage("Please use biome identifiers instead of names in level generation parameter \"" + Identifier + "\".", Color.Orange); - } - } - - allowedBiomes.Add(matchingBiome); - } - } - - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -691,87 +601,6 @@ namespace Barotrauma } } - public static void LoadPresets() - { - LevelParams = new List(); - biomes = new List(); - - var files = GameMain.Instance.GetFilesOfType(ContentType.LevelGenerationParameters); - if (!files.Any()) - { - files = new List() { new ContentFile("Content/Map/LevelGenerationParameters.xml", ContentType.LevelGenerationParameters) }; - } - - List biomeElements = new List(); - Dictionary levelParamElements = new Dictionary(); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - biomeElements.Clear(); - DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); - } - else if (biomeElements.Any() && mainElement.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) - { - DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use tags to override the biomes."); - break; - } - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - if (isOverride) - { - if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) - { - biomeElements.Clear(); - biomeElements.AddRange(element.FirstElement().Elements()); - DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); - } - else - { - string identifier = element.FirstElement().GetAttributeString("identifier", null) ?? element.GetAttributeString("name", ""); - if (levelParamElements.ContainsKey(identifier)) - { - DebugConsole.NewMessage($"Overriding the level generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow); - levelParamElements.Remove(identifier); - } - levelParamElements.Add(identifier, element.FirstElement()); - } - } - else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) - { - biomeElements.AddRange(element.Elements()); - } - else - { - string identifier = element.GetAttributeString("identifier", null) ?? element.GetAttributeString("name", ""); - if (levelParamElements.ContainsKey(identifier)) - { - DebugConsole.ThrowError($"Duplicate level generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use tags to override the generation parameters."); - continue; - } - else - { - levelParamElements.Add(identifier, element); - } - } - } - } - - foreach (XElement biomeElement in biomeElements) - { - biomes.Add(new Biome(biomeElement)); - } - - foreach (XElement levelParamElement in levelParamElements.Values) - { - LevelParams.Add(new LevelGenerationParams(levelParamElement)); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 673ce3a7b..1975dc33d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -81,7 +81,7 @@ namespace Barotrauma public string Name => Prefab?.Name ?? "LevelObject (null)"; - public Dictionary SerializableProperties { get; } = new Dictionary(); + public Dictionary SerializableProperties { get; } = new Dictionary(); public Level.Cave ParentCave; @@ -93,7 +93,7 @@ namespace Barotrauma Rotation = rotation; Health = prefab.Health; - spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1; + spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.ServerAndClient) : -1; if (Sprite != null && prefab.SpriteSpecificPhysicsBodyElements.ContainsKey(Sprite)) { @@ -115,7 +115,7 @@ namespace Barotrauma Physics.CollisionWall | Physics.CollisionCharacter; } - foreach (XElement triggerElement in prefab.LevelTriggerElements) + foreach (var triggerElement in prefab.LevelTriggerElements) { Triggers ??= new List(); Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale; @@ -147,7 +147,7 @@ namespace Barotrauma if (overrideProperties == null) { continue; } if (overrideProperties.Sprites.Count > 0) { - spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.Server); + spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.ServerAndClient); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index dfd89203d..9bb8b1561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -7,7 +7,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Voronoi2; using Barotrauma.Extensions; @@ -140,7 +139,7 @@ namespace Barotrauma new GraphEdge(level.EndPosition - Vector2.UnitX, level.EndPosition + Vector2.UnitX), -Vector2.UnitY, LevelObjectPrefab.SpawnPosType.LevelEnd, Alignment.Top)); - var availablePrefabs = new List(LevelObjectPrefab.List); + var availablePrefabs =LevelObjectPrefab.Prefabs.OrderBy(p => p.UintIdentifier).ToList(); objects = new List(); updateableObjects = new List(); @@ -167,7 +166,7 @@ namespace Barotrauma suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } - SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level); if (prefab.MaxCount < amount) @@ -181,7 +180,8 @@ namespace Barotrauma foreach (Level.Cave cave in level.Caves) { - availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall))); + availablePrefabs = LevelObjectPrefab.Prefabs.Where(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall)) + .OrderBy(p => p.UintIdentifier).ToList(); availableSpawnPositions.Clear(); suitableSpawnPositions.Clear(); spawnPositionWeights.Clear(); @@ -210,7 +210,7 @@ namespace Barotrauma spawnPositionWeights.Add(prefab, suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } - SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level, cave); if (prefab.MaxCount < amount) @@ -228,7 +228,8 @@ namespace Barotrauma { Rand.SetSyncedSeed(ToolBox.StringToInt(level.Seed)); - var availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall))); + var availablePrefabs = LevelObjectPrefab.Prefabs.Where(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall)) + .OrderBy(p => p.UintIdentifier).ToList(); Dictionary> suitableSpawnPositions = new Dictionary>(); Dictionary> spawnPositionWeights = new Dictionary>(); @@ -258,7 +259,7 @@ namespace Barotrauma spawnPositionWeights.Add(prefab, suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); } - SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.ServerAndClient); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } PlaceObject(prefab, spawnPosition, level); if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount) @@ -275,49 +276,49 @@ namespace Barotrauma { rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X)); } - rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server); + rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.ServerAndClient); Vector2 position = Vector2.Zero; Vector2 edgeDir = Vector2.UnitX; if (spawnPosition == null) { position = new Vector2( - Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server), - Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server)); + Rand.Range(0.0f, level.Size.X, Rand.RandSync.ServerAndClient), + Rand.Range(0.0f, level.Size.Y, Rand.RandSync.ServerAndClient)); } else { edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length; - position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server); + position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.ServerAndClient); } if (!MathUtils.NearlyEqual(prefab.RandomOffset.X, 0.0f) || !MathUtils.NearlyEqual(prefab.RandomOffset.Y, 0.0f)) { - Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.Server); - position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.Server); + Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.ServerAndClient); + position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.ServerAndClient); } var newObject = new LevelObject(prefab, - new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation); + new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.ServerAndClient)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.ServerAndClient), rotation); AddObject(newObject, level); newObject.ParentCave = parentCave; foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) { - int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.Server); + int childCount = Rand.Range(child.MinCount, child.MaxCount + 1, Rand.RandSync.ServerAndClient); for (int j = 0; j < childCount; j++) { - var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name)); + var matchingPrefabs = LevelObjectPrefab.Prefabs.Where(p => child.AllowedNames.Contains(p.Name)); int prefabCount = matchingPrefabs.Count(); - var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server)); + var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.ServerAndClient)); if (childPrefab == null) { continue; } - Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth; + Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient) * prefab.MinSurfaceWidth; var childObject = new LevelObject(childPrefab, - new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)), - Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server), - rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server)); + new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.ServerAndClient)), + Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.ServerAndClient), + rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.ServerAndClient)); AddObject(childObject, level); childObject.ParentCave = parentCave; @@ -577,7 +578,7 @@ namespace Barotrauma if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server); + availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.ServerAndClient); } private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) @@ -585,7 +586,7 @@ namespace Barotrauma if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.Server); + availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.ServerAndClient); } public override void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index b9b83f5d7..9917943c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -1,14 +1,15 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - partial class LevelObjectPrefab : ISerializableEntity + partial class LevelObjectPrefab : PrefabWithUintIdentifier, ISerializableEntity { - public static List List { get; } = new List(); + public readonly static PrefabCollection Prefabs = new PrefabCollection(); public class ChildObject { @@ -24,7 +25,7 @@ namespace Barotrauma public ChildObject(XElement element) { - AllowedNames = element.GetAttributeStringArray("names", new string[0]).ToList(); + AllowedNames = element.GetAttributeStringArray("names", Array.Empty()).ToList(); MinCount = element.GetAttributeInt("mincount", 1); MaxCount = Math.Max(element.GetAttributeInt("maxcount", 1), MinCount); } @@ -58,13 +59,13 @@ namespace Barotrauma private set; } - [Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public float MinSize { get; private set; } - [Serialize(1.0f, false), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] + [Serialize(1.0f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public float MaxSize { get; @@ -74,14 +75,14 @@ namespace Barotrauma /// /// Which sides of a wall the object can appear on. /// - [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), true, description: "Which sides of a wall the object can spawn on."), Editable] + [Serialize((Alignment.Top | Alignment.Bottom | Alignment.Left | Alignment.Right), IsPropertySaveable.Yes, description: "Which sides of a wall the object can spawn on."), Editable] public Alignment Alignment { get; private set; } - [Serialize(SpawnPosType.Wall, false), Editable()] + [Serialize(SpawnPosType.Wall, IsPropertySaveable.No), Editable()] public SpawnPosType SpawnPos { get; @@ -94,13 +95,13 @@ namespace Barotrauma private set; } - public readonly List LevelTriggerElements; + public readonly List LevelTriggerElements; /// /// Overrides the commonness of the object in a specific level type. /// Key = name of the level type, value = commonness in that level type. /// - public Dictionary OverrideCommonness; + public readonly Dictionary OverrideCommonness; public XElement PhysicsBodyElement { @@ -120,14 +121,14 @@ namespace Barotrauma } = new Dictionary(); - [Serialize(10000, false, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] + [Serialize(10000, IsPropertySaveable.No, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] public int MaxCount { get; private set; } - [Serialize("0.0,1.0", true), Editable] + [Serialize("0.0,1.0", IsPropertySaveable.Yes), Editable] public Vector2 DepthRange { get; @@ -135,7 +136,7 @@ namespace Barotrauma } [Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f), - Serialize(0.0f, true, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")] + Serialize(0.0f, IsPropertySaveable.Yes, description: "The tendency for the prefab to form clusters. Used as an exponent for perlin noise values that are used to determine the probability for an object to spawn at a specific position.")] /// /// The tendency for the prefab to form clusters. Used as an exponent for perlin noise values /// that are used to determine the probability for an object to spawn at a specific position. @@ -147,7 +148,7 @@ namespace Barotrauma } [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f), - Serialize(0.0f, true, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " + + Serialize(0.0f, IsPropertySaveable.Yes, description: "A value between 0-1 that determines the z-coordinate to sample perlin noise from when determining the probability " + " for an object to spawn at a specific position. Using the same (or close) value for different objects means the objects tend " + "to form clusters in the same areas.")] /// @@ -162,35 +163,35 @@ namespace Barotrauma private set; } - [Editable, Serialize("0,0", true, description: "Random offset from the surface the object spawns on.")] + [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "Random offset from the surface the object spawns on.")] public Vector2 RandomOffset { get; private set; } - [Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")] + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the object be rotated to align it with the wall surface it spawns on.")] public bool AlignWithSurface { get; private set; } - [Editable, Serialize(true, true, description: "Can the object be placed near the start of the level.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the start of the level.")] public bool AllowAtStart { get; private set; } - [Editable, Serialize(true, true, description: "Can the object be placed near the end of the level.")] + [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the object be placed near the end of the level.")] public bool AllowAtEnd { get; private set; } - [Serialize(0.0f, true, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum length of a graph edge the object can spawn on."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] /// /// Minimum length of a graph edge the object can spawn on. /// @@ -201,7 +202,7 @@ namespace Barotrauma } private Vector2 randomRotation; - [Editable, Serialize("0.0,0.0", true, description: "How much the rotation of the object can vary (min and max values in degrees).")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the rotation of the object can vary (min and max values in degrees).")] public Vector2 RandomRotation { get { return new Vector2(MathHelper.ToDegrees(randomRotation.X), MathHelper.ToDegrees(randomRotation.Y)); } @@ -214,7 +215,7 @@ namespace Barotrauma public Vector2 RandomRotationRad => randomRotation; private float swingAmount; - [Serialize(0.0f, true, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object swings (in degrees)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 360.0f)] public float SwingAmount { get { return MathHelper.ToDegrees(swingAmount); } @@ -226,28 +227,28 @@ namespace Barotrauma public float SwingAmountRad => swingAmount; - [Serialize(0.0f, true, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object swings."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SwingFrequency { get; private set; } - [Editable, Serialize("0.0,0.0", true, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")] + [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "How much the scale of the object oscillates on each axis. A value of 0.5,0.5 would make the object's scale oscillate from 100% to 150%.")] public Vector2 ScaleOscillation { get; private set; } - [Serialize(0.0f, true, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the object's scale oscillates."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ScaleOscillationFrequency { get; private set; } - [Editable, Serialize(1.0f, true, description: "How likely it is for the object to spawn in a level. " + + [Editable, Serialize(1.0f, IsPropertySaveable.Yes, description: "How likely it is for the object to spawn in a level. " + "This is relative to the commonness of the other objects - for example, having an object with " + "a commonness of 1 and another with a commonness of 10 would mean the latter appears in levels 10 times as frequently as the former. " + "The commonness value can be overridden on specific level types.")] @@ -257,45 +258,35 @@ namespace Barotrauma private set; } - [Serialize(0.0f, true, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "How much the object disrupts submarine's sonar."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SonarDisruption { get; private set; } - [Serialize(false, true, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable] + [Serialize(false, IsPropertySaveable.Yes, description: "Can the object take damage from weapons/attacks that damage level walls."), Editable] public bool TakeLevelWallDamage { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool HideWhenBroken { get; private set; } - [Serialize(100.0f, true), Editable] + [Serialize(100.0f, IsPropertySaveable.Yes), Editable] public float Health { get; private set; } - public string Identifier - { - get; - set; - } - - - public string Name - { - get { return Identifier; } - } + public string Name => Identifier.Value; public List ChildObjects { @@ -303,7 +294,7 @@ namespace Barotrauma private set; } - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; } @@ -323,91 +314,20 @@ namespace Barotrauma return "LevelObjectPrefab (" + Identifier + ")"; } - public static void LoadAll() - { - List.Clear(); - var files = GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs); - if (files.Count() > 0) - { - foreach (var file in files) - { - LoadConfig(file.Path); - } - } - else - { - LoadConfig("Content/LevelObjects/LevelObject/Prefabs.xml"); - } - } - - private static void LoadConfig(string configPath) - { - try - { - XDocument doc = XMLExtensions.TryLoadXml(configPath); - if (doc == null) { return; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - DebugConsole.NewMessage($"Overriding all level object prefabs with '{configPath}'", Color.Yellow); - List.Clear(); - } - else if (List.Any()) - { - DebugConsole.Log($"Loading additional level object prefabs from file '{configPath}'"); - } - foreach (XElement subElement in mainElement.Elements()) - { - var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; - string identifier = element.GetAttributeString("identifier", ""); - var existingPrefab = List.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); - if (existingPrefab != null) - { - if (subElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing level object prefab '{identifier}' using the file '{configPath}'", Color.Yellow); - List.Remove(existingPrefab); - } - else - { - DebugConsole.ThrowError($"Error in '{configPath}': Duplicate level object prefab '{identifier}' found in '{configPath}'! Each level object prefab must have a unique identifier. " + - "Use tags to override prefabs."); - continue; - } - } - List.Add(new LevelObjectPrefab(element)); - } - } - catch (Exception e) - { - DebugConsole.ThrowError(string.Format("Failed to load LevelObject prefabs from {0}", configPath), e); - } - } - public LevelObjectPrefab(XElement element, string identifier = null) + public LevelObjectPrefab(ContentXElement element, LevelObjectPrefabsFile file, Identifier identifierOverride = default) : base(file, ParseIdentifier(identifierOverride, element)) { ChildObjects = new List(); - LevelTriggerElements = new List(); + LevelTriggerElements = new List(); OverrideProperties = new List(); - OverrideCommonness = new Dictionary(); + OverrideCommonness = new Dictionary(); - Identifier = null; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (element != null) { Config = element; - Identifier = element.GetAttributeString("identifier", null) ?? identifier; - if (string.IsNullOrEmpty(Identifier)) - { -#if DEBUG - DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); -#else - DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); -#endif - Identifier = element.Name.ToString(); - } - LoadElements(element, -1); + + LoadElements(file, element, -1); InitProjSpecific(element); } @@ -419,10 +339,26 @@ namespace Barotrauma } } - private void LoadElements(XElement element, int parentTriggerIndex) + public static Identifier ParseIdentifier(Identifier identifierOverride, XElement element) + { + if (!identifierOverride.IsEmpty) { return identifierOverride; } + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); + if (identifier.IsEmpty) + { +#if DEBUG + DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); +#else + DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); +#endif + identifier = element.NameAsIdentifier(); + } + return identifier; + } + + private void LoadElements(LevelObjectPrefabsFile file, ContentXElement element, int parentTriggerIndex) { int propertyOverrideCount = 0; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -430,8 +366,8 @@ namespace Barotrauma var newSprite = new Sprite(subElement, lazyLoad: true); Sprites.Add(newSprite); var spriteSpecificPhysicsBodyElement = - subElement.Element("PhysicsBody") ?? subElement.Element("Body") ?? - subElement.Element("physicsbody") ?? subElement.Element("body"); + subElement.GetChildElement("PhysicsBody") ?? subElement.GetChildElement("Body") ?? + subElement.GetChildElement("physicsbody") ?? subElement.GetChildElement("body"); if (spriteSpecificPhysicsBodyElement != null) { SpriteSpecificPhysicsBodyElements.Add(newSprite, spriteSpecificPhysicsBodyElement); @@ -441,7 +377,7 @@ namespace Barotrauma DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + Identifier levelType = subElement.GetAttributeIdentifier("leveltype", Identifier.Empty); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -451,13 +387,13 @@ namespace Barotrauma case "trigger": OverrideProperties.Add(null); LevelTriggerElements.Add(subElement); - LoadElements(subElement, LevelTriggerElements.Count - 1); + LoadElements(file, subElement, LevelTriggerElements.Count - 1); break; case "childobject": ChildObjects.Add(new ChildObject(subElement)); break; case "overrideproperties": - var propertyOverride = new LevelObjectPrefab(subElement, identifier: Identifier + "-" + propertyOverrideCount); + var propertyOverride = new LevelObjectPrefab(subElement, file, identifierOverride: $"{Identifier}-{propertyOverrideCount}".ToIdentifier()); OverrideProperties[OverrideProperties.Count - 1] = propertyOverride; if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null) { @@ -475,12 +411,13 @@ namespace Barotrauma } } - partial void InitProjSpecific(XElement element); + partial void InitProjSpecific(ContentXElement element); public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true) { - if (generationParams?.Identifier != null && + if (generationParams != null && + generationParams.Identifier != Identifier.Empty && OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) { return commonness; @@ -490,13 +427,16 @@ namespace Barotrauma public float GetCommonness(LevelGenerationParams generationParams) { - if (generationParams?.Identifier != null && + if (generationParams != null && + generationParams.Identifier != Identifier.Empty && (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || - (generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + (!generationParams.OldIdentifier.IsEmpty && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) { return commonness; } return Commonness; } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 3ba23fb92..ec1d9fbe4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -184,7 +184,7 @@ namespace Barotrauma set; } - public string InfectIdentifier + public Identifier InfectIdentifier { get; set; @@ -199,7 +199,7 @@ namespace Barotrauma private bool triggeredOnce; private readonly bool triggerOnce; - public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") + public LevelTrigger(ContentXElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") { TriggererPosition = new Dictionary(); @@ -223,7 +223,7 @@ namespace Barotrauma cameraShake = element.GetAttributeFloat("camerashake", 0.0f); - InfectIdentifier = element.GetAttributeString("infectidentifier", null); + InfectIdentifier = element.GetAttributeIdentifier("infectidentifier", Identifier.Empty); InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f); triggerOnce = element.GetAttributeBool("triggeronce", false); @@ -264,7 +264,7 @@ namespace Barotrauma TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); - var tagsArray = element.GetAttributeStringArray("tags", new string[0]); + var tagsArray = element.GetAttributeStringArray("tags", Array.Empty()); foreach (string tag in tagsArray) { tags.Add(tag.ToLowerInvariant()); @@ -272,7 +272,7 @@ namespace Barotrauma if (triggeredBy.HasFlag(TriggererType.OtherTrigger)) { - var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", new string[0]); + var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", Array.Empty()); foreach (string tag in otherTagsArray) { allowedOtherTriggerTags.Add(tag.ToLowerInvariant()); @@ -280,7 +280,7 @@ namespace Barotrauma } string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}"; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -317,12 +317,12 @@ namespace Barotrauma -sa * unrotatedForce.X + ca * unrotatedForce.Y); } - public static void LoadStatusEffect(List statusEffects, XElement element, string parentDebugName) + public static void LoadStatusEffect(List statusEffects, ContentXElement element, string parentDebugName) { statusEffects.Add(StatusEffect.Load(element, parentDebugName)); } - public static void LoadAttack(XElement element, string parentDebugName, bool triggerOnce, List attacks) + public static void LoadAttack(ContentXElement element, string parentDebugName, bool triggerOnce, List attacks) { var attack = new Attack(element, parentDebugName); if (!triggerOnce) @@ -574,7 +574,7 @@ namespace Barotrauma else if (triggerer is Submarine submarine) { ApplyAttacks(attacks, worldPosition, deltaTime); - if (!string.IsNullOrWhiteSpace(InfectIdentifier)) + if (!InfectIdentifier.IsEmpty) { submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 64dc5acb9..89c509288 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; +using Barotrauma.Extensions; #if DEBUG using System.Xml; #else @@ -20,93 +22,34 @@ namespace Barotrauma.RuinGeneration class RuinGenerationParams : OutpostGenerationParams { - public static List RuinParams - { - get - { - if (paramsList == null) - { - LoadAll(); - } - return paramsList; - } - } + public readonly static PrefabCollection RuinParams = + new PrefabCollection(); - private static List paramsList; + public override string Name => "RuinGenerationParams"; - private readonly string filePath; - - private RuinGenerationParams(XElement element, string filePath) : base(element, filePath) - { - this.filePath = filePath; - } - - public static RuinGenerationParams GetRandom(Rand.RandSync randSync = Rand.RandSync.Server) - { - if (paramsList == null) { LoadAll(); } - - if (paramsList.Count == 0) - { - DebugConsole.ThrowError("No ruin configuration files found in any content package."); - return new RuinGenerationParams(null, null); - } - - return paramsList[Rand.Int(paramsList.Count, randSync)]; - } - - private static void LoadAll() - { - paramsList = new List(); - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc?.Root == null) { continue; } - - foreach (XElement subElement in doc.Root.Elements()) - { - var mainElement = subElement; - if (subElement.IsOverride()) - { - mainElement = subElement.FirstElement(); - paramsList.Clear(); - DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow); - } - else if (paramsList.Any()) - { - DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'"); - } - var newParams = new RuinGenerationParams(mainElement, configFile.Path); - paramsList.Add(newParams); - } - } - } - - public static void ClearAll() - { - paramsList?.Clear(); - paramsList = null; - } + public RuinGenerationParams(ContentXElement element, RuinConfigFile file) : base(element, file) { } public static void SaveAll() { + #warning TODO: revise System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true }; - + foreach (RuinGenerationParams generationParams in RuinParams) { - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) + foreach (RuinConfigFile configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { - if (configFile.Path != generationParams.filePath) { continue; } + if (configFile.Path != generationParams.ContentFile.Path) { continue; } XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } SerializableProperty.SerializeProperties(generationParams, doc.Root); - using (var writer = XmlWriter.Create(configFile.Path, settings)) + using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -114,5 +57,7 @@ namespace Barotrauma.RuinGeneration } } } + + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index ef4e0c1cb..72504a631 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -67,7 +67,7 @@ namespace Barotrauma.RuinGeneration if (interestingPosCount == 0) { //make sure there's at least one PositionsOfInterest in the ruins - level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); + level.PositionsOfInterest.Add(new Level.InterestingPosition(waypoints.GetRandom(Rand.RandSync.ServerAndClient).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 7c26aea92..0ace84cde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.IO; +using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { @@ -21,10 +23,26 @@ namespace Barotrauma } public readonly SubmarineInfo subInfo; - - public LinkedSubmarinePrefab(SubmarineInfo subInfo) + + public override Sprite Sprite => null; + + public override string OriginalName => Name.Value; + + public override LocalizedString Name => subInfo.Name; + + public override ImmutableHashSet Tags => null; + + public override ImmutableHashSet AllowedLinks => null; + + public override MapEntityCategory Category => MapEntityCategory.Misc; + + public override ImmutableHashSet Aliases { get; } + + public LinkedSubmarinePrefab(SubmarineInfo subInfo) : base(subInfo.Name.ToIdentifier()) { this.subInfo = subInfo; + + Aliases = Name.Value.ToEnumerable().ToImmutableHashSet(); } protected override void CreateInstance(Rectangle rect) @@ -71,6 +89,8 @@ namespace Barotrauma return true; } } + + public int CargoCapacity { get; private set; } public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID) : base(null, submarine, id) @@ -110,6 +130,7 @@ namespace Barotrauma { LinkedSubmarine sl = new LinkedSubmarine(mainSub, id); sl.GenerateWallVertices(element); + sl.CargoCapacity = element.GetAttributeInt("cargocapacity", 0); if (sl.wallVertices.Any()) { sl.Rect = new Rectangle( @@ -152,7 +173,7 @@ namespace Barotrauma if (element.Name != "Structure") { continue; } string name = element.GetAttributeString("name", ""); - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = Structure.FindPrefab(name, identifier); if (prefab == null) { continue; } @@ -173,7 +194,7 @@ namespace Barotrauma } // LinkedSubmarine.Load() is called from MapEntity.LoadAll() - public static LinkedSubmarine Load(XElement element, Submarine submarine, IdRemap idRemap) + public static LinkedSubmarine Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); LinkedSubmarine linkedSub; @@ -206,14 +227,16 @@ namespace Barotrauma } } - linkedSub.filePath = element.GetAttributeString("filepath", ""); - int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]); + #warning TODO: revise + linkedSub.filePath = element.GetAttributeContentPath("filepath")?.Value ?? string.Empty; + int[] linkedToIds = element.GetAttributeIntArray("linkedto", Array.Empty()); for (int i = 0; i < linkedToIds.Length; i++) { linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i])); } linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0)); linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0); + linkedSub.CargoCapacity = element.GetAttributeInt("cargocapacity", 0); return linkedSub.loadSub ? linkedSub : null; } @@ -269,7 +292,7 @@ namespace Barotrauma DockingPort linkedPort = null; DockingPort myPort = null; - MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent() != null); + MapEntity linkedItem = linkedTo.FirstOrDefault(lt => (lt as Item)?.GetComponent() != null); if (linkedItem == null) { linkedPort = DockingPort.List.FirstOrDefault(dp => dp.DockingTarget != null && dp.DockingTarget.Item.Submarine == sub); @@ -349,7 +372,7 @@ namespace Barotrauma wall.SetDamage(i, 0, createNetworkEvent: false); } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != sub) { continue; } hull.WaterVolume = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index d3275a394..a6884d570 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -16,10 +16,10 @@ namespace Barotrauma { public readonly ushort OriginalID; public readonly ushort ModuleIndex; - public readonly string Identifier; + public readonly Identifier Identifier; public readonly int OriginalContainerIndex; - public TakenItem(string identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex) + public TakenItem(Identifier identifier, UInt16 originalID, int originalContainerIndex, ushort moduleIndex) { OriginalID = originalID; OriginalContainerIndex = originalContainerIndex; @@ -34,7 +34,7 @@ namespace Barotrauma OriginalContainerIndex = item.OriginalContainerIndex; OriginalID = item.ID; ModuleIndex = (ushort) item.OriginalModuleIndex; - Identifier = item.prefab.Identifier; + Identifier = ((MapEntity)item).Prefab.Identifier; } public bool IsEqual(TakenItem obj) @@ -46,11 +46,11 @@ namespace Barotrauma { if (item.OriginalContainerIndex != Entity.NullEntityID) { - return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier; } else { - return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && ((MapEntity)item).Prefab.Identifier == Identifier; } } } @@ -252,7 +252,7 @@ namespace Barotrauma return $"Location ({Name ?? "null"})"; } - public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable existingLocations = null) + public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable existingLocations = null) { Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost); Name = RandomName(Type, rand, existingLocations); @@ -263,22 +263,21 @@ namespace Barotrauma public Location(XElement element) { - string locationType = element.GetAttributeString("type", ""); - Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + Identifier locationType = element.GetAttributeIdentifier("type", ""); + Type = LocationType.Prefabs[locationType]; bool typeNotFound = false; if (Type == null) { //turn lairs into abandoned outposts - if (locationType.Equals("lair", StringComparison.OrdinalIgnoreCase)) + if (locationType == "lair") { - Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("Abandoned", StringComparison.OrdinalIgnoreCase)); + Type ??= LocationType.Prefabs["Abandoned"]; addInitialMissionsForType = Type; } if (Type == null) { DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead."); - Type ??= LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase)); - Type ??= LocationType.List.First(); + Type ??= LocationType.Prefabs["None"] ?? LocationType.Prefabs.First(); } if (Type != null) { @@ -287,8 +286,8 @@ namespace Barotrauma typeNotFound = true; } - string originalLocationType = element.GetAttributeString("originaltype", locationType); - OriginalType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + Identifier originalLocationType = element.GetAttributeIdentifier("originaltype", locationType); + OriginalType = LocationType.Prefabs[locationType]; baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); @@ -312,7 +311,7 @@ namespace Barotrauma LoadLocationTypeChange(element); } - string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]); + string[] takenItemStr = element.GetAttributeStringArray("takenitems", Array.Empty()); foreach (string takenItem in takenItemStr) { string[] takenItemSplit = takenItem.Split(';'); @@ -336,15 +335,15 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\""); continue; } - takenItems.Add(new TakenItem(takenItemSplit[0], id, containerIndex, moduleIndex)); + takenItems.Add(new TakenItem(takenItemSplit[0].ToIdentifier(), id, containerIndex, moduleIndex)); } - killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", new int[0]).ToHashSet(); + killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty()).ToHashSet(); System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationType}\"!"); if (Type == null) { - Type = LocationType.List.First(); + Type = LocationType.Prefabs.First(); } LevelData = new LevelData(element.Element("Level")); @@ -359,7 +358,7 @@ namespace Barotrauma { TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0); LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0); - foreach (XElement subElement in locationElement.Elements()) + foreach (var subElement in locationElement.Elements()) { switch (subElement.Name.ToString()) { @@ -377,8 +376,8 @@ namespace Barotrauma } else { - string missionIdentifier = subElement.GetAttributeString("missionidentifier", ""); - var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + Identifier missionIdentifier = subElement.GetAttributeIdentifier("missionidentifier", ""); + var mission = MissionPrefab.Prefabs[missionIdentifier]; if (mission == null) { DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found."); @@ -400,7 +399,7 @@ namespace Barotrauma { var id = childElement.GetAttributeString("prefabid", null); if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = MissionPrefab.List.Find(p => p.Identifier.Equals(id, StringComparison.OrdinalIgnoreCase)); + var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id); if (prefab == null) { continue; } var destination = childElement.GetAttributeInt("destinationindex", -1); var selected = childElement.GetAttributeBool("selected", false); @@ -410,7 +409,7 @@ namespace Barotrauma } - public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable existingLocations = null) + public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable existingLocations = null) { return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } @@ -432,11 +431,11 @@ namespace Barotrauma if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom()); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandomUnsynced()); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom()); + UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); } CreateStore(force: true); @@ -446,11 +445,11 @@ namespace Barotrauma { if (Type.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.Server)); + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom(Rand.RandSync.ServerAndClient)); } if (Type.MissionTags.Any()) { - UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.Server)); + UnlockMissionByTag(Type.MissionTags.GetRandom(Rand.RandSync.ServerAndClient)); } } @@ -474,11 +473,11 @@ namespace Barotrauma #endif } - public MissionPrefab UnlockMissionByIdentifier(string identifier) + public MissionPrefab UnlockMissionByIdentifier(Identifier identifier) { - if (AvailableMissions.Any(m => m.Prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))) { return null; } + if (AvailableMissions.Any(m => m.Prefab.Identifier == identifier)) { return null; } - var missionPrefab = MissionPrefab.List.Find(mp => mp.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + var missionPrefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == identifier); if (missionPrefab == null) { DebugConsole.ThrowError($"Failed to unlock a mission with the identifier \"{identifier}\": matching mission not found."); @@ -500,9 +499,9 @@ namespace Barotrauma return null; } - public MissionPrefab UnlockMissionByTag(string tag) + public MissionPrefab UnlockMissionByTag(Identifier tag) { - var matchingMissions = MissionPrefab.List.FindAll(mp => mp.Tags.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase))); + var matchingMissions = MissionPrefab.Prefabs.Where(mp => mp.Tags.Any(t => t == tag)); if (!matchingMissions.Any()) { DebugConsole.ThrowError($"Failed to unlock a mission with the tag \"{tag}\": no matching missions not found."); @@ -623,11 +622,11 @@ namespace Barotrauma { if (addInitialMissionsForType.MissionIdentifiers.Any()) { - UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandom()); + UnlockMissionByIdentifier(addInitialMissionsForType.MissionIdentifiers.GetRandomUnsynced()); } if (addInitialMissionsForType.MissionTags.Any()) { - UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandom()); + UnlockMissionByTag(addInitialMissionsForType.MissionTags.GetRandomUnsynced()); } addInitialMissionsForType = null; } @@ -661,7 +660,7 @@ namespace Barotrauma public LocationType GetLocationType() { - if (IsCriticallyRadiated() && LocationType.List.FirstOrDefault(lt => lt.Identifier.Equals(Type.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newLocationType) + if (IsCriticallyRadiated() && LocationType.Prefabs[Type.ReplaceInRadiation] is { } newLocationType) { return newLocationType; } @@ -757,8 +756,8 @@ namespace Barotrauma List specials = new List(); foreach (var childElement in element.GetChildElements("item")) { - var id = childElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } + var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); + if (id.IsEmpty) { continue; } var prefab = ItemPrefab.Find(null, id); if (prefab == null) { continue; } specials.Add(prefab); @@ -1045,9 +1044,9 @@ namespace Barotrauma RequestedGoods.Clear(); for (int i = 0; i < RequestedGoodsCount; i++) { - var item = ItemPrefab.Prefabs.GetRandom(p => + var item = ItemPrefab.Prefabs.GetRandom(p => p.CanBeSold && !RequestedGoods.Contains(p) && - p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial); + p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced); if (item == null) { break; } RequestedGoods.Add(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 792cce9bc..2bfa64c65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -7,23 +7,24 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Collections.Immutable; namespace Barotrauma { - class LocationType + class LocationType : PrefabWithUintIdentifier { - public static readonly List List = new List(); + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + private readonly List names; private readonly List portraits = new List(); // - private readonly List> hireableJobs; + private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; private readonly float totalHireableWeight; public Dictionary CommonnessPerZone = new Dictionary(); - public readonly string Identifier; - public readonly string Name; + public readonly LocalizedString Name; public readonly float BeaconStationChance; @@ -31,8 +32,8 @@ namespace Barotrauma public readonly List CanChangeTo = new List(); - public readonly List MissionIdentifiers = new List(); - public readonly List MissionTags = new List(); + public readonly ImmutableArray MissionIdentifiers; + public readonly ImmutableArray MissionTags; public readonly List HideEntitySubcategories = new List(); @@ -44,7 +45,15 @@ namespace Barotrauma private set; } - public List NameFormats { get; private set; } + private ImmutableArray? nameFormats = null; + public IReadOnlyList NameFormats + { + get + { + nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray(); + return nameFormats; + } + } public bool HasHireableCharacters { @@ -104,32 +113,30 @@ namespace Barotrauma return $"LocationType (" + Identifier + ")"; } - private LocationType(XElement element) + public LocationType(ContentXElement element, LocationTypesFile file) : base(file, element.GetAttributeIdentifier("identifier", element.Name.LocalName)) { - Identifier = element.GetAttributeString("identifier", element.Name.ToString()); - Name = TextManager.Get("LocationName." + Identifier, fallBackTag: "unknown"); + Name = TextManager.Get("LocationName." + Identifier, "unknown"); BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); HasOutpost = element.GetAttributeBool("hasoutpost", true); IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); - MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList(); - MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList(); + MissionIdentifiers = element.GetAttributeIdentifierArray("missionidentifiers", Array.Empty()).ToImmutableArray(); + MissionTags = element.GetAttributeIdentifierArray("missiontags", Array.Empty()).ToImmutableArray(); - HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList(); + HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", Array.Empty()).ToList(); ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), ""); string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); - string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt"); + ContentPath nameFile = element.GetAttributeContentPath("namefile") ?? ContentPath.FromRaw(null, "Content/Map/locationNames.txt"); try { - names = File.ReadAllLines(nameFile).ToList(); + names = File.ReadAllLines(nameFile.Value).ToList(); } catch (Exception e) { @@ -151,31 +158,16 @@ namespace Barotrauma CommonnessPerZone[zoneIndex] = zoneCommonness; } - hireableJobs = new List>(); - foreach (XElement subElement in element.Elements()) + var hireableJobs = new List<(Identifier, float)>(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "hireable": - string jobIdentifier = subElement.GetAttributeString("identifier", ""); - JobPrefab jobPrefab = null; - if (jobIdentifier == "") - { - DebugConsole.ThrowError("Error in location type \""+ Identifier + "\" - hireable jobs should be configured using identifiers instead of names."); - } - else - { - jobPrefab = JobPrefab.Get(jobIdentifier.ToLowerInvariant()); - } - if (jobPrefab == null) - { - DebugConsole.ThrowError("Error in in location type " + Identifier + " - could not find a job with the identifier \"" + jobIdentifier + "\"."); - continue; - } + Identifier jobIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f); totalHireableWeight += jobCommonness; - Tuple hireableJob = new Tuple(jobPrefab, jobCommonness); - hireableJobs.Add(hireableJob); + hireableJobs.Add((jobIdentifier, jobCommonness)); break; case "symbol": Sprite = new Sprite(subElement, lazyLoad: true); @@ -216,16 +208,17 @@ namespace Barotrauma break; } } + this.hireableJobs = hireableJobs.ToImmutableArray(); } public JobPrefab GetRandomHireable() { - float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.Server); + float randFloat = Rand.Range(0.0f, totalHireableWeight, Rand.RandSync.ServerAndClient); - foreach (Tuple hireable in hireableJobs) + foreach ((Identifier jobIdentifier, float commonness) in hireableJobs) { - if (randFloat < hireable.Item2) return hireable.Item1; - randFloat -= hireable.Item2; + if (randFloat < commonness) { return JobPrefab.Prefabs[jobIdentifier]; } + randFloat -= commonness; } return null; @@ -252,12 +245,13 @@ namespace Barotrauma public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false) { - Debug.Assert(List.Count > 0, "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); + Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes"); - List allowedLocationTypes = - List.FindAll(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)); + LocationType[] allowedLocationTypes = + Prefabs.Where(lt => (!zone.HasValue || lt.CommonnessPerZone.ContainsKey(zone.Value)) && (!requireOutpost || lt.HasOutpost)) + .OrderBy(p => p.UintIdentifier).ToArray(); - if (allowedLocationTypes.Count == 0) + if (allowedLocationTypes.Length == 0) { DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!"); } @@ -266,82 +260,15 @@ namespace Barotrauma { return ToolBox.SelectWeightedRandom( allowedLocationTypes, - allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToList(), + allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToArray(), rand); } else { - return allowedLocationTypes[rand.Next() % allowedLocationTypes.Count]; + return allowedLocationTypes[rand.Next() % allowedLocationTypes.Length]; } } - public static void Init() - { - List.Clear(); - var locationTypeFiles = GameMain.Instance.GetFilesOfType(ContentType.LocationTypes); - if (!locationTypeFiles.Any()) - { - DebugConsole.ThrowError("No location types configured in any of the selected content packages. Attempting to load from the vanilla content package..."); - locationTypeFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), ContentType.LocationTypes); - if (!locationTypeFiles.Any()) - { - throw new Exception("No location types configured in any of the selected content packages. Please try uninstalling mods or reinstalling the game."); - } - } - - foreach (ContentFile file in locationTypeFiles) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - DebugConsole.NewMessage($"Overriding all location types with '{file.Path}'", Color.Yellow); - List.Clear(); - } - else if (List.Any()) - { - DebugConsole.NewMessage($"Loading additional location types from file '{file.Path}'"); - } - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement; - bool allowOverriding = false; - if (sourceElement.IsOverride()) - { - element = sourceElement.FirstElement(); - allowOverriding = true; - } - string identifier = element.GetAttributeString("identifier", null); - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"Error in '{file.Path}': No identifier defined for {element.Name.ToString()}"); - continue; - } - var duplicate = List.FirstOrDefault(l => l.Identifier == identifier); - if (duplicate != null) - { - if (allowOverriding) - { - List.Remove(duplicate); - DebugConsole.NewMessage($"Overriding the location type with the identifier '{identifier}' with '{file.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{file.Path}': Duplicate identifier defined with the identifier '{identifier}'"); - continue; - } - } - LocationType locationType = new LocationType(element); - List.Add(locationType); - } - } - - foreach (EventSet eventSet in EventSet.List) - { - eventSet.CheckLocationTypeErrors(); - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 4f7c3424c..0639ceb6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -1,8 +1,10 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -21,7 +23,7 @@ namespace Barotrauma /// /// The change can only happen if there's at least one of the given types of locations near this one /// - public readonly List RequiredLocations; + public readonly ImmutableArray RequiredLocations; /// /// How close the location needs to be to one of the RequiredLocations for the change to occur @@ -55,7 +57,7 @@ namespace Barotrauma public Requirement(XElement element, LocationTypeChange change) { - RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList(); + RequiredLocations = element.GetAttributeIdentifierArray("requiredlocations", element.GetAttributeIdentifierArray("requiredadjacentlocations", Array.Empty())).ToImmutableArray(); RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); @@ -124,9 +126,9 @@ namespace Barotrauma } } - public readonly string CurrentType; + public readonly Identifier CurrentType; - public readonly string ChangeToType; + public readonly Identifier ChangeToType; /// /// Base probability per turn for the location to change if near one of the RequiredLocations @@ -137,12 +139,33 @@ namespace Barotrauma public List Requirements = new List(); - public List Messages = new List(); + private readonly bool requireChangeMessages; + private readonly string messageTag; + private ImmutableArray? messages = null; + public IReadOnlyList Messages + { + get + { + if (!messages.HasValue) + { + messages = TextManager.GetAll(messageTag).ToImmutableArray(); + if (messages.Value.None()) + { + if (requireChangeMessages) + { + DebugConsole.ThrowError($"No messages defined for the location type change {CurrentType} -> {ChangeToType}"); + } + } + } + + return messages.Value; + } + } /// /// The change can't happen if there's one or more of the given types of locations near this one /// - public readonly List DisallowedAdjacentLocations; + public readonly ImmutableArray DisallowedAdjacentLocations; /// /// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled @@ -156,14 +179,14 @@ namespace Barotrauma public readonly Point RequiredDurationRange; - public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) + public LocationTypeChange(Identifier currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) { CurrentType = currentType; - ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", "")); + ChangeToType = element.GetAttributeIdentifier("type", element.GetAttributeIdentifier("to", "")); RequireDiscovered = element.GetAttributeBool("requirediscovered", false); - DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList(); + DisallowedAdjacentLocations = element.GetAttributeIdentifierArray("disallowedadjacentlocations", Array.Empty()).ToImmutableArray(); DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1); RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero); @@ -184,19 +207,10 @@ namespace Barotrauma RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0)); } - string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType); + this.requireChangeMessages = requireChangeMessages; + messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType); - Messages = TextManager.GetAll(messageTag); - if (Messages == null) - { - if (requireChangeMessages) - { - DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); - } - Messages = new List(); - } - - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index a5929ded1..f7b648463 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -88,7 +88,7 @@ namespace Barotrauma bool lairsFound = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -112,11 +112,11 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(!Locations.Contains(null)); for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); } List connectionElements = new List(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -133,10 +133,10 @@ namespace Barotrauma Locations[locationIndices.Y].Connections.Add(connection); connection.LevelData = new LevelData(subElement.Element("Level")); string biomeId = subElement.GetAttributeString("biome", ""); - connection.Biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ?? - LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ?? - LevelGenerationParams.GetBiomes().First(); + connection.Biome = + Biome.Prefabs.FirstOrDefault(b => b.Identifier == biomeId) ?? + Biome.Prefabs.FirstOrDefault(b => !b.OldIdentifier.IsEmpty && b.OldIdentifier == biomeId) ?? + Biome.Prefabs.First(); Connections.Add(connection); connectionElements.Add(subElement); break; @@ -214,13 +214,13 @@ namespace Barotrauma for (int i = 0; i < Locations.Count; i++) { - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}", -100, 100, Rand.Range(-10, 11, Rand.RandSync.Server)); + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, Locations[i], $"location.{i}".ToIdentifier(), -100, 100, Rand.Range(-10, 11, Rand.RandSync.ServerAndClient)); } foreach (Location location in Locations) { - if (!location.Type.Identifier.Equals("city", StringComparison.OrdinalIgnoreCase) && - !location.Type.Identifier.Equals("outpost", StringComparison.OrdinalIgnoreCase)) + if (location.Type.Identifier != "city" && + location.Type.Identifier != "outpost") { continue; } @@ -252,8 +252,8 @@ namespace Barotrauma for (float y = 10.0f; y < Height - 10.0f; y += generationParams.VoronoiSiteInterval.Y) { voronoiSites.Add(new Vector2( - x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server), - y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server))); + x + generationParams.VoronoiSiteVariance.X * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient), + y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient))); } } @@ -297,12 +297,12 @@ namespace Barotrauma Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 }; - int positionIndex = Rand.Int(1, Rand.RandSync.Server); + int positionIndex = Rand.Int(1, Rand.RandSync.ServerAndClient); Vector2 position = points[positionIndex]; if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; } int zone = GetZoneIndex(position.X); - newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations); + newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient), requireOutpost: false, existingLocations: Locations); Locations.Add(newLocations[i]); } @@ -394,7 +394,7 @@ namespace Barotrauma connectionsBetweenZones[i] = new List(); } var shuffledConnections = Connections.ToList(); - shuffledConnections.Shuffle(Rand.RandSync.Server); + shuffledConnections.Shuffle(Rand.RandSync.ServerAndClient); foreach (var connection in shuffledConnections) { int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X); @@ -447,9 +447,10 @@ namespace Barotrauma Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ? Connections[i].Locations[0] : Connections[i].Locations[1]; - if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase)) + if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost && !lt.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase))); + #warning TODO: determinism? + leftMostLocation.ChangeType(LocationType.Prefabs.First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; @@ -473,10 +474,10 @@ namespace Barotrauma 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.Server), 1.2f, 100.0f); + //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.Server), 1.0f, 100.0f); + connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-random, random, Rand.RandSync.ServerAndClient), 1.0f, 100.0f); } AssignBiomes(); @@ -522,21 +523,13 @@ namespace Barotrauma { float zoneWidth = Width / generationParams.DifficultyZones; int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1; - if (zoneIndex < 1) - { - return LevelGenerationParams.GetBiomes().First(); - - } - else if (zoneIndex >= generationParams.DifficultyZones) - { - return LevelGenerationParams.GetBiomes().Last(); - } - return LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex)); + zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1); + return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex)); } private void AssignBiomes() { - var biomes = LevelGenerationParams.GetBiomes(); + var biomes = Biome.Prefabs; float zoneWidth = Width / generationParams.DifficultyZones; List allowedBiomes = new List(10); @@ -550,7 +543,7 @@ namespace Barotrauma { if (location.MapPosition.X < zoneX) { - location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.Server)]; + location.Biome = allowedBiomes[Rand.Range(0, allowedBiomes.Count, Rand.RandSync.ServerAndClient)]; } } } @@ -692,10 +685,10 @@ namespace Barotrauma if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) { - metadata.SetValue("campaign.location.id", CurrentLocationIndex); - metadata.SetValue("campaign.location.name", CurrentLocation.Name); - metadata.SetValue("campaign.location.biome", CurrentLocation.Biome?.Identifier ?? "null"); - metadata.SetValue("campaign.location.type", CurrentLocation.Type?.Identifier ?? "null"); + metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex); + metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name); + metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier()); + metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier()); } } @@ -999,7 +992,7 @@ namespace Barotrauma { string prevName = location.Name; - var newType = LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)); + var newType = LocationType.Prefabs[change.ChangeToType]; if (newType == null) { DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); @@ -1053,7 +1046,7 @@ namespace Barotrauma return; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1083,14 +1076,14 @@ namespace Barotrauma } } - string locationType = subElement.GetAttributeString("type", ""); + Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; - LocationType newLocationType = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)) ?? LocationType.List.First(); + LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First(); location.ChangeType(newLocationType); if (showNotifications && prevLocationType != location.Type) { - var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase)); + var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType == location.Type.Identifier); if (change != null) { ChangeLocationTypeProjSpecific(location, prevLocationName, change); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs index 3c47acbd5..38ee2f1dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs @@ -1,30 +1,31 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { - class MapGenerationParams : ISerializableEntity + class MapGenerationParams : Prefab, ISerializableEntity { - private static MapGenerationParams instance; - private static string loadedFile; + public static readonly PrefabSelector Params = new PrefabSelector(); public static MapGenerationParams Instance { get { - return instance; + return Params.ActivePrefab; } } #if DEBUG - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool ShowLocations { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool ShowLevelTypeNames { get; set; } - [Serialize(true, true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool ShowOverlay { get; set; } #else public readonly bool ShowLocations = true; @@ -32,70 +33,70 @@ namespace Barotrauma public readonly bool ShowOverlay = true; #endif - [Serialize(6, true)] + [Serialize(6, IsPropertySaveable.Yes)] public int DifficultyZones { get; set; } //Number of difficulty zones - [Serialize(8000, true), Editable] + [Serialize(8000, IsPropertySaveable.Yes), Editable] public int Width { get; set; } - [Serialize(500, true), Editable] + [Serialize(500, IsPropertySaveable.Yes), Editable] public int Height { get; set; } - [Serialize(20.0f, true, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] + [Serialize(20.0f, IsPropertySaveable.Yes, description: "Connections with a length smaller or equal to this generate the smallest possible levels (using the MinWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] public float SmallLevelConnectionLength { get; set; } - [Serialize(200.0f, true, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] + [Serialize(200.0f, IsPropertySaveable.Yes, description: "Connections with a length larger or equal to this generate the largest possible levels (using the MaxWidth parameter in the level generation paramaters)."), Editable(0.0f, 5000.0f)] public float LargeLevelConnectionLength { get; set; } - [Serialize("20,20", true, description: "How far from each other voronoi sites are placed. " + + [Serialize("20,20", IsPropertySaveable.Yes, description: "How far from each other voronoi sites are placed. " + "Sites determine shape of the voronoi graph. Locations are placed at the vertices of the voronoi cells. " + "(Decreasing this value causes the number of sites, and the complexity of the map, to increase exponentially - be careful when adjusting)"), Editable] public Point VoronoiSiteInterval { get; set; } - [Serialize("5,5", true), Editable] + [Serialize("5,5", IsPropertySaveable.Yes), Editable] public Point VoronoiSiteVariance { get; set; } - [Serialize(10.0f, true, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "Connections smaller than this are removed."), Editable(0.0f, 500.0f)] public float MinConnectionDistance { get; set; } - [Serialize(5.0f, true, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)] + [Serialize(5.0f, IsPropertySaveable.Yes, description: "Locations that are closer than this to another location are removed."), Editable(0.0f, 100.0f)] public float MinLocationDistance { get; set; } - [Serialize(0.1f, true, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "ConnectionIterationMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] public float ConnectionIndicatorIterationMultiplier { get; set; } - [Serialize(0.1f, true, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] + [Serialize(0.1f, IsPropertySaveable.Yes, description: "ConnectionDisplacementMultiplier for the UI indicator lines between locations."), Editable(0.0f, 10.0f, DecimalCount = 2)] public float ConnectionIndicatorDisplacementMultiplier { get; set; } - public int[] GateCount { get; private set; } + public readonly ImmutableArray GateCount; #if CLIENT - [Serialize(0.75f, true), Editable(DecimalCount = 2)] + [Serialize(0.75f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float MinZoom { get; set; } - [Serialize(1.5f, true), Editable(DecimalCount = 2)] + [Serialize(1.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float MaxZoom { get; set; } - [Serialize(1.0f, true), Editable(DecimalCount = 2)] + [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)] public float MapTileScale { get; set; } - [Serialize(15.0f, true, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] + [Serialize(15.0f, IsPropertySaveable.Yes, description: "Size of the location icons in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] public float LocationIconSize { get; set; } - [Serialize(5.0f, true, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] + [Serialize(5.0f, IsPropertySaveable.Yes, description: "Width of the connections between locations, in pixels when at 100% zoom."), Editable(1.0f, 1000.0f)] public float LocationConnectionWidth { get; set; } - [Serialize("220,220,100,255", true, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()] + [Serialize("220,220,100,255", IsPropertySaveable.Yes, description: "The color used to display the indicators (current location, selected location, etc)."), Editable()] public Color IndicatorColor { get; set; } - [Serialize("150,150,150,255", true, description: "The color used to display the connections between locations."), Editable()] + [Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections between locations."), Editable()] public Color ConnectionColor { get; set; } - [Serialize("150,150,150,255", true, description: "The color used to display the connections between locations when they're highlighted."), Editable()] + [Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections between locations when they're highlighted."), Editable()] public Color HighlightedConnectionColor { get; set; } - [Serialize("150,150,150,255", true, description: "The color used to display the connections the player hasn't travelled through."), Editable()] + [Serialize("150,150,150,255", IsPropertySaveable.Yes, description: "The color used to display the connections the player hasn't travelled through."), Editable()] public Color UnvisitedConnectionColor { get; set; } public Sprite ConnectionSprite { get; private set; } @@ -110,110 +111,36 @@ namespace Barotrauma public Sprite CurrentLocationIndicator { get; private set; } public Sprite SelectedLocationIndicator { get; private set; } - private readonly Dictionary> mapTiles = new Dictionary>(); - public Dictionary> MapTiles - { - get { return mapTiles; } - } + public readonly ImmutableDictionary> MapTiles; #endif - public string Name - { - get { return GetType().ToString(); } - } + public string Name => GetType().ToString(); - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; } public RadiationParams RadiationParams; - public static void Init() - { - - var files = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.MapGenerationParameters); - if (!files.Any()) - { - DebugConsole.ThrowError("No map generation parameters found in the selected content packages!"); - return; - } - // Let's not actually load the parameters until we have solved which file is the last, because loading the parameters takes some resources that would also need to be released. - XElement selectedElement = null; - string selectedFile = null; - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - if (selectedElement != null) - { - DebugConsole.NewMessage($"Overriding the map generation parameters with '{file.Path}'", Color.Yellow); - } - } - else if (selectedElement != null) - { - DebugConsole.ThrowError($"Error in {file.Path}: Another map generation parameter file already loaded! Use tags to override it."); - break; - } - selectedElement = mainElement; - selectedFile = file.Path; - } - - if (selectedFile == loadedFile) { return; } - -#if CLIENT - if (instance != null) - { - instance?.ConnectionSprite?.Remove(); - instance?.PassedConnectionSprite?.Remove(); - instance?.SelectedLocationIndicator?.Remove(); - instance?.CurrentLocationIndicator?.Remove(); - instance?.DecorativeGraphSprite?.Remove(); - instance?.MissionIcon?.Remove(); - instance?.TypeChangeIcon?.Remove(); - instance?.FogOfWarSprite?.Remove(); - foreach (List spriteList in instance.mapTiles.Values) - { - foreach (Sprite sprite in spriteList) - { - sprite.Remove(); - } - } - instance.mapTiles.Clear(); - } -#endif - instance = null; - - if (selectedElement == null) - { - DebugConsole.ThrowError("Could not find a valid element in the map generation parameter files!"); - } - else - { - instance = new MapGenerationParams(selectedElement); - loadedFile = selectedFile; - } - } - - private MapGenerationParams(XElement element) + public MapGenerationParams(ContentXElement element, MapGenerationParametersFile file) : base(file, file.Path.Value.ToIdentifier()) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - GateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null); - if (GateCount == null) + var gateCount = element.GetAttributeIntArray("gatecount", null) ?? element.GetAttributeIntArray("GateCount", null); + if (gateCount == null) { - GateCount = new int[DifficultyZones]; + gateCount = new int[DifficultyZones]; for (int i = 0; i < DifficultyZones; i++) { - GateCount[i] = 1; + gateCount[i] = 1; } } + GateCount = gateCount.ToImmutableArray(); - foreach (XElement subElement in element.Elements()) + Dictionary> mapTiles = new Dictionary>(); + + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -225,7 +152,7 @@ namespace Barotrauma PassedConnectionSprite = new Sprite(subElement); break; case "maptile": - string biome = subElement.GetAttributeString("biome", ""); + Identifier biome = subElement.GetAttributeIdentifier("biome", ""); if (!mapTiles.ContainsKey(biome)) { mapTiles[biome] = new List(); @@ -257,6 +184,30 @@ namespace Barotrauma break; } } +#if CLIENT + MapTiles = mapTiles.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); +#endif + } + + public override void Dispose() + { +#if CLIENT + ConnectionSprite?.Remove(); + PassedConnectionSprite?.Remove(); + SelectedLocationIndicator?.Remove(); + CurrentLocationIndicator?.Remove(); + DecorativeGraphSprite?.Remove(); + MissionIcon?.Remove(); + TypeChangeIcon?.Remove(); + FogOfWarSprite?.Remove(); + foreach (ImmutableArray spriteList in MapTiles.Values) + { + foreach (Sprite sprite in spriteList) + { + sprite.Remove(); + } + } +#endif } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs index 9fd7a488e..1e1e136c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -12,18 +12,18 @@ namespace Barotrauma { public string Name => nameof(Radiation); - [Serialize(defaultValue: 0f, isSaveable: true)] + [Serialize(defaultValue: 0f, isSaveable: IsPropertySaveable.Yes)] public float Amount { get; set; } - [Serialize(defaultValue: true, isSaveable: true)] + [Serialize(defaultValue: true, isSaveable: IsPropertySaveable.Yes)] public bool Enabled { get; set; } - public Dictionary SerializableProperties { get; } + public Dictionary SerializableProperties { get; } public readonly Map Map; public readonly RadiationParams Params; - private Affliction radiationAffliction; + private Affliction? radiationAffliction; private float radiationTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs index b61811521..20933077b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs @@ -7,39 +7,39 @@ namespace Barotrauma internal class RadiationParams: ISerializableEntity { public string Name => nameof(RadiationParams); - public Dictionary SerializableProperties { get; } + public Dictionary SerializableProperties { get; } - [Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")] + [Serialize(defaultValue: -100f, isSaveable: IsPropertySaveable.No, "How much radiation the world starts with.")] public float StartingRadiation { get; set; } - [Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")] + [Serialize(defaultValue: 100f, isSaveable: IsPropertySaveable.No, "How much radiation is added on each step.")] public float RadiationStep { get; set; } - [Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")] + [Serialize(defaultValue: 10, isSaveable: IsPropertySaveable.No, "How many turns in radiation does it take for an outpost to be removed from the map.")] public int CriticalRadiationThreshold { get; set; } - [Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")] + [Serialize(defaultValue: 3, isSaveable: IsPropertySaveable.No, "Minimum amount of outposts in the level that cannot be removed due to radiation.")] public int MinimumOutpostAmount { get; set; } - [Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")] + [Serialize(defaultValue: 3f, isSaveable: IsPropertySaveable.No, "How fast the radiation increase animation goes.")] public float AnimationSpeed { get; set; } - [Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")] + [Serialize(defaultValue: 10f, isSaveable: IsPropertySaveable.No, "How long it takes to apply more radiation damage while in a radiated zone.")] public float RadiationDamageDelay { get; set; } - [Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")] + [Serialize(defaultValue: 1f, isSaveable: IsPropertySaveable.No, "How much is the radiation affliction increased by while in a radiated zone.")] public float RadiationDamageAmount { get; set; } - [Serialize(defaultValue: -1.0f, isSaveable: false, "Maximum amount of radiation.")] + [Serialize(defaultValue: -1.0f, isSaveable: IsPropertySaveable.No, "Maximum amount of radiation.")] public float MaxRadiation { get; set; } - [Serialize(defaultValue: "139,0,0,85", isSaveable: false, "The color of the radiated area.")] + [Serialize(defaultValue: "139,0,0,85", isSaveable: IsPropertySaveable.No, "The color of the radiated area.")] public Color RadiationAreaColor { get; set; } - [Serialize(defaultValue: "255,0,0,255", isSaveable: false, "The tint of the radiation border sprites.")] + [Serialize(defaultValue: "255,0,0,255", isSaveable: IsPropertySaveable.No, "The tint of the radiation border sprites.")] public Color RadiationBorderTint { get; set; } - [Serialize(defaultValue: 16.66f, isSaveable: false, "Speed of the border spritesheet animation.")] + [Serialize(defaultValue: 16.66f, isSaveable: IsPropertySaveable.No, "Speed of the border spritesheet animation.")] public float BorderAnimationSpeed { get; set; } public RadiationParams(XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 48da9e5a5..fb989ec73 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public static List mapEntityList = new List(); - public readonly MapEntityPrefab prefab; + public readonly MapEntityPrefab Prefab; protected List linkedToID; public List unresolvedLinkedToID; @@ -27,23 +27,22 @@ namespace Barotrauma /// protected readonly List Upgrades = new List(); - public HashSet disallowedUpgrades = new HashSet(); - - [Editable, Serialize("", true)] + public readonly HashSet DisallowedUpgradeSet = new HashSet(); + + [Editable, Serialize("", IsPropertySaveable.Yes)] public string DisallowedUpgrades { - get { return string.Join(",", disallowedUpgrades); } + get { return string.Join(",", DisallowedUpgradeSet); } set { - disallowedUpgrades.Clear(); + DisallowedUpgradeSet.Clear(); if (!string.IsNullOrWhiteSpace(value)) { string[] splitTags = value.Split(','); foreach (string tag in splitTags) { string[] splitTag = tag.Trim().Split(':'); - splitTag[0] = splitTag[0].ToLowerInvariant(); - disallowedUpgrades.Add(string.Join(":", splitTag)); + DisallowedUpgradeSet.Add(string.Join(":", splitTag).ToIdentifier()); } } } @@ -108,19 +107,19 @@ namespace Barotrauma get { return false; } } - public List AllowedLinks => prefab == null ? new List() : prefab.AllowedLinks; + public IEnumerable AllowedLinks => Prefab == null ? Enumerable.Empty() : Prefab.AllowedLinks; public bool ResizeHorizontal { - get { return prefab != null && prefab.ResizeHorizontal; } + get { return Prefab != null && Prefab.ResizeHorizontal; } } public bool ResizeVertical { - get { return prefab != null && prefab.ResizeVertical; } + get { return Prefab != null && Prefab.ResizeVertical; } } //for upgrading the dimensions of the entity from xml - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int RectWidth { get { return rect.Width; } @@ -131,7 +130,7 @@ namespace Barotrauma } } //for upgrading the dimensions of the entity from xml - [Serialize(0, false)] + [Serialize(0, IsPropertySaveable.No)] public int RectHeight { get { return rect.Height; } @@ -147,7 +146,7 @@ namespace Barotrauma public bool SpriteDepthOverrideIsSet { get; private set; } public float SpriteOverrideDepth => SpriteDepth; private float _spriteOverrideDepth = float.NaN; - [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)] + [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, IsPropertySaveable.Yes)] public float SpriteDepth { get @@ -166,10 +165,10 @@ namespace Barotrauma } } - [Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] + [Serialize(1f, IsPropertySaveable.Yes), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] public virtual float Scale { get; set; } = 1; - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool HiddenInGame { get; @@ -225,14 +224,14 @@ namespace Barotrauma } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool RemoveIfLinkedOutpostDoorInUse { get; protected set; } = true; - [Serialize("", true, "Submarine editor layer")] + [Serialize("", IsPropertySaveable.Yes, "Submarine editor layer")] public string Layer { get; set; } /// @@ -249,7 +248,7 @@ namespace Barotrauma public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id) { - this.prefab = prefab; + this.Prefab = prefab; Scale = prefab != null ? prefab.Scale : 1; } @@ -303,12 +302,12 @@ namespace Barotrauma return (Submarine.RectContains(WorldRect, position)); } - public bool HasUpgrade(string identifier) + public bool HasUpgrade(Identifier identifier) { return GetUpgrade(identifier) != null; } - public Upgrade GetUpgrade(string identifier) + public Upgrade GetUpgrade(Identifier identifier) { return Upgrades.Find(upgrade => upgrade.Identifier == identifier); } @@ -331,7 +330,7 @@ namespace Barotrauma { AddUpgrade(upgrade, createNetworkEvent); } - DebugConsole.Log($"Set (ID: {ID} {prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); + DebugConsole.Log($"Set (ID: {ID} {Prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); } /// @@ -344,7 +343,7 @@ namespace Barotrauma return false; } - if (disallowedUpgrades.Contains(upgrade.Identifier)) { return false; } + if (DisallowedUpgradeSet.Contains(upgrade.Identifier)) { return false; } Upgrade existingUpgrade = GetUpgrade(upgrade.Identifier); @@ -560,7 +559,7 @@ namespace Barotrauma /// public static void UpdateAll(float deltaTime, Camera cam) { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { hull.Update(deltaTime, cam); } @@ -635,7 +634,7 @@ namespace Barotrauma IdRemap idRemap = new IdRemap(parentElement, idOffset); List entities = new List(); - foreach (XElement element in parentElement.Elements()) + foreach (var element in parentElement.Elements()) { string typeName = element.Name.ToString(); @@ -658,7 +657,7 @@ namespace Barotrauma if (t == typeof(Structure)) { string name = element.Attribute("name").Value; - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null) { @@ -672,7 +671,7 @@ namespace Barotrauma try { - MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) }); + MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(ContentXElement), typeof(Submarine), typeof(IdRemap) }); if (loadMethod == null) { DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + "."); @@ -683,7 +682,7 @@ namespace Barotrauma } else { - object newEntity = loadMethod.Invoke(t, new object[] { element, submarine, idRemap }); + object newEntity = loadMethod.Invoke(t, new object[] { element.FromPackage(null), submarine, idRemap }); if (newEntity != null) { entities.Add((MapEntity)newEntity); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 6596c4067..7a7fa2598 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -1,8 +1,10 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -23,7 +25,7 @@ namespace Barotrauma Legacy = 1024 } - abstract partial class MapEntityPrefab : IPrefab, IDisposable + abstract partial class MapEntityPrefab : PrefabWithUintIdentifier { public static IEnumerable List { @@ -51,207 +53,15 @@ namespace Barotrauma } } - protected string originalName; - protected string identifier; - - public Sprite sprite; + //which prefab has been selected for placing + public static MapEntityPrefab Selected { get; set; } //the position where the structure is being placed (needed when stretching the structure) protected static Vector2 placePosition; - protected ConstructorInfo constructor; - - //is it possible to stretch the entity horizontally/vertically - [Serialize(false, false)] - public bool ResizeHorizontal { get; protected set; } - - [Serialize(false, false)] - public bool ResizeVertical { get; protected set; } - - //which prefab has been selected for placing - protected static MapEntityPrefab selected; - - public string OriginalName - { - get { return originalName; } - } - - public virtual string Name - { - get { return originalName; } - } - - public string GetItemNameTextId() - { - var textId = $"entityname.{Identifier}"; - return TextManager.ContainsTag(textId) ? textId : null; - } - - public string GetHullNameTextId() - { - var textId = $"roomname.{Identifier}"; - return TextManager.ContainsTag(textId) ? textId : null; - } - - //Used to differentiate between items when saving/loading - //Allows changing the name of an item without breaking existing subs or having multiple items with the same name - public string Identifier - { - get { return identifier; } - } - - public string FilePath { get; protected set; } - - public ContentPackage ContentPackage { get; protected set; } - - public HashSet Tags - { - get; - protected set; - } = new HashSet(); - - public static MapEntityPrefab Selected - { - get { return selected; } - set { selected = value; } - } - - [Serialize("", false)] - public string Description - { - get; - protected set; - } - - [Serialize("", false)] - public string AllowedUpgrades { get; set; } - - [Serialize(false, false)] - public bool HideInMenus { get; set; } - - [Serialize("", false)] - public string Subcategory { get; set; } - - [Serialize(false, false)] - public bool Linkable - { - get; - protected set; - } - - /// - /// Links defined to identifiers. - /// - public List AllowedLinks { get; protected set; } = new List(); - - public MapEntityCategory Category - { - get; - protected set; - } - - [Serialize("1.0,1.0,1.0,1.0", false)] - public Color SpriteColor - { - get; - protected set; - } - - [Serialize(1f, true), Editable(0.1f, 10f, DecimalCount = 3)] - public float Scale { get; protected set; } - - //If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias. - //(allows changing names while keeping backwards compatibility with older sub files) - public HashSet Aliases - { - get; - protected set; - } - - public static void Init() - { - CoreEntityPrefab ep = new CoreEntityPrefab - { - identifier = "hull", - originalName = TextManager.Get("EntityName.hull"), - Description = TextManager.Get("EntityDescription.hull"), - constructor = typeof(Hull).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), - ResizeHorizontal = true, - ResizeVertical = true, - Linkable = true - }; - ep.AllowedLinks.Add("hull"); - ep.Aliases = new HashSet { "hull" }; - CoreEntityPrefab.Prefabs.Add(ep, false); - - ep = new CoreEntityPrefab - { - identifier = "gap", - originalName = TextManager.Get("EntityName.gap"), - Description = TextManager.Get("EntityDescription.gap"), - constructor = typeof(Gap).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }), - ResizeHorizontal = true, - ResizeVertical = true - }; - CoreEntityPrefab.Prefabs.Add(ep, false); - ep.Aliases = new HashSet { "gap" }; - - ep = new CoreEntityPrefab - { - identifier = "waypoint", - originalName = TextManager.Get("EntityName.waypoint"), - Description = TextManager.Get("EntityDescription.waypoint"), - constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }) - }; - CoreEntityPrefab.Prefabs.Add(ep, false); - ep.Aliases = new HashSet { "waypoint" }; - - ep = new CoreEntityPrefab - { - identifier = "spawnpoint", - originalName = TextManager.Get("EntityName.spawnpoint"), - Description = TextManager.Get("EntityDescription.spawnpoint"), - constructor = typeof(WayPoint).GetConstructor(new Type[] { typeof(MapEntityPrefab), typeof(Rectangle) }) - }; - CoreEntityPrefab.Prefabs.Add(ep, false); - ep.Aliases = new HashSet { "spawnpoint" }; - } - - public abstract void Dispose(); - - public MapEntityPrefab() - { - Category = MapEntityCategory.Structure; - } - - public string[] GetAllowedUpgrades() - { - return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(","); - } - - public bool HasSubCategory(string subcategory) - { - return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false; - } - - protected virtual void CreateInstance(Rectangle rect) - { - if (constructor == null) return; - object[] lobject = new object[] { this, rect }; - constructor.Invoke(lobject); - } - -#if DEBUG - public void DebugCreateInstance() - { - Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y)); - CreateInstance(rect); - } -#endif - public static bool SelectPrefab(object selection) { - if ((selected = selection as MapEntityPrefab) != null) + if ((Selected = selection as MapEntityPrefab) != null) { placePosition = Vector2.Zero; return true; @@ -262,37 +72,45 @@ namespace Barotrauma } } + //a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected + public static object GetSelected() + { + return (object)Selected; + } + /// /// Find a matching map entity prefab /// /// The name of the item (can be omitted when searching based on identifier) /// The identifier of the item (if null, the identifier is ignored and the search is done only based on the name) + [Obsolete("Prefer MapEntityPrefab.FindByIdentifier or MapEntityPrefab.FindByName")] public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true) { - if (name != null) - { - name = name.ToLowerInvariant(); - } + return Find(name, (identifier ?? "").ToIdentifier(), showErrorMessages); + } + [Obsolete("Prefer MapEntityPrefab.FindByIdentifier or MapEntityPrefab.FindByName")] + public static MapEntityPrefab Find(string name, Identifier identifier, bool showErrorMessages = true) + { //try to search based on identifier first - if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(identifier)) + if (string.IsNullOrEmpty(name) && !identifier.IsEmpty) { - foreach (MapEntityPrefab prefab in List) - { - if (prefab.identifier == identifier) { return prefab; } - } + if (CoreEntityPrefab.Prefabs.ContainsKey(identifier)) { return CoreEntityPrefab.Prefabs[identifier]; } + if (StructurePrefab.Prefabs.ContainsKey(identifier)) { return StructurePrefab.Prefabs[identifier]; } + if (ItemPrefab.Prefabs.ContainsKey(identifier)) { return ItemPrefab.Prefabs[identifier]; } + if (ItemAssemblyPrefab.Prefabs.ContainsKey(identifier)) { return ItemAssemblyPrefab.Prefabs[identifier]; } } foreach (MapEntityPrefab prefab in List) { - if (identifier != null) + if (!identifier.IsEmpty) { - if (prefab.identifier != identifier) + if (prefab.Identifier != identifier) { - if (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(identifier, StringComparison.OrdinalIgnoreCase))) + if (prefab.Aliases != null && prefab.Aliases.Any(a => a == identifier)) { return prefab; - } + } continue; } else @@ -302,8 +120,8 @@ namespace Barotrauma } if (!string.IsNullOrEmpty(name)) { - if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || - prefab.originalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))) { return prefab; @@ -332,28 +150,133 @@ namespace Barotrauma return List.FirstOrDefault(p => predicate(p)); } + + public static MapEntityPrefab FindByName(string name) + { + if (name.IsNullOrEmpty()) { throw new ArgumentException($"{nameof(name)} must not be null or empty"); } + + return Find(prefab => + prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.OriginalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + (prefab.Aliases != null && + prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))); + } + + public static MapEntityPrefab FindByIdentifier(Identifier identifier) + => CoreEntityPrefab.Prefabs.TryGet(identifier, out var corePrefab) ? corePrefab + : ItemPrefab.Prefabs.TryGet(identifier, out var itemPrefab) ? itemPrefab + : StructurePrefab.Prefabs.TryGet(identifier, out var structurePrefab) ? structurePrefab + : ItemAssemblyPrefab.Prefabs.TryGet(identifier, out var itemAssemblyPrefab) ? itemAssemblyPrefab + : (MapEntityPrefab)null; + + public abstract Sprite Sprite { get; } + + public abstract string OriginalName { get; } + + public abstract LocalizedString Name { get; } + + public abstract ImmutableHashSet Tags { get; } + + /// + /// Links defined to identifiers. + /// + public abstract ImmutableHashSet AllowedLinks { get; } + + public abstract MapEntityCategory Category { get; } + + //If a matching prefab is not found when loading a sub, the game will attempt to find a prefab with a matching alias. + //(allows changing names while keeping backwards compatibility with older sub files) + public abstract ImmutableHashSet Aliases { get; } + + //is it possible to stretch the entity horizontally/vertically + [Serialize(false, IsPropertySaveable.No)] + public bool ResizeHorizontal { get; protected set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool ResizeVertical { get; protected set; } + + [Serialize("", IsPropertySaveable.No)] + public LocalizedString Description { get; protected set; } + + [Serialize("", IsPropertySaveable.No)] + public string AllowedUpgrades { get; protected set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool HideInMenus { get; protected set; } + + [Serialize("", IsPropertySaveable.No)] + public string Subcategory { get; protected set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool Linkable { get; protected set; } + + [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.No)] + public Color SpriteColor { get; protected set; } + + [Serialize(1f, IsPropertySaveable.Yes), Editable(0.1f, 10f, DecimalCount = 3)] + public float Scale { get; protected set; } + + protected MapEntityPrefab(Identifier identifier) : base(null, identifier) { } + + public MapEntityPrefab(ContentXElement element, ContentFile file) : base(file, element) { } + + public string GetItemNameTextId() + { + var textId = $"entityname.{Identifier}"; + return TextManager.ContainsTag(textId) ? textId : null; + } + + public string GetHullNameTextId() + { + var textId = $"roomname.{Identifier}"; + return TextManager.ContainsTag(textId) ? textId : null; + } + + private string cachedAllowedUpgrades = ""; + private ImmutableHashSet allowedUpgradeSet; + public IEnumerable GetAllowedUpgrades() + { + if (string.IsNullOrWhiteSpace(AllowedUpgrades)) { return Enumerable.Empty(); } + if (allowedUpgradeSet is null || cachedAllowedUpgrades != AllowedUpgrades) + { + allowedUpgradeSet = AllowedUpgrades.Split(",").ToIdentifiers().ToImmutableHashSet(); + cachedAllowedUpgrades = AllowedUpgrades; + } + + return allowedUpgradeSet; + } + + public bool HasSubCategory(string subcategory) + { + return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false; + } + + protected abstract void CreateInstance(Rectangle rect); + +#if DEBUG + public void DebugCreateInstance() + { + Rectangle rect = new Rectangle(new Point((int)Screen.Selected.Cam.WorldViewCenter.X, (int)Screen.Selected.Cam.WorldViewCenter.Y), new Point((int)Submarine.GridSize.X, (int)Submarine.GridSize.Y)); + CreateInstance(rect); + } +#endif + /// /// Check if the name or any of the aliases of this prefab match the given name. /// - public bool NameMatches(string name, StringComparison comparisonType) => originalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType))); + public bool NameMatches(string name, StringComparison comparisonType) => OriginalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType))); public bool NameMatches(IEnumerable allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType)); public bool IsLinkAllowed(MapEntityPrefab target) { if (target == null) { return false; } - if (target is StructurePrefab && AllowedLinks.Contains("structure")) { return true; } - if (target is ItemPrefab && AllowedLinks.Contains("item")) { return true; } - if (target is LinkedSubmarinePrefab && Tags.Contains("dock")) { return true; } - if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock")) { return true; } - return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier) - || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); - } - - //a method that allows the GUIListBoxes to check through a delegate if the entityprefab is still selected - public static object GetSelected() - { - return (object)selected; + if (target is StructurePrefab && AllowedLinks.Contains("structure".ToIdentifier())) { return true; } + if (target is ItemPrefab && AllowedLinks.Contains("item".ToIdentifier())) { return true; } + if (target is LinkedSubmarinePrefab && Tags.Contains("dock".ToIdentifier())) { return true; } + if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock".ToIdentifier())) { return true; } + return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(Identifier) + || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index 8f9d7b13d..ba0c54911 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -1,233 +1,209 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; +using System.Linq; using Barotrauma.IO; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using System.Xml.Linq; -using System.Linq; namespace Barotrauma { public class Md5Hash { - private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private const string cachePath = "Data/hashcache.txt"; - private static readonly Dictionary> cache = new Dictionary>(); - - public static void LoadCache() + public static class Cache { - try + private const string cachePath = "Data/hashcache.txt"; + + private readonly static List<(string Path, Md5Hash Hash, DateTime DateTime)> Entries + = new List<(string Path, Md5Hash Hash, DateTime DateTime)>(); + + public static void Load() { if (!File.Exists(cachePath)) { return; } - string[] lines = File.ReadAllLines(cachePath, Encoding.UTF8); - if (lines.Length <= 0 || lines[0] != GameMain.Version.ToString()) { return; } - foreach (string line in lines.Skip(1)) + var lines = File.ReadAllLines(cachePath); + if (Version.TryParse(lines[0], out var cacheVersion) && cacheVersion == GameMain.Version) { - if (string.IsNullOrWhiteSpace(line)) { continue; } - string[] parts = line.Split('|'); - if (parts.Length < 3) { continue; } - - string path = parts[0].CleanUpPath(); - string hashStr = parts[1]; - long timeLong = long.Parse(parts[2]); - - Md5Hash hash = new Md5Hash(hashStr); - DateTime time = DateTime.FromBinary(timeLong); - - if (File.GetLastWriteTime(path) == time && !cache.ContainsKey(path)) + for (int i = 1; i < lines.Length; i++) { - cache.Add(path, new Tuple(hash, timeLong)); + string[] split = lines[i].Split('|'); + string path = split[0].CleanUpPathCrossPlatform(); + Md5Hash hash = Md5Hash.StringAsHash(split[1]); + DateTime? dateTime = null; + if (long.TryParse(split[2], out long dateTimeUlong)) + { + dateTime = DateTime.FromBinary(dateTimeUlong); + } + + if (File.Exists(path) && dateTime.HasValue && dateTime >= File.GetLastWriteTime(path)) + { + Entries.Add((path, hash, dateTime.Value)); + } } } } - catch (Exception e) + + public static void Add(string path, Md5Hash hash, DateTime dateTime) { - DebugConsole.NewMessage($"Failed to load hash cache: {e.Message}\n{e.StackTrace.CleanupStackTrace()}", Microsoft.Xna.Framework.Color.Orange); - cache.Clear(); + path = path.CleanUpPathCrossPlatform(); + Remove(path); + Entries.Add((path, hash, dateTime)); + } + + public static void Remove(string path) + { + path = path.CleanUpPathCrossPlatform(); + Entries.RemoveAll(e => e.Path == path); } } - public static void SaveCache() - { -#if SERVER - //don't save to the cache if the server is owned by a client, - //since this suggests that they're running concurrently and - //will interfere with each other here - if (GameMain.Server?.OwnerConnection != null) { return; } -#endif + public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); + + private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + //thanks to Jlobblet for this regex + private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - string[] lines = new string[cache.Count + 1]; - lines[0] = GameMain.Version.ToString(); - int i = 1; - foreach (KeyValuePair> kpv in cache) - { - lines[i] = kpv.Key + "|" + kpv.Value.Item1 + "|" + kpv.Value.Item2; - i++; - } - File.WriteAllLines(cachePath, lines, Encoding.UTF8); - } + public readonly byte[] ByteRepresentation; + public readonly string StringRepresentation; + public readonly string ShortRepresentation; - private bool LoadFromCache(string filename) - { - if (!string.IsNullOrWhiteSpace(filename)) - { - filename = filename.CleanUpPath(); - lock (cache) - { - if (cache.ContainsKey(filename)) - { - Hash = cache[filename].Item1.Hash; - ShortHash = cache[filename].Item1.ShortHash; - return true; - } - } - } - return false; - } - - public void SaveToCache(string filename, long? time = null) - { - if (string.IsNullOrWhiteSpace(filename)) { return; } - - lock (cache) - { - filename = filename.CleanUpPath(); - Tuple cacheVal = new Tuple(this, time ?? File.GetLastWriteTime(filename).ToBinary()); - if (cache.ContainsKey(filename)) - { - cache[filename] = cacheVal; - } - else - { - cache.Add(filename, cacheVal); - } - SaveCache(); - } - } - - public static Md5Hash FetchFromCache(string filename) - { - Md5Hash newHash = new Md5Hash(); - if (newHash.LoadFromCache(filename)) { return newHash; } - return null; - } - - public string Hash { get; private set; } - - public string ShortHash { get; private set; } - - private Md5Hash() - { - this.Hash = null; - ShortHash = null; - } - - public Md5Hash(string md5Hash) - { - this.Hash = md5Hash; - ShortHash = GetShortHash(md5Hash); - } - - public Md5Hash(byte[] bytes) - { - Hash = CalculateHash(bytes); - - ShortHash = GetShortHash(Hash); - } - - public Md5Hash(FileStream fileStream, string filename = null, bool tryLoadFromCache = true) - { - if (tryLoadFromCache) - { - if (LoadFromCache(filename)) { return; } - } - - Hash = CalculateHash(fileStream); - - ShortHash = GetShortHash(Hash); - - SaveToCache(filename); - } - - public Md5Hash(XDocument doc, string filename = null, bool tryLoadFromCache = true) - { - if (tryLoadFromCache) - { - if (LoadFromCache(filename)) { return; } - } - - if (doc == null) { return; } - - string docString = removeWhitespaceRegex.Replace(doc.ToString(), ""); - - byte[] inputBytes = Encoding.ASCII.GetBytes(docString); - - Hash = CalculateHash(inputBytes); - ShortHash = GetShortHash(Hash); - - SaveToCache(filename); - } - - public override string ToString() - { - return Hash; - } - - private string CalculateHash(FileStream stream) - { - using (MD5 md5 = MD5.Create()) - { - byte[] byteHash = md5.ComputeHash(stream); - // step 2, convert byte array to hex string - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < byteHash.Length; i++) - { - sb.Append(byteHash[i].ToString("X2")); - } - - return sb.ToString(); - } - } - - private string CalculateHash(byte[] bytes) + private static void CalculateHash(byte[] bytes, out string stringRepresentation, out byte[] byteRepresentation) { using (MD5 md5 = MD5.Create()) { byte[] byteHash = md5.ComputeHash(bytes); - // step 2, convert byte array to hex string - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < byteHash.Length; i++) - { - sb.Append(byteHash[i].ToString("X2")); - } - return sb.ToString(); + byteRepresentation = byteHash; + stringRepresentation = ByteRepresentationToStringRepresentation(byteHash); } } + private static string ByteRepresentationToStringRepresentation(byte[] byteHash) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < byteHash.Length; i++) + { + sb.Append(byteHash[i].ToString("X2")); + } + + return sb.ToString(); + } + + private static byte[] StringRepresentationToByteRepresentation(string strHash) + { + var byteRepresentation = new byte[strHash.Length / 2]; + for (int i = 0; i < byteRepresentation.Length; i++) + { + byteRepresentation[i] = Convert.ToByte(strHash.Substring(i * 2, 2), 16); + } + + return byteRepresentation; + } + public static string GetShortHash(string fullHash) { - if (string.IsNullOrEmpty(fullHash)) { return ""; } return fullHash.Length < 7 ? fullHash : fullHash.Substring(0, 7); } - public static bool RemoveFromCache(string filename) + private Md5Hash(string md5Hash) { - if (!string.IsNullOrWhiteSpace(filename)) + StringRepresentation = md5Hash; + ByteRepresentation = StringRepresentationToByteRepresentation(StringRepresentation); + + ShortRepresentation = GetShortHash(md5Hash); + } + + private Md5Hash(byte[] bytes, bool calculate) + { + if (calculate) { - filename = filename.CleanUpPath(); - lock (cache) - { - if (cache.ContainsKey(filename)) - { - cache.Remove(filename); - return true; - } - } + CalculateHash(bytes, out StringRepresentation, out ByteRepresentation); + } + else + { + StringRepresentation = ByteRepresentationToStringRepresentation(bytes); + ByteRepresentation = bytes; + } + + ShortRepresentation = GetShortHash(StringRepresentation); + } + + public static Md5Hash StringAsHash(string hash) + { + if (!stringHashRegex.IsMatch(hash)) { throw new ArgumentException($"{hash} is not a valid hash"); } + return new Md5Hash(hash); + } + + public static Md5Hash CalculateForBytes(byte[] bytes) + { + return new Md5Hash(bytes, calculate: true); + } + + public static Md5Hash BytesAsHash(byte[] bytes) + { + return new Md5Hash(bytes, calculate: false); + } + + [Flags] + public enum StringHashOptions + { + BytePerfect = 0, + IgnoreCase = 0x1, + IgnoreWhitespace = 0x2 + } + + public static Md5Hash CalculateForFile(string path, StringHashOptions options) + { + if (options.HasFlag(StringHashOptions.IgnoreWhitespace) || options.HasFlag(StringHashOptions.IgnoreCase)) + { + string str = File.ReadAllText(path, Encoding.UTF8); + return CalculateForString(str, options); + } + else + { + byte[] bytes = File.ReadAllBytes(path); + return CalculateForBytes(bytes); + } + } + + public static Md5Hash CalculateForString(string str, StringHashOptions options) + { + if (options.HasFlag(StringHashOptions.IgnoreCase)) + { + str = str.ToLowerInvariant(); + } + if (options.HasFlag(StringHashOptions.IgnoreWhitespace)) + { + str = removeWhitespaceRegex.Replace(str, ""); + } + byte[] bytes = Encoding.UTF8.GetBytes(str); + return CalculateForBytes(bytes); + } + + public override string ToString() + { + return StringRepresentation; + } + + public override bool Equals(object? obj) + { + if (obj is Md5Hash { StringRepresentation: { } otherStr }) + { + string selfStr = otherStr.Length < StringRepresentation.Length + ? StringRepresentation[..otherStr.Length] + : StringRepresentation; + otherStr = StringRepresentation.Length < otherStr.Length + ? otherStr[..StringRepresentation.Length] + : otherStr; + return selfStr.Equals(otherStr, StringComparison.OrdinalIgnoreCase); } return false; } + + public static bool operator ==(Md5Hash? a, Md5Hash? b) + => (a is null == b is null) && (a?.Equals(b) ?? true); + + public static bool operator !=(Md5Hash? a, Md5Hash? b) => !(a == b); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs index 4e76b70b0..5397af405 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/NPCSet.cs @@ -1,93 +1,37 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma { - internal class NPCSet : IDisposable + internal class NPCSet : Prefab { - private static List? Sets { get; set; } + public readonly static PrefabCollection Sets = new PrefabCollection(); - private string Identifier { get; } - private readonly List Humans = new List(); + private readonly ImmutableArray Humans; private bool Disposed { get; set; } - private NPCSet(XElement element, string filePath) + public NPCSet(ContentXElement element, NPCSetsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { - Identifier = element.GetAttributeString("identifier", string.Empty); - - foreach (XElement npcElement in element.Elements()) - { - Humans.Add(new HumanPrefab(npcElement, filePath)); - } + Humans = element.Elements().Select(npcElement => new HumanPrefab(npcElement, file)).ToImmutableArray(); } - public static HumanPrefab? Get(string identifier, string npcidentifier) + public static HumanPrefab? Get(Identifier setIdentifier, Identifier npcidentifier) { - HumanPrefab prefab = Sets.Where(set => set.Identifier == identifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); + HumanPrefab? prefab = Sets.Where(set => set.Identifier == setIdentifier).SelectMany(npcSet => npcSet.Humans.Where(npcSetHuman => npcSetHuman.Identifier == npcidentifier)).FirstOrDefault(); if (prefab == null) { - DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{identifier}\"."); + DebugConsole.ThrowError($"Could not find human prefab \"{npcidentifier}\" from \"{setIdentifier}\"."); return null; } - return new HumanPrefab(prefab.Element, prefab.FilePath); - } - - public static void LoadSets() - { - Sets?.ForEach(set => set.Dispose()); - Sets = new List(); - IEnumerable files = GameMain.Instance.GetFilesOfType(ContentType.NPCSets); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - XElement? rootElement = doc?.Root; - - if (doc == null || rootElement == null) { continue; } - - if (doc.Root.IsOverride()) - { - Sets.Clear(); - DebugConsole.NewMessage($"Overriding all NPC sets with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in rootElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the NPC set config '{elementName}' in file '{file.Path}'"); - continue; - } - - var existingParams = Sets.Find(set => set.Identifier == identifier); - if (existingParams != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding NPC set config '{identifier}' using the file '{file.Path}'", Color.Yellow); - Sets.Remove(existingParams); - } - else - { - DebugConsole.ThrowError($"Duplicate NPC set config: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - - Sets.Add(new NPCSet(element, file.Path)); - } - } + return prefab; } private void Dispose(bool disposing) @@ -103,7 +47,7 @@ namespace Barotrauma Disposed = true; } - public void Dispose() + public override void Dispose() { Dispose(true); GC.SuppressFinalize(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 7b29db634..3ceac779c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -1,164 +1,209 @@ 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 { - class OutpostGenerationParams : ISerializableEntity + class OutpostGenerationParams : PrefabWithUintIdentifier, ISerializableEntity { - public static List Params { get; private set; } - + public readonly static PrefabCollection OutpostParams = new PrefabCollection(); + public virtual string Name { get; private set; } - - public string Identifier { get; private set; } - - private readonly List allowedLocationTypes = new List(); + + private readonly HashSet allowedLocationTypes = new HashSet(); /// /// Identifiers of the location types this outpost can appear in. If empty, can appear in all types of locations. /// - public IEnumerable AllowedLocationTypes + public IEnumerable AllowedLocationTypes { get { return allowedLocationTypes; } } - [Serialize(10, isSaveable: true), Editable(MinValueInt = 1, MaxValueInt = 50)] + [Serialize(10, IsPropertySaveable.Yes), Editable(MinValueInt = 1, MaxValueInt = 50)] public int TotalModuleCount { get; set; } - [Serialize(200.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(200.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float MinHallwayLength { get; set; } - [Serialize(false, isSaveable: true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AlwaysDestructible { get; set; } - [Serialize(false, isSaveable: true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AlwaysRewireable { get; set; } - [Serialize(false, isSaveable: true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool AllowStealing { get; set; } - [Serialize(true, isSaveable: true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool SpawnCrewInsideOutpost { get; set; } - - [Serialize(true, isSaveable: true), Editable] + + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool LockUnusedDoors { get; set; } - [Serialize(true, isSaveable: true), Editable] + [Serialize(true, IsPropertySaveable.Yes), Editable] public bool RemoveUnusedGaps { get; set; } - [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MinWaterPercentage { get; set; } - [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] public float MaxWaterPercentage { get; set; } - [Serialize("", isSaveable: true), Editable] + [Serialize("", IsPropertySaveable.Yes), Editable] public string ReplaceInRadiation { get; set; } - private readonly Dictionary moduleCounts = new Dictionary(); + private readonly Dictionary moduleCounts = new Dictionary(); - public IEnumerable> ModuleCounts + public IReadOnlyDictionary ModuleCounts { get { return moduleCounts; } } - private readonly List> humanPrefabLists = new List>(); - - public Dictionary SerializableProperties { get; private set; } - - protected OutpostGenerationParams(XElement element, string filePath) + private class NpcCollection : IReadOnlyList { - Identifier = element.GetAttributeString("identifier", ""); - Name = element.GetAttributeString("name", Identifier); - allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty()).ToList(); - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + private class Entry + { + private readonly HumanPrefab humanPrefab = null; + private readonly Identifier setIdentifier = Identifier.Empty; + private readonly Identifier npcIdentifier = Identifier.Empty; + + public Entry(HumanPrefab humanPrefab) + { + this.humanPrefab = humanPrefab; + } - if (element == null) { return; } - foreach (XElement subElement in element.Elements()) + public Entry(Identifier setIdentifier, Identifier npcIdentifier) + { + this.setIdentifier = setIdentifier; + this.npcIdentifier = npcIdentifier; + } + + public HumanPrefab HumanPrefab + => humanPrefab ?? NPCSet.Get(setIdentifier, npcIdentifier); + } + + private readonly List entries = new List(); + + public void Add(HumanPrefab humanPrefab) + => entries.Add(new Entry(humanPrefab)); + + + public void Add(Identifier setIdentifier, Identifier npcIdentifier) + => entries.Add(new Entry(setIdentifier, npcIdentifier)); + + public IEnumerator GetEnumerator() + { + foreach (var entry in entries) + { + yield return entry.HumanPrefab; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => entries.Count; + + public HumanPrefab this[int index] => entries[index].HumanPrefab; + } + + private readonly ImmutableArray> humanPrefabCollections; + + public Dictionary SerializableProperties { get; private set; } + + #warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes + public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) + { + Name = element.GetAttributeString("name", Identifier.Value); + allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty()).ToHashSet(); + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + var humanPrefabCollections = new List>(); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "modulecount": - string moduleFlag = (subElement.GetAttributeString("flag", null) ?? subElement.GetAttributeString("moduletype", "")).ToLowerInvariant(); + Identifier moduleFlag = subElement.GetAttributeIdentifier("flag", subElement.GetAttributeIdentifier("moduletype", "")); moduleCounts[moduleFlag] = subElement.GetAttributeInt("count", 0); break; case "npcs": - humanPrefabLists.Add(new List()); - foreach (XElement npcElement in subElement.Elements()) + var newCollection = new NpcCollection(); + foreach (var npcElement in subElement.Elements()) { - string from = npcElement.GetAttributeString("from", string.Empty); - - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (!string.IsNullOrWhiteSpace(from)) + Identifier from = npcElement.GetAttributeIdentifier("from", Identifier.Empty); + + if (from != Identifier.Empty) { - HumanPrefab prefab = NPCSet.Get(from, npcElement.GetAttributeString("identifier", string.Empty)); - if (prefab != null) - { - humanPrefabLists.Last().Add(prefab); - } + newCollection.Add(from, npcElement.GetAttributeIdentifier("identifier", Identifier.Empty)); } else { - humanPrefabLists.Last().Add(new HumanPrefab(npcElement, filePath)); + newCollection.Add(new HumanPrefab(npcElement, file)); } } + humanPrefabCollections.Add(newCollection); break; } } + + this.humanPrefabCollections = humanPrefabCollections.ToImmutableArray(); } - public int GetModuleCount(string moduleFlag) + public int GetModuleCount(Identifier moduleFlag) { - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return int.MaxValue; } + if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return int.MaxValue; } return moduleCounts.ContainsKey(moduleFlag) ? moduleCounts[moduleFlag] : 0; } - public void SetModuleCount(string moduleFlag, int count) + public void SetModuleCount(Identifier moduleFlag, int count) { - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag == "none") { return; } + if (moduleFlag == Identifier.Empty || moduleFlag == "none") { return; } if (count <= 0) { moduleCounts.Remove(moduleFlag); @@ -169,66 +214,22 @@ namespace Barotrauma } } - public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) + public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) { this.allowedLocationTypes.Clear(); - foreach (string locationType in allowedLocationTypes) + foreach (Identifier locationType in allowedLocationTypes) { - if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; } + if (locationType == "any") { continue; } this.allowedLocationTypes.Add(locationType); } } - public IEnumerable GetHumanPrefabs(Rand.RandSync randSync) + public IReadOnlyList GetHumanPrefabs(Rand.RandSync randSync) { - if (humanPrefabLists == null || !humanPrefabLists.Any()) { return Enumerable.Empty(); } - return humanPrefabLists.GetRandom(randSync); + if (!humanPrefabCollections.Any()) { return Array.Empty(); } + return humanPrefabCollections.GetRandom(randSync); } - public static void LoadPresets() - { - Params = new List(); - var files = GameMain.Instance.GetFilesOfType(ContentType.OutpostConfig); - foreach (ContentFile file in files) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc?.Root == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - Params.Clear(); - DebugConsole.NewMessage($"Overriding all outpost generation parameters with '{file.Path}'", Color.Yellow); - } - - foreach (XElement element in mainElement.Elements()) - { - bool isOverride = element.IsOverride(); - XElement sourceElement = isOverride ? element.FirstElement() : element; - string elementName = sourceElement.Name.ToString().ToLowerInvariant(); - string identifier = sourceElement.GetAttributeString("identifier", null); - - if (string.IsNullOrWhiteSpace(identifier)) - { - DebugConsole.ThrowError($"No identifier defined for the outpost config '{elementName}' in file '{file.Path}'"); - continue; - } - var existingParams = Params.Find(p => p.Identifier == identifier); - if (existingParams != null) - { - if (isOverride) - { - DebugConsole.NewMessage($"Overriding outpost config '{identifier}' using the file '{file.Path}'", Color.Yellow); - Params.Remove(existingParams); - } - else - { - DebugConsole.ThrowError($"Duplicate outpost config: '{identifier}' defined in {elementName} of '{file.Path}'"); - continue; - } - } - Params.Add(new OutpostGenerationParams(element, file.Path)); - } - } - } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 39b5f6bf1..8a92048f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -3,7 +3,9 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Security.Cryptography; namespace Barotrauma { @@ -26,7 +28,7 @@ namespace Barotrauma public OutpostModuleInfo.GapPosition UsedGapPositions = 0; - public readonly HashSet FulfilledModuleTypes = new HashSet(); + public readonly HashSet FulfilledModuleTypes = new HashSet(); public Vector2 Offset; @@ -67,10 +69,18 @@ namespace Barotrauma private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) { - var outpostModuleFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.OutpostModule); + var outpostModuleFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToArray(); + var uintIdDupes = outpostModuleFiles.Where(f1 => + outpostModuleFiles.Any(f2 => f1 != f2 && f1.UintIdentifier == f2.UintIdentifier)).ToArray(); + if (uintIdDupes.Any()) + { + throw new Exception($"OutpostModuleFile UintIdentifier duplicates found: {uintIdDupes.Select(f => f.Path)}"); + } if (location != null) { - if (location.IsCriticallyRadiated() && OutpostGenerationParams.Params.FirstOrDefault(p => p.Identifier.Equals(generationParams.ReplaceInRadiation, StringComparison.OrdinalIgnoreCase)) is { } newParams) + if (location.IsCriticallyRadiated() && OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.Identifier == generationParams.ReplaceInRadiation) is { } newParams) { generationParams = newParams; } @@ -80,21 +90,21 @@ namespace Barotrauma //load the infos of the outpost module files List outpostModules = new List(); - foreach (ContentFile outpostModuleFile in outpostModuleFiles) + foreach (var outpostModuleFile in outpostModuleFiles) { - var subInfo = new SubmarineInfo(outpostModuleFile.Path); + var subInfo = new SubmarineInfo(outpostModuleFile.Path.Value); if (subInfo.OutpostModuleInfo != null) { if (generationParams is RuinGeneration.RuinGenerationParams) { //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") && + if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()) && !generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key))) { continue; } } - else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin")) + else if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier())) { continue; } @@ -131,14 +141,23 @@ namespace Barotrauma selectedModules.Clear(); //select which module types the outpost should consist of - var pendingModuleFlags = onlyEntrance ? new List() { generationParams.ModuleCounts.First().Key } : SelectModules(outpostModules, generationParams); - foreach (string flag in pendingModuleFlags.Distinct().ToList()) + List pendingModuleFlags; + using (var md5 = MD5.Create()) { - if (flag.Equals("none", StringComparison.OrdinalIgnoreCase)) { continue; } + #warning TODO: cursed + pendingModuleFlags = onlyEntrance + ? generationParams.ModuleCounts + .Keys.OrderBy(k => ToolBox.IdentifierToUint32Hash(k, md5)) + .First().ToEnumerable().ToList() + : SelectModules(outpostModules, generationParams); + } + foreach (Identifier flag in pendingModuleFlags) + { + if (flag == "none") { continue; } int pendingCount = pendingModuleFlags.Count(f => f == flag); int availableModuleCount = outpostModules - .Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f.Equals(flag, StringComparison.OrdinalIgnoreCase))) + .Where(m => m.OutpostModuleInfo.ModuleFlags.Any(f => f == flag)) .Select(m => m.OutpostModuleInfo.MaxCount) .DefaultIfEmpty(0) .Sum(); @@ -154,7 +173,7 @@ namespace Barotrauma } //the first module is spawned separately, remove it from the list of pending modules - string initialModuleFlag = pendingModuleFlags.FirstOrDefault() ?? "airlock"; + Identifier initialModuleFlag = pendingModuleFlags.FirstOrDefault().IfEmpty("airlock".ToIdentifier()); pendingModuleFlags.Remove(initialModuleFlag); var initialModule = GetRandomModule(outpostModules, initialModuleFlag, locationType); @@ -162,9 +181,9 @@ namespace Barotrauma { throw new Exception("Failed to generate an outpost (no airlock modules found)."); } - foreach (string initialFlag in initialModule.OutpostModuleInfo.ModuleFlags) + foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags) { - if (pendingModuleFlags.Contains("initialFlag")) { pendingModuleFlags.Remove(initialFlag); } + if (pendingModuleFlags.Contains("initialFlag".ToIdentifier())) { pendingModuleFlags.Remove(initialFlag); } } if (remainingTries == 1) @@ -176,7 +195,7 @@ namespace Barotrauma selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); - if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase))) + if (pendingModuleFlags.Any(flag => flag != "none")) { remainingTries--; if (remainingTries <= 0) @@ -196,7 +215,7 @@ namespace Barotrauma sub.Info.OutpostGenerationParams = generationParams; if (!generationFailed) { - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != sub) { continue; } if (string.IsNullOrEmpty(hull.RoomName) || @@ -223,12 +242,14 @@ namespace Barotrauma DebugConsole.NewMessage("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead..."); - var outpostFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Outpost); + var outpostFiles = ContentPackageManager.EnabledPackages.All + .SelectMany(p => p.GetFiles()) + .OrderBy(f => f.UintIdentifier).ToArray(); if (!outpostFiles.Any()) { throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available."); } - var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.Server).Path) + var prebuiltOutpostInfo = new SubmarineInfo(outpostFiles.GetRandom(Rand.RandSync.ServerAndClient).Path.Value) { Type = SubmarineType.Outpost }; @@ -386,7 +407,7 @@ namespace Barotrauma } else { - hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.Server) * 0.01f; + hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.ServerAndClient) * 0.01f; } } } @@ -400,13 +421,13 @@ namespace Barotrauma /// /// Select the number and types of the modules to use in the outpost /// - private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) + private static List SelectModules(IEnumerable modules, OutpostGenerationParams generationParams) { int totalModuleCount = generationParams.TotalModuleCount; - var pendingModuleFlags = new List(); + var pendingModuleFlags = new List(); bool availableModulesFound = true; - string initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; + Identifier initialModuleFlag = generationParams.ModuleCounts.FirstOrDefault().Key; pendingModuleFlags.Add(initialModuleFlag); while (pendingModuleFlags.Count < totalModuleCount && availableModulesFound) { @@ -426,13 +447,17 @@ namespace Barotrauma pendingModuleFlags.Add(moduleFlag.Key); } } - pendingModuleFlags.Shuffle(Rand.RandSync.Server); + 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); while (pendingModuleFlags.Count < totalModuleCount) { //don't place "none" modules at the end because // a. "filler rooms" at the end of a hallway are pointless // b. placing the unnecessary filler rooms first give more options for the placement of the more important modules - pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.Server), "none"); + pendingModuleFlags.Insert(Rand.Int(pendingModuleFlags.Count - 1, Rand.RandSync.ServerAndClient), "none".ToIdentifier()); } //make sure the initial module is inserted first @@ -452,7 +477,7 @@ namespace Barotrauma /// The modules we've already selected to be used in the outpost. private static bool AppendToModule(PlacedModule currentModule, List availableModules, - List pendingModuleFlags, + List pendingModuleFlags, List selectedModules, LocationType locationType, bool retry = true, @@ -461,7 +486,7 @@ namespace Barotrauma if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server)) + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } if (!allowExtendBelowInitialModule) @@ -526,15 +551,15 @@ namespace Barotrauma PlacedModule currentModule, OutpostModuleInfo.GapPosition gapPosition, List availableModules, - List pendingModuleFlags, + List pendingModuleFlags, List selectedModules, LocationType locationType) { if (pendingModuleFlags.Count == 0) { return null; } - string flagToPlace = "none"; + Identifier flagToPlace = "none".ToIdentifier(); SubmarineInfo nextModule = null; - foreach (string moduleFlag in pendingModuleFlags) + foreach (Identifier moduleFlag in pendingModuleFlags) { flagToPlace = moduleFlag; nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType); @@ -547,7 +572,7 @@ namespace Barotrauma { Offset = currentModule.Offset + GetMoveDir(gapPosition), }; - foreach (string moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags) + foreach (Identifier moduleFlag in nextModule.OutpostModuleInfo.ModuleFlags) { if (!pendingModuleFlags.Contains(moduleFlag)) { continue; } if (moduleFlag != "none" || flagToPlace == "none") @@ -711,19 +736,19 @@ namespace Barotrauma return solutionFound; } - private static SubmarineInfo GetRandomModule(IEnumerable modules, string moduleFlag, LocationType locationType) + private static SubmarineInfo GetRandomModule(IEnumerable modules, Identifier moduleFlag, LocationType locationType) { IEnumerable availableModules = null; - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag.Equals("none")) + if (moduleFlag.IsEmpty || moduleFlag == "none") { - availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none")); + availableModules = modules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())); } else { availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical") { - availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal") && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical")); + availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal".ToIdentifier()) && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical".ToIdentifier())); } } @@ -731,7 +756,7 @@ namespace Barotrauma //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type if (!modulesSuitableForLocationType.Any()) @@ -742,21 +767,21 @@ namespace Barotrauma if (!modulesSuitableForLocationType.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.Server); + return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server); + return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } } - private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, string moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) + private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) { IEnumerable availableModules = null; - if (string.IsNullOrEmpty(moduleFlag) || moduleFlag.Equals("none")) + if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { availableModules = modules - .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none")) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); } else { @@ -772,7 +797,7 @@ namespace Barotrauma //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type if (!modulesSuitableForLocationType.Any()) @@ -783,11 +808,11 @@ namespace Barotrauma if (!modulesSuitableForLocationType.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.Server); + return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } else { - return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.Server); + return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient); } } @@ -807,13 +832,13 @@ namespace Barotrauma } } - private static IEnumerable GapPositions() + private readonly static OutpostModuleInfo.GapPosition[] GapPositions = new[] { - yield return OutpostModuleInfo.GapPosition.Right; - yield return OutpostModuleInfo.GapPosition.Left; - yield return OutpostModuleInfo.GapPosition.Top; - yield return OutpostModuleInfo.GapPosition.Bottom; - } + OutpostModuleInfo.GapPosition.Right, + OutpostModuleInfo.GapPosition.Left, + OutpostModuleInfo.GapPosition.Top, + OutpostModuleInfo.GapPosition.Bottom + }; private static OutpostModuleInfo.GapPosition GetOpposingGapPosition(OutpostModuleInfo.GapPosition thisGapPosition) { @@ -824,7 +849,7 @@ namespace Barotrauma OutpostModuleInfo.GapPosition.Bottom => OutpostModuleInfo.GapPosition.Top, OutpostModuleInfo.GapPosition.Top => OutpostModuleInfo.GapPosition.Bottom, OutpostModuleInfo.GapPosition.None => OutpostModuleInfo.GapPosition.None, - _ => throw new InvalidOperationException() + _ => throw new ArgumentException() }; } @@ -837,7 +862,7 @@ namespace Barotrauma OutpostModuleInfo.GapPosition.Bottom => Vector2.UnitY, OutpostModuleInfo.GapPosition.Top => -Vector2.UnitY, OutpostModuleInfo.GapPosition.None => Vector2.Zero, - _ => throw new InvalidOperationException() + _ => throw new ArgumentException() }; } @@ -885,7 +910,7 @@ namespace Barotrauma private static bool CanAttachTo(OutpostModuleInfo from, OutpostModuleInfo to) { - if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) { return true; } + if (!from.AllowAttachToModules.Any() || from.AllowAttachToModules.All(s => s == "any")) { return true; } return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s)); } @@ -915,7 +940,7 @@ namespace Barotrauma { for (int i = 0; i < thisJunctionBox.Connections.Count && i < previousJunctionBox.Connections.Count; i++) { - var wirePrefab = MapEntityPrefab.Find(name: null, identifier: thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire") as ItemPrefab; + var wirePrefab = MapEntityPrefab.FindByIdentifier((thisJunctionBox.Connections[i].IsPower ? "redwire" : "bluewire").ToIdentifier()) as ItemPrefab; var wire = new Item(wirePrefab, thisJunctionBox.Item.Position, sub).GetComponent(); if (!thisJunctionBox.Connections[i].TryAddLink(wire)) @@ -1021,9 +1046,9 @@ namespace Barotrauma { suitableModules = availableModules.Where(m => !m.OutpostModuleInfo.AllowAttachToModules.Any() || - m.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))); + m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")); } - var hallwayInfo = GetRandomModule(suitableModules, isHorizontal ? "hallwayhorizontal" : "hallwayvertical", locationType); + var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType); if (hallwayInfo == null) { DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\"."); @@ -1442,50 +1467,47 @@ namespace Barotrauma if (outpost?.Info?.OutpostGenerationParams == null) { return; } List killedCharacters = new List(); - Dictionary selectedCharacters = new Dictionary(); - foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.Server)) + List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters + = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>(); + foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient)) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { killedCharacters.Add(humanPrefab); continue; } - selectedCharacters.Add(humanPrefab, characterInfo); + selectedCharacters.Add((humanPrefab, characterInfo)); } //replace killed characters with new ones foreach (HumanPrefab killedCharacter in killedCharacters) { - int tries = 0; - while (tries < 100) + for (int tries = 0; tries < 100; tries++) { - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: killedCharacter.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); if (!location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { - selectedCharacters.Add(killedCharacter, characterInfo); + selectedCharacters.Add((killedCharacter, characterInfo)); break; } } } - foreach (var selectedCharacter in selectedCharacters) + foreach ((var humanPrefab, var characterInfo) in selectedCharacters) { - HumanPrefab humanPrefab = selectedCharacter.Key; - CharacterInfo characterInfo = selectedCharacter.Value; - Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name)); ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags()); if (gotoTarget == null) { - gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.Server); + gotoTarget = outpost.GetHulls(true).GetRandom(Rand.RandSync.ServerAndClient); } characterInfo.TeamID = CharacterTeamType.FriendlyNPC; - var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + var npc = Character.Create(CharacterPrefab.HumanSpeciesName, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); npc.AnimController.FindHull(gotoTarget.WorldPosition, setSubmarine: true); npc.TeamID = CharacterTeamType.FriendlyNPC; - npc.Prefab = humanPrefab; + npc.HumanPrefab = humanPrefab; if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) { outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); @@ -1499,7 +1521,7 @@ namespace Barotrauma { npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier); } - humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server); + humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient); foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { item.AllowStealing = outpost.Info.OutpostGenerationParams.AllowStealing; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index c0c051586..c79a10a64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -18,46 +18,46 @@ namespace Barotrauma Bottom = 8 } - private readonly HashSet moduleFlags = new HashSet(); - public IEnumerable ModuleFlags + private readonly HashSet moduleFlags = new HashSet(); + public IEnumerable ModuleFlags { get { return moduleFlags; } } - private readonly HashSet allowAttachToModules = new HashSet(); - public IEnumerable AllowAttachToModules + private readonly HashSet allowAttachToModules = new HashSet(); + public IEnumerable AllowAttachToModules { get { return allowAttachToModules; } } - private readonly HashSet allowedLocationTypes = new HashSet(); - public IEnumerable AllowedLocationTypes + private readonly HashSet allowedLocationTypes = new HashSet(); + public IEnumerable AllowedLocationTypes { get { return allowedLocationTypes; } } - [Serialize(100, isSaveable: true, description: "How many instances of this module can be used in one outpost."), Editable] + [Serialize(100, IsPropertySaveable.Yes, description: "How many instances of this module can be used in one outpost."), Editable] public int MaxCount { get; set; } - [Serialize(10.0f, isSaveable: true, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable] + [Serialize(10.0f, IsPropertySaveable.Yes, description: "How likely it is for the module to get picked when selecting from a set of modules during the outpost generation."), Editable] public float Commonness { get; set; } - [Serialize(GapPosition.None, isSaveable: true, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] + [Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] public GapPosition GapPositions { get; set; } public string Name { get; private set; } - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } public OutpostModuleInfo(SubmarineInfo submarineInfo, XElement element) { Name = $"OutpostModuleInfo ({submarineInfo.Name})"; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); SetFlags( - element.GetAttributeStringArray("flags", null, convertToLowerInvariant: true) ?? - element.GetAttributeStringArray("moduletypes", new string[0], convertToLowerInvariant: true)); - SetAllowAttachTo(element.GetAttributeStringArray("allowattachto", new string[0], convertToLowerInvariant: true)); - allowedLocationTypes = new HashSet(element.GetAttributeStringArray("allowedlocationtypes", new string[0], convertToLowerInvariant: true)); + element.GetAttributeIdentifierArray("flags", null) ?? + element.GetAttributeIdentifierArray("moduletypes", Array.Empty())); + SetAllowAttachTo(element.GetAttributeIdentifierArray("allowattachto", Array.Empty())); + allowedLocationTypes = new HashSet(element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty())); } public OutpostModuleInfo(SubmarineInfo submarineInfo) @@ -68,12 +68,12 @@ namespace Barotrauma public OutpostModuleInfo(OutpostModuleInfo original) { Name = original.Name; - moduleFlags = new HashSet(original.moduleFlags); - allowAttachToModules = new HashSet(original.allowAttachToModules); - allowedLocationTypes = new HashSet(original.allowedLocationTypes); - SerializableProperties = new Dictionary(); + moduleFlags = new HashSet(original.moduleFlags); + allowAttachToModules = new HashSet(original.allowAttachToModules); + allowedLocationTypes = new HashSet(original.allowedLocationTypes); + SerializableProperties = new Dictionary(); GapPositions = original.GapPositions; - foreach (KeyValuePair kvp in original.SerializableProperties) + foreach (KeyValuePair kvp in original.SerializableProperties) { SerializableProperties.Add(kvp.Key, kvp.Value); if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null) @@ -83,51 +83,51 @@ namespace Barotrauma } } - public void SetFlags(IEnumerable newFlags) + public void SetFlags(IEnumerable newFlags) { moduleFlags.Clear(); - if (newFlags.Contains("hallwayhorizontal")) + if (newFlags.Contains("hallwayhorizontal".ToIdentifier())) { - moduleFlags.Add("hallwayhorizontal"); - if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } + moduleFlags.Add("hallwayhorizontal".ToIdentifier()); + if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } return; } - if (newFlags.Contains("hallwayvertical")) + if (newFlags.Contains("hallwayvertical".ToIdentifier())) { - moduleFlags.Add("hallwayvertical"); - if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } + moduleFlags.Add("hallwayvertical".ToIdentifier()); + if (newFlags.Contains("ruin".ToIdentifier())) { moduleFlags.Add("ruin".ToIdentifier()); } return; } if (!newFlags.Any()) { - moduleFlags.Add("none"); + moduleFlags.Add("none".ToIdentifier()); } - foreach (string flag in newFlags) + foreach (Identifier flag in newFlags) { if (flag == "none" && newFlags.Count() > 1) { continue; } - moduleFlags.Add(flag.ToLowerInvariant()); + moduleFlags.Add(flag); } } - public void SetAllowAttachTo(IEnumerable allowAttachTo) + public void SetAllowAttachTo(IEnumerable allowAttachTo) { allowAttachToModules.Clear(); if (!allowAttachTo.Any()) { - allowAttachToModules.Add("any"); + allowAttachToModules.Add("any".ToIdentifier()); } - foreach (string flag in allowAttachTo) + foreach (Identifier flag in allowAttachTo) { if (flag == "any" && allowAttachTo.Count() > 1) { continue; } allowAttachToModules.Add(flag); } } - public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) + public void SetAllowedLocationTypes(IEnumerable allowedLocationTypes) { this.allowedLocationTypes.Clear(); - foreach (string locationType in allowedLocationTypes) + foreach (Identifier locationType in allowedLocationTypes) { - if (locationType.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; } + if (locationType == "any") { continue; } this.allowedLocationTypes.Add(locationType); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 25dff440e..0037d0a9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -28,6 +28,7 @@ namespace Barotrauma /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. /// public readonly float BuyingPriceMultiplier = 1f; + public bool DisplayNonEmpty { get; } = false; /// @@ -48,7 +49,7 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f) + public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, bool displayNonEmpty = false) { Price = price; CanBeBought = canBeBought; @@ -58,38 +59,41 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, minAmount); MinLevelDifficulty = minLevelDifficulty; CanBeSpecial = canBeSpecial; + DisplayNonEmpty = displayNonEmpty; } - public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) + public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { defaultPrice = null; - var basePrice = element.GetAttributeInt("baseprice", 0); - var soldByDefault = element.GetAttributeBool("soldbydefault", true); - var minAmount = GetMinAmount(element); - var maxAmount = GetMaxAmount(element); - var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); - var canBeSpecial = element.GetAttributeBool("canbespecial", true); - var buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); - var priceInfos = new List>(); + int basePrice = element.GetAttributeInt("baseprice", 0); + bool soldByDefault = element.GetAttributeBool("soldbydefault", true); + int minAmount = GetMinAmount(element); + int maxAmount = GetMaxAmount(element); + int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); + bool canBeSpecial = element.GetAttributeBool("canbespecial", true); + float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); + bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); + var priceInfos = new List>(); foreach (XElement childElement in element.GetChildElements("price")) { - var priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); - var sold = childElement.GetAttributeBool("sold", soldByDefault); - priceInfos.Add(new Tuple(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(), - new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold, - minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, - maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, + float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); + bool sold = childElement.GetAttributeBool("sold", soldByDefault); + priceInfos.Add(new Tuple(childElement.GetAttributeIdentifier("locationtype", ""), + new PriceInfo((int)(priceMultiplier * basePrice), sold, + sold ? GetMinAmount(childElement, minAmount) : 0, + sold ? GetMaxAmount(childElement, maxAmount) : 0, canBeSpecial, - childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier)))); + childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), + childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier), + displayNonEmpty))); } - var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); + bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations, - minAmount: canBeBoughtAtOtherLocations ? minAmount : 0, - maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0, - canBeSpecial, - minLevelDifficulty, buyingPriceMultiplier); + canBeBoughtAtOtherLocations ? minAmount : 0, + canBeBoughtAtOtherLocations ? maxAmount : 0, + canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index c68763505..7556b281a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using System.Collections.Immutable; using Barotrauma.Abilities; #if CLIENT using Microsoft.Xna.Framework.Graphics; @@ -56,9 +57,9 @@ namespace Barotrauma private readonly List bodyDebugDimensions = new List(); #if DEBUG - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] #else - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] #endif public bool Indestructible { @@ -75,7 +76,7 @@ namespace Barotrauma public override Sprite Sprite { - get { return prefab.sprite; } + get { return base.Prefab.Sprite; } } public bool IsPlatform @@ -91,7 +92,7 @@ namespace Barotrauma public override string Name { - get { return prefab.Name; } + get { return base.Prefab.Name.Value; } } public bool HasBody @@ -115,7 +116,7 @@ namespace Barotrauma private float? maxHealth; - [Serialize(100.0f, true), Editable(MinValueFloat = 0)] + [Serialize(100.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0)] public float MaxHealth { get => maxHealth ?? Prefab.Health; @@ -124,7 +125,7 @@ namespace Barotrauma private float crushDepth; - [Serialize(Level.DefaultRealWorldCrushDepth, true)] + [Serialize(Level.DefaultRealWorldCrushDepth, IsPropertySaveable.Yes)] public float CrushDepth { get => crushDepth; @@ -163,29 +164,26 @@ namespace Barotrauma private set; } - public StructurePrefab Prefab => prefab as StructurePrefab; + public new StructurePrefab Prefab => base.Prefab as StructurePrefab; - public HashSet Tags - { - get { return prefab.Tags; } - } + public ImmutableHashSet Tags => Prefab.Tags; protected Color spriteColor; - [Editable, Serialize("1.0,1.0,1.0,1.0", true)] + [Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes)] public Color SpriteColor { get { return spriteColor; } set { spriteColor = value; } } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool UseDropShadow { get; private set; } - [Editable, Serialize("0,0", true, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")] + [Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")] public Vector2 DropShadowOffset { get; @@ -201,7 +199,7 @@ namespace Barotrauma if (scale == value) { return; } scale = MathHelper.Clamp(value, 0.1f, 10.0f); - float relativeScale = scale / prefab.Scale; + float relativeScale = scale / base.Prefab.Scale; if (!ResizeHorizontal || !ResizeVertical) { @@ -230,7 +228,7 @@ namespace Barotrauma protected Vector2 textureScale = Vector2.One; - [Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", false)] + [Editable(DecimalCount = 3, MinValueFloat = 0.01f, MaxValueFloat = 10f, ValueStep = 0.1f), Serialize("1.0, 1.0", IsPropertySaveable.No)] public Vector2 TextureScale { get { return textureScale; } @@ -250,7 +248,7 @@ namespace Barotrauma } protected Vector2 textureOffset = Vector2.Zero; - [Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", true)] + [Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", IsPropertySaveable.Yes)] public Vector2 TextureOffset { get { return textureOffset; } @@ -343,14 +341,14 @@ namespace Barotrauma } } - [Serialize(false, true), Editable] + [Serialize(false, IsPropertySaveable.Yes), Editable] public bool NoAITarget { get; private set; } - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -406,7 +404,7 @@ namespace Barotrauma rect = rectangle; TextureScale = sp.TextureScale; - spriteColor = prefab.SpriteColor; + spriteColor = base.Prefab.SpriteColor; if (sp.IsHorizontal.HasValue) { IsHorizontal = sp.IsHorizontal.Value; @@ -461,7 +459,7 @@ namespace Barotrauma SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this); #if CLIENT - foreach (XElement subElement in sp.ConfigElement.Elements()) + foreach (var subElement in sp.ConfigElement.Elements()) { if (subElement.Name.ToString().Equals("light", StringComparison.OrdinalIgnoreCase)) { @@ -524,7 +522,7 @@ namespace Barotrauma { defaultRect = defaultRect }; - foreach (KeyValuePair property in SerializableProperties) + foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) { continue; } clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); @@ -572,13 +570,13 @@ namespace Barotrauma { if (FlippedX && IsHorizontal) { - xsections = (int)Math.Ceiling((float)rect.Width / prefab.sprite.SourceRect.Width); - width = prefab.sprite.SourceRect.Width; + xsections = (int)Math.Ceiling((float)rect.Width / base.Prefab.Sprite.SourceRect.Width); + width = base.Prefab.Sprite.SourceRect.Width; } else if (FlippedY && !IsHorizontal) { - ysections = (int)Math.Ceiling((float)rect.Height / prefab.sprite.SourceRect.Height); - width = prefab.sprite.SourceRect.Height; + ysections = (int)Math.Ceiling((float)rect.Height / base.Prefab.Sprite.SourceRect.Height); + width = base.Prefab.Sprite.SourceRect.Height; } else { @@ -1184,7 +1182,7 @@ namespace Barotrauma { if (damageDiff < 0.0f) { - attacker.Info?.IncreaseSkillLevel("mechanical", + attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(), -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f)); } } @@ -1374,10 +1372,10 @@ namespace Barotrauma } } - public static Structure Load(XElement element, Submarine submarine, IdRemap idRemap) + public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { string name = element.Attribute("name").Value; - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = FindPrefab(name, identifier); if (prefab == null) @@ -1398,7 +1396,7 @@ namespace Barotrauma } bool hasDamage = false; - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -1421,7 +1419,7 @@ namespace Barotrauma break; case "upgrade": { - var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty); + var upgradeIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier); int level = subElement.GetAttributeInt("level", 1); if (upgradePrefab != null) @@ -1460,18 +1458,18 @@ namespace Barotrauma return s; } - public static StructurePrefab FindPrefab(string name, string identifier) + public static StructurePrefab FindPrefab(string name, Identifier identifier) { StructurePrefab prefab = null; - if (string.IsNullOrEmpty(identifier)) + if (identifier.IsEmpty) { //legacy support: //1. attempt to find a prefab with an empty identifier and a matching name prefab = MapEntityPrefab.Find(name, "") as StructurePrefab; //2. not found, attempt to find a prefab with a matching name - if (prefab == null) prefab = MapEntityPrefab.Find(name) as StructurePrefab; + if (prefab == null) { prefab = MapEntityPrefab.Find(name) as StructurePrefab; } //3. not found, attempt to find a prefab that uses the previous name as an identifier - if (prefab == null) prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; + if (prefab == null) { prefab = MapEntityPrefab.Find(null, name) as StructurePrefab; } } else { @@ -1488,8 +1486,8 @@ namespace Barotrauma int height = ResizeVertical ? rect.Height : defaultRect.Height; element.Add( - new XAttribute("name", prefab.Name), - new XAttribute("identifier", prefab.Identifier), + new XAttribute("name", base.Prefab.Name), + new XAttribute("identifier", base.Prefab.Identifier), new XAttribute("ID", ID), new XAttribute("rect", (int)(rect.X - Submarine.HiddenSubPosition.X) + "," + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 805bc42b8..91da0bd7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.IO; +using System.Collections.Immutable; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -15,169 +16,98 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); - private bool disposed = false; - public override void Dispose() - { - if (disposed) { return; } - disposed = true; - Prefabs.Remove(this); - } + public override LocalizedString Name { get; } - private string name; - public override string Name - { - get { return name; } - } + public readonly ContentXElement ConfigElement; - public XElement ConfigElement { get; private set; } - - private bool canSpriteFlipX, canSpriteFlipY; - - private float health; - - //default size - private Vector2 size; - - //does the structure have a physics body - [Serialize(false, false)] - public bool Body - { - get; - private set; - } - - //rotation of the physics body in degrees - [Serialize(0.0f, false)] - public float BodyRotation - { - get; - private set; - } - - //in display units - [Serialize(0.0f, false)] - public float BodyWidth - { - get; - private set; - } - - //in display units - [Serialize(0.0f, false)] - public float BodyHeight - { - get; - private set; - } - - //in display units - [Serialize("0.0,0.0", false)] - public Vector2 BodyOffset - { - get; - private set; - } - - [Serialize(false, false)] - public bool Platform - { - get; - private set; - } - - [Serialize(false, false)] - public bool AllowAttachItems - { - get; - private set; - } - - [Serialize(0.0f, false)] - public float MinHealth - { - get; - set; - } - - [Serialize(100.0f, false)] - public float Health - { - get { return health; } - set { health = Math.Max(value, MinHealth); } - } - - [Serialize(true, false)] - public bool IndestructibleInOutposts - { - get; - set; - } - - [Serialize(false, false)] - public bool CastShadow - { - get; - private set; - } + public readonly bool CanSpriteFlipX; + public readonly bool CanSpriteFlipY; /// /// If null, the orientation is determined automatically based on the dimensions of the structure instances /// - public bool? IsHorizontal + public readonly bool? IsHorizontal; + + public Vector2 ScaledSize => Size * Scale; + + public readonly Sprite BackgroundSprite; + + public override Sprite Sprite { get; } + + public override string OriginalName => Name.Value; + + public override ImmutableHashSet Tags { get; } + + public override ImmutableHashSet AllowedLinks { get; } + + public override MapEntityCategory Category { get; } + + public override ImmutableHashSet Aliases { get; } + + //does the structure have a physics body + [Serialize(false, IsPropertySaveable.No)] + public bool Body { get; private set; } + + //rotation of the physics body in degrees + [Serialize(0.0f, IsPropertySaveable.No)] + public float BodyRotation { get; private set; } + + //in display units + [Serialize(0.0f, IsPropertySaveable.No)] + public float BodyWidth { get; private set; } + + //in display units + [Serialize(0.0f, IsPropertySaveable.No)] + public float BodyHeight { get; private set; } + + //in display units + [Serialize("0.0,0.0", IsPropertySaveable.No)] + public Vector2 BodyOffset { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool Platform { get; private set; } + + [Serialize(false, IsPropertySaveable.No)] + public bool AllowAttachItems { get; private set; } + + [Serialize(0.0f, IsPropertySaveable.No)] + public float MinHealth { get; private set; } + + private float health; + [Serialize(100.0f, IsPropertySaveable.No)] + public float Health { - get; - private set; + get { return health; } + private set { health = Math.Max(value, MinHealth); } } - [Serialize(Direction.None, false)] - public Direction StairDirection - { - get; - private set; - } + [Serialize(true, IsPropertySaveable.No)] + public bool IndestructibleInOutposts { get; private set; } - [Serialize(45.0f, false)] - public float StairAngle - { - get; - private set; - } + [Serialize(false, IsPropertySaveable.No)] + public bool CastShadow { get; private set; } - [Serialize(false, false)] - public bool NoAITarget - { - get; - private set; - } + [Serialize(Direction.None, IsPropertySaveable.No)] + public Direction StairDirection { get; private set; } - public bool CanSpriteFlipX - { - get { return canSpriteFlipX; } - } + [Serialize(45.0f, IsPropertySaveable.No)] + public float StairAngle { get; private set; } - public bool CanSpriteFlipY - { - get { return canSpriteFlipY; } - } + [Serialize(false, IsPropertySaveable.No)] + public bool NoAITarget { get; private set; } - [Serialize("0,0", true)] - public Vector2 Size - { - get { return size; } - private set { size = value; } - } + [Serialize("0,0", IsPropertySaveable.Yes)] + public Vector2 Size { get; private set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string DamageSound { get; private set; } - public Vector2 ScaledSize => size * Scale; - protected Vector2 textureScale = Vector2.One; - [Editable(DecimalCount = 3), Serialize("1.0, 1.0", true)] + [Editable(DecimalCount = 3), Serialize("1.0, 1.0", IsPropertySaveable.Yes)] public Vector2 TextureScale { get { return textureScale; } - set + private set { textureScale = new Vector2( MathHelper.Clamp(value.X, 0.01f, 10), @@ -185,155 +115,112 @@ namespace Barotrauma } } - public Sprite BackgroundSprite + protected override Identifier DetermineIdentifier(XElement element) { - get; - private set; - } - - public static void LoadAll(IEnumerable files) - { - foreach (ContentFile file in files) + Identifier identifier = base.DetermineIdentifier(element); + string originalName = element.GetAttributeString("name", ""); + if (identifier.IsEmpty && !string.IsNullOrEmpty(originalName)) { - LoadFromFile(file); - } - } - - public static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - if (doc == null) { return; } - var rootElement = doc.Root; - if (rootElement.IsOverride()) - { - foreach (var element in rootElement.Elements()) + string categoryStr = element.GetAttributeString("category", "Misc"); + if (Enum.TryParse(categoryStr, true, out MapEntityCategory category) && category.HasFlag(MapEntityCategory.Legacy)) { - foreach (var childElement in element.Elements()) - { - Load(childElement, true, file); - } - } - } - else - { - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - foreach (var childElement in element.Elements()) - { - Load(childElement, true, file); - } - } - else - { - Load(element, false, file); - } + identifier = $"legacystructure_{originalName.Replace(" ", "")}".ToIdentifier(); } } + return identifier; } - public static void RemoveByFile(string filePath) + public StructurePrefab(ContentXElement element, StructureFile file) : base(element, file) { - Prefabs.RemoveByFile(filePath); - } + Name = element.GetAttributeString("name", ""); + ConfigElement = element; - private static StructurePrefab Load(XElement element, bool allowOverride, ContentFile file) - { - StructurePrefab sp = new StructurePrefab - { - originalName = element.GetAttributeString("name", ""), - FilePath = file.Path, - ContentPackage = file.ContentPackage - }; - sp.name = sp.originalName; - sp.ConfigElement = element; - sp.identifier = element.GetAttributeString("identifier", ""); - - var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty; - - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + var parentType = element.Parent?.GetAttributeIdentifier("prefabtype", Identifier.Empty) ?? Identifier.Empty; + + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); //only used if the item doesn't have a name/description defined in the currently selected language - string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); + Identifier fallbackNameIdentifier = element.GetAttributeIdentifier("fallbacknameidentifier", ""); - string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); + Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - if (string.IsNullOrEmpty(sp.originalName)) + if (Name.IsNullOrEmpty()) { - if (string.IsNullOrEmpty(nameIdentifier)) + Name = TextManager.Get($"EntityName.{Identifier}"); + if (!nameIdentifier.IsEmpty) { - sp.name = TextManager.Get("EntityName." + sp.identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; + Name = TextManager.Get($"EntityName.{nameIdentifier}").Fallback(Name); } - else + + if (!fallbackNameIdentifier.IsEmpty) { - sp.name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; + Name = Name.Fallback(TextManager.Get($"EntityName.{fallbackNameIdentifier}")); } } - if (string.IsNullOrEmpty(sp.name)) - { - sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})"; - } - sp.Tags = new HashSet(); + var tags = new HashSet(); string joinedTags = element.GetAttributeString("tags", ""); if (string.IsNullOrEmpty(joinedTags)) joinedTags = element.GetAttributeString("Tags", ""); foreach (string tag in joinedTags.Split(',')) { - sp.Tags.Add(tag.Trim().ToLowerInvariant()); + tags.Add(tag.Trim().ToIdentifier()); } if (element.Attribute("ishorizontal") != null) { - sp.IsHorizontal = element.GetAttributeBool("ishorizontal", false); + IsHorizontal = element.GetAttributeBool("ishorizontal", false); } - foreach (XElement subElement in element.Elements()) +#if CLIENT + var decorativeSprites = new List(); + var decorativeSpriteGroups = new Dictionary>(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString()) { case "sprite": - sp.sprite = new Sprite(subElement, lazyLoad: true); + Sprite = new Sprite(subElement, lazyLoad: true); if (subElement.Attribute("sourcerect") == null && subElement.Attribute("sheetindex") == null) { - DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!"); + DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!"); } #if CLIENT if (subElement.GetAttributeBool("fliphorizontal", false)) - sp.sprite.effects = SpriteEffects.FlipHorizontally; + Sprite.effects = SpriteEffects.FlipHorizontally; if (subElement.GetAttributeBool("flipvertical", false)) - sp.sprite.effects = SpriteEffects.FlipVertically; + Sprite.effects = SpriteEffects.FlipVertically; #endif - sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); - sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); + CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); + CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); - if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name)) + if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) { - sp.sprite.Name = sp.Name; + Sprite.Name = Name.Value; } - sp.sprite.EntityID = sp.identifier; + Sprite.EntityIdentifier = Identifier; break; case "backgroundsprite": - sp.BackgroundSprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && sp.sprite != null) + BackgroundSprite = new Sprite(subElement, lazyLoad: true); + if (subElement.Attribute("sourcerect") == null && Sprite != null) { - sp.BackgroundSprite.SourceRect = sp.sprite.SourceRect; - sp.BackgroundSprite.size = sp.sprite.size; - sp.BackgroundSprite.size.X *= sp.sprite.SourceRect.Width; - sp.BackgroundSprite.size.Y *= sp.sprite.SourceRect.Height; - sp.BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); + BackgroundSprite.SourceRect = Sprite.SourceRect; + BackgroundSprite.size = Sprite.size; + BackgroundSprite.size.X *= Sprite.SourceRect.Width; + BackgroundSprite.size.Y *= Sprite.SourceRect.Height; + BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); } #if CLIENT - if (subElement.GetAttributeBool("fliphorizontal", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; } - if (subElement.GetAttributeBool("flipvertical", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; } - sp.BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White); + if (subElement.GetAttributeBool("fliphorizontal", false)) { BackgroundSprite.effects = SpriteEffects.FlipHorizontally; } + if (subElement.GetAttributeBool("flipvertical", false)) { BackgroundSprite.effects = SpriteEffects.FlipVertically; } + BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White); #endif break; case "decorativesprite": #if CLIENT string decorativeSpriteFolder = ""; - if (!subElement.GetAttributeString("texture", "").Contains("/")) + if (subElement.DoesAttributeReferenceFileNameAlone("texture")) { decorativeSpriteFolder = Path.GetDirectoryName(file.Path); } @@ -347,101 +234,111 @@ namespace Barotrauma else { decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); - sp.DecorativeSprites.Add(decorativeSprite); + decorativeSprites.Add(decorativeSprite); groupID = decorativeSprite.RandomGroupID; } - if (!sp.DecorativeSpriteGroups.ContainsKey(groupID)) + if (!decorativeSpriteGroups.ContainsKey(groupID)) { - sp.DecorativeSpriteGroups.Add(groupID, new List()); + decorativeSpriteGroups.Add(groupID, new List()); } - sp.DecorativeSpriteGroups[groupID].Add(decorativeSprite); + decorativeSpriteGroups[groupID].Add(decorativeSprite); #endif break; } } - - if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) +#if CLIENT + DecorativeSprites = decorativeSprites.ToImmutableArray(); + DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); +#endif + + if (parentType == "wrecked") { - if (!string.IsNullOrEmpty(sp.Name)) + if (!Name.IsNullOrEmpty()) { - sp.name = TextManager.GetWithVariable("wreckeditemformat", "[name]", sp.name); + Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); } } string categoryStr = element.GetAttributeString("category", "Structure"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { - category = MapEntityCategory.Structure; + category = MapEntityCategory.Structure; } - sp.Category = category; + Category = category; - if (category.HasFlag(MapEntityCategory.Legacy)) - { - if (string.IsNullOrWhiteSpace(sp.identifier)) - { - sp.identifier = "legacystructure_" + sp.name.ToLowerInvariant().Replace(" ", ""); - } - } - - sp.Aliases = - (element.GetAttributeStringArray("aliases", null) ?? - element.GetAttributeStringArray("Aliases", new string[0])).ToHashSet(); + Aliases = + (element.GetAttributeStringArray("aliases", null, convertToLowerInvariant: true) ?? + element.GetAttributeStringArray("Aliases", Array.Empty(), convertToLowerInvariant: true)).ToImmutableHashSet(); string nonTranslatedName = element.GetAttributeString("name", null) ?? element.Name.ToString(); - sp.Aliases.Add(nonTranslatedName.ToLowerInvariant()); + Aliases.Add(nonTranslatedName.ToLowerInvariant()); - SerializableProperty.DeserializeProperties(sp, element); - if (sp.Body) + SerializableProperty.DeserializeProperties(this, element); + if (Body) { - sp.Tags.Add("wall"); + tags.Add("wall".ToIdentifier()); } - if (string.IsNullOrEmpty(sp.Description)) + if (Description.IsNullOrEmpty()) { - if (!string.IsNullOrEmpty(descriptionIdentifier)) + if (!descriptionIdentifier.IsEmpty) { - sp.Description = TextManager.Get("EntityDescription." + descriptionIdentifier, returnNull: true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}"); } - else if (string.IsNullOrEmpty(nameIdentifier)) + else if (nameIdentifier.IsEmpty) { - sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{Identifier}"); } else { - sp.Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; + Description = TextManager.Get($"EntityDescription.{nameIdentifier}"); } } //backwards compatibility if (element.Attribute("size") == null) { - sp.size = Vector2.Zero; + Size = Vector2.Zero; if (element.Attribute("width") == null && element.Attribute("height") == null) { - sp.size.X = sp.sprite.SourceRect.Width; - sp.size.Y = sp.sprite.SourceRect.Height; + Size = Sprite.SourceRect.Size.ToVector2(); } else { - sp.size.X = element.GetAttributeFloat("width", 0.0f); - sp.size.Y = element.GetAttributeFloat("height", 0.0f); + Size = new Vector2( + element.GetAttributeFloat("width", 0.0f), + element.GetAttributeFloat("height", 0.0f)); } } //backwards compatibility if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) { - sp.Category = MapEntityCategory.Wrecked; - sp.Subcategory = "Thalamus"; + Category = MapEntityCategory.Wrecked; + Subcategory = "Thalamus"; } - if (string.IsNullOrEmpty(sp.identifier)) + if (Identifier == Identifier.Empty) { DebugConsole.ThrowError( - "Structure prefab \"" + sp.name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); + "Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } - Prefabs.Add(sp, allowOverride); - return sp; + + Tags = tags.ToImmutableHashSet(); + AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); + } + + protected override void CreateInstance(Rectangle rect) + { + throw new NotImplementedException(); + } + + private bool disposed = false; + public override void Dispose() + { + if (disposed) { return; } + disposed = true; + Prefabs.Remove(this); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 6901ddf25..1ba321562 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -291,7 +291,7 @@ namespace Barotrauma public int CalculateBasePrice() { int minPrice = 1000; - float volume = Hull.hullList.Where(h => h.Submarine == this).Sum(h => h.Volume); + float volume = Hull.HullList.Where(h => h.Submarine == this).Sum(h => h.Volume); float itemValue = Item.ItemList.Where(it => it.Submarine == this).Sum(it => it.Prefab.GetMinPrice() ?? 0); float price = volume / 500.0f + itemValue / 100.0f; System.Diagnostics.Debug.Assert(price >= 0); @@ -300,7 +300,7 @@ namespace Barotrauma private float ballastFloraTimer; public bool ImmuneToBallastFlora { get; set; } - public void AttemptBallastFloraInfection(string identifier, float deltaTime, float probability) + public void AttemptBallastFloraInfection(Identifier identifier, float deltaTime, float probability) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (ImmuneToBallastFlora) { return; } @@ -557,7 +557,7 @@ namespace Barotrauma public Rectangle CalculateDimensions(bool onlyHulls = true) { List entities = onlyHulls ? - Hull.hullList.FindAll(h => h.Submarine == this).Cast().ToList() : + Hull.HullList.FindAll(h => h.Submarine == this).Cast().ToList() : MapEntity.mapEntityList.FindAll(me => me.Submarine == this); //ignore items whose body is disabled (wires, items inside cabinets) @@ -990,7 +990,7 @@ namespace Barotrauma { if (e.Submarine == this) { - Spawner.AddToRemoveQueue(e); + Spawner.AddEntityToRemoveQueue(e); } } @@ -1114,7 +1114,7 @@ namespace Barotrauma float waterVolume = 0.0f; float volume = 0.0f; float excessWater = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != this) { continue; } waterVolume += hull.WaterVolume; @@ -1212,7 +1212,7 @@ namespace Barotrauma /// public bool IsConnectedTo(Submarine otherSub) => this == otherSub || GetConnectedSubs().Contains(otherSub); - public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList); + public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.HullList); public List GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList); public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList); public List GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList); @@ -1285,7 +1285,7 @@ namespace Barotrauma if (element.Name != "Structure") { continue; } string name = element.GetAttributeString("name", ""); - string identifier = element.GetAttributeString("identifier", ""); + Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = Structure.FindPrefab(name, identifier); if (prefab == null || !prefab.Body) { continue; } @@ -1348,7 +1348,7 @@ namespace Barotrauma } Vector2 center = Vector2.Zero; - var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this); + var matchingHulls = Hull.HullList.FindAll(h => h.Submarine == this); if (matchingHulls.Any()) { @@ -1538,7 +1538,16 @@ namespace Barotrauma Rectangle dimensions = VisibleBorders; element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); var cargoContainers = GetCargoContainers(); - element.Add(new XAttribute("cargocapacity", cargoContainers.Sum(c => c.container.Capacity))); + int cargoCapacity = cargoContainers.Sum(c => c.container.Capacity); + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + { + cargoCapacity += linkedSub.CargoCapacity; + } + } + + 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 ?? "")); @@ -1654,7 +1663,7 @@ namespace Barotrauma Unloading = true; #if CLIENT - RemoveAllRoundSounds(); + RoundSound.RemoveAllRoundSounds(); GameMain.LightManager?.ClearLights(); #endif @@ -1697,6 +1706,8 @@ namespace Barotrauma GameMain.World?.Clear(); GameMain.World = null; + Powered.Grids.Clear(); + GC.Collect(); Unloading = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 499acf25d..3216589a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -118,7 +118,7 @@ namespace Barotrauma Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; Body farseerBody = null; - if (!Hull.hullList.Any(h => h.Submarine == sub)) + if (!Hull.HullList.Any(h => h.Submarine == sub)) { farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f); if (showWarningMessages) @@ -156,7 +156,7 @@ namespace Barotrauma } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != submarine || hull.IdFreed) { continue; } @@ -446,7 +446,7 @@ namespace Barotrauma { float waterVolume = 0.0f; float volume = 0.0f; - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != submarine) continue; @@ -850,7 +850,7 @@ namespace Barotrauma { errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server."; } - if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "SubmarineBody.ApplyImpact:InvalidImpulse", GameAnalyticsManager.ErrorSeverity.Error, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 1ec236c0a..cc6881f34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -31,10 +31,7 @@ namespace Barotrauma public const string SavePath = "Submarines"; private static List savedSubmarines = new List(); - public static IEnumerable SavedSubmarines - { - get { return savedSubmarines; } - } + public static IEnumerable SavedSubmarines => savedSubmarines; private Task hashTask; private Md5Hash hash; @@ -59,13 +56,13 @@ namespace Barotrauma set; } - public string DisplayName + public LocalizedString DisplayName { get; set; } - public string Description + public LocalizedString Description { get; set; @@ -170,7 +167,7 @@ namespace Barotrauma get { if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } - return RequiredContentPackages.All(cp => GameMain.Config.AllEnabledPackages.Any(cp2 => cp2.Name == cp)); + return RequiredContentPackages.All(reqName => ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.NameMatches(reqName))); } set { @@ -199,13 +196,14 @@ namespace Barotrauma public OutpostGenerationParams OutpostGenerationParams; - public readonly Dictionary> OutpostNPCs = new Dictionary>(); + public readonly Dictionary> OutpostNPCs = new Dictionary>(); //constructors & generation ---------------------------------------------------- public SubmarineInfo() { FilePath = null; - Name = DisplayName = TextManager.Get("UnspecifiedSubFileName"); + DisplayName = TextManager.Get("UnspecifiedSubFileName"); + Name = DisplayName.Value; IsFileCorrupted = false; RequiredContentPackages = new HashSet(); } @@ -219,7 +217,8 @@ namespace Barotrauma } try { - Name = DisplayName = Path.GetFileNameWithoutExtension(filePath); + DisplayName = Path.GetFileNameWithoutExtension(filePath); + Name = DisplayName.Value; } catch (Exception e) { @@ -228,7 +227,7 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(hash)) { - this.hash = new Md5Hash(hash); + this.hash = Md5Hash.StringAsHash(hash); } IsFileCorrupted = false; @@ -314,11 +313,9 @@ namespace Barotrauma private void Init() { - DisplayName = TextManager.Get("Submarine.Name." + Name, true); - if (string.IsNullOrEmpty(DisplayName)) { DisplayName = Name; } + DisplayName = TextManager.Get("Submarine.Name." + Name).Fallback(Name); - Description = TextManager.Get("Submarine.Description." + Name, true); - if (string.IsNullOrEmpty(Description)) { Description = SubmarineElement.GetAttributeString("description", ""); } + Description = TextManager.Get("Submarine.Description." + Name).Fallback(SubmarineElement.GetAttributeString("description", "")); EqualityCheckVal = SubmarineElement.GetAttributeInt("checkval", 0); @@ -379,7 +376,7 @@ namespace Barotrauma } RequiredContentPackages.Clear(); - string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", new string[0]); + string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty()); foreach (string contentPackageName in contentPackageNames) { RequiredContentPackages.Add(contentPackageName); @@ -405,14 +402,9 @@ namespace Barotrauma var vanilla = GameMain.VanillaContent; if (vanilla != null) { - var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine) - .Concat(vanilla.GetFilesOfType(ContentType.Wreck)) - .Concat(vanilla.GetFilesOfType(ContentType.BeaconStation)) - .Concat(vanilla.GetFilesOfType(ContentType.EnemySubmarine)) - .Concat(vanilla.GetFilesOfType(ContentType.Outpost)) - .Concat(vanilla.GetFilesOfType(ContentType.OutpostModule)); - string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant(); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) + var vanillaSubs = vanilla.GetFiles(); + string pathToCompare = FilePath.CleanUpPath(); + if (vanillaSubs.Any(sub => sub.Path == pathToCompare)) { return true; } @@ -427,7 +419,8 @@ namespace Barotrauma hashTask = new Task(() => { - hash = new Md5Hash(doc, FilePath); + hash = Md5Hash.CalculateForString(doc.ToString(), Md5Hash.StringHashOptions.IgnoreWhitespace); + Md5Hash.Cache.Add(FilePath, hash, DateTime.UtcNow); }); hashTask.Start(); } @@ -459,7 +452,7 @@ namespace Barotrauma LeftBehindSubDockingPortOccupied = false; LeftBehindDockingPortIDs.Clear(); BlockedDockingPortIDs.Clear(); - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; } if (subElement.Attribute("location") == null) { continue; } @@ -469,7 +462,7 @@ namespace Barotrauma LeftBehindDockingPortIDs.Add(targetDockingPortID); XElement targetPortElement = targetDockingPortID == 0 ? null : element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); - if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) + if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", Array.Empty()).Length > 0) { BlockedDockingPortIDs.Add(targetDockingPortID); LeftBehindSubDockingPortOccupied = true; @@ -488,7 +481,7 @@ namespace Barotrauma foreach (var structureElement in SubmarineElement.GetChildElements("structure")) { string name = structureElement.Attribute("name")?.Value ?? ""; - string identifier = structureElement.GetAttributeString("identifier", ""); + Identifier identifier = structureElement.GetAttributeIdentifier("identifier", ""); var structurePrefab = Structure.FindPrefab(name, identifier); if (structurePrefab == null || !structurePrefab.Body) { continue; } if (!structureCrushDepthsDefined && structureElement.Attribute("crushdepth") != null) @@ -546,7 +539,7 @@ namespace Barotrauma } SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.RemoveFromCache(filePath); + Md5Hash.Cache.Remove(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) @@ -578,10 +571,7 @@ namespace Barotrauma public static void RefreshSavedSubs() { - var contentPackageSubs = ContentPackage.GetFilesOfType( - GameMain.Config.AllEnabledPackages, - ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, - ContentType.Wreck, ContentType.BeaconStation, ContentType.EnemySubmarine); + var contentPackageSubs = ContentPackageManager.EnabledPackages.All.SelectMany(c => c.GetFiles()); for (int i = savedSubmarines.Count - 1; i >= 0; i--) { @@ -589,7 +579,7 @@ namespace Barotrauma { bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath); - bool isInContentPackage = contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()); + bool isInContentPackage = contentPackageSubs.Any(f => f.Path == savedSubmarines[i].FilePath); if (isDownloadedSub) { continue; } if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; } } @@ -640,11 +630,11 @@ namespace Barotrauma } } - foreach (ContentFile subFile in contentPackageSubs) + foreach (BaseSubFile subFile in contentPackageSubs) { - if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path))) + if (!filePaths.Any(fp => fp == subFile.Path)) { - filePaths.Add(subFile.Path); + filePaths.Add(subFile.Path.Value); } } @@ -661,7 +651,7 @@ namespace Barotrauma TextManager.Get("Error"), TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" + TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); string filePath = path; deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 98b33bb6e..6b22e9acf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -27,7 +27,7 @@ namespace Barotrauma public Ladder Ladders; public Structure Stairs; - private List tags; + private HashSet tags; public bool isObstructed; @@ -78,10 +78,7 @@ namespace Barotrauma } } - public IEnumerable Tags - { - get { return tags; } - } + public IEnumerable Tags => tags; public JobPrefab AssignedJob { get; private set; } @@ -114,7 +111,7 @@ namespace Barotrauma public WayPoint(Rectangle newRect, Submarine submarine) - : this (MapEntityPrefab.Find(null, "waypoint"), newRect, submarine) + : this (MapEntityPrefab.FindByIdentifier("waypoint".ToIdentifier()), newRect, submarine) { } @@ -122,8 +119,8 @@ namespace Barotrauma : base (prefab, submarine, id) { rect = newRect; - idCardTags = new string[0]; - tags = new List(); + idCardTags = Array.Empty(); + tags = new HashSet(); #if CLIENT if (iconSprites == null) @@ -165,7 +162,7 @@ namespace Barotrauma public static bool GenerateSubWaypoints(Submarine submarine) { - if (!Hull.hullList.Any()) + if (!Hull.HullList.Any()) { DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found."); return false; @@ -189,13 +186,14 @@ namespace Barotrauma door.Body.Enabled = true; } } - bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin"); + bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier()); float diffFromHullEdge = 50; float minDist = 100.0f; float heightFromFloor = 110.0f; float hullMinHeight = 100; + var removals = new HashSet(); - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (isFlooded) { @@ -587,7 +585,7 @@ namespace Barotrauma Body pickedBody = Submarine.PickBody( ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, Physics.CollisionWall, false, - (Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent() != null); + (Fixture f) => f.Body.UserData is Item pickedItem && pickedItem.GetComponent() != null); Door pickedDoor = null; if (pickedBody != null) @@ -876,9 +874,9 @@ namespace Barotrauma return WayPointList.GetRandom(wp => (ignoreSubmarine || wp.Submarine == sub) && wp.spawnType == spawnType && - (string.IsNullOrEmpty(spawnPointTag) || wp.Tags.Any(t => t.Equals(spawnPointTag, StringComparison.OrdinalIgnoreCase))) && + (spawnPointTag.IsNullOrEmpty() || wp.Tags.Any(t => t == spawnPointTag)) && (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), - useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); + useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced); } public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) @@ -922,7 +920,7 @@ namespace Barotrauma var nonJobSpecificPoints = subWayPoints.FindAll(wp => wp.spawnType == SpawnType.Human && wp.AssignedJob == null); if (nonJobSpecificPoints.Any()) { - assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.Server)]; + assignedWayPoints[i] = nonJobSpecificPoints[Rand.Int(nonJobSpecificPoints.Count, Rand.RandSync.ServerAndClient)]; } if (assignedWayPoints[i] != null) { continue; } @@ -966,13 +964,9 @@ namespace Barotrauma { Stairs = null; Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs); - if (pickedBody != null && pickedBody.UserData is Structure) + if (pickedBody != null && pickedBody.UserData is Structure structure && structure.StairDirection != Direction.None) { - Structure structure = (Structure)pickedBody.UserData; - if (structure != null && structure.StairDirection != Direction.None) - { - Stairs = structure; - } + Stairs = structure; } } @@ -985,13 +979,12 @@ namespace Barotrauma } if (ladderId > 0) { - Item ladderItem = FindEntityByID(ladderId) as Item; - if (ladderItem != null) { Ladders = ladderItem.GetComponent(); } + if (FindEntityByID(ladderId) is Item ladderItem) { Ladders = ladderItem.GetComponent(); } ladderId = 0; } } - public static WayPoint Load(XElement element, Submarine submarine, IdRemap idRemap) + public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = new Rectangle( int.Parse(element.Attribute("x").Value), @@ -1000,8 +993,10 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); - WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine, idRemap.GetOffsetId(element)); - w.spawnType = spawnType; + WayPoint w = new WayPoint(MapEntityPrefab.FindByIdentifier((spawnType == SpawnType.Path ? "waypoint" : "spawnpoint").ToIdentifier()), rect, submarine, idRemap.GetOffsetId(element)) + { + spawnType = spawnType + }; string idCardDescString = element.GetAttributeString("idcarddesc", ""); if (!string.IsNullOrWhiteSpace(idCardDescString)) @@ -1014,7 +1009,7 @@ namespace Barotrauma w.IdCardTags = idCardTagString.Split(','); } - w.tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true).ToList(); + w.tags = element.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); string jobIdentifier = element.GetAttributeString("job", "").ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(jobIdentifier)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 12ecc872f..b56eb93b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -56,9 +56,9 @@ namespace Barotrauma.Networking { if (Type.HasFlag(ChatMessageType.Server) || Type.HasFlag(ChatMessageType.Error) || Type.HasFlag(ChatMessageType.ServerLog)) { - if (translatedText == null || translatedText.Length == 0) + if (translatedText.IsNullOrEmpty()) { - translatedText = TextManager.GetServerMessage(Text); + translatedText = TextManager.GetServerMessage(Text).Value; } return translatedText; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 332de65d0..daf4b37c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -15,11 +15,11 @@ namespace Barotrauma.Networking public UInt64 SteamID; public UInt64 OwnerSteamID; - public string Language; + public LanguageIdentifier Language; public UInt16 Ping; - public string PreferredJob; + public Identifier PreferredJob; public CharacterTeamType TeamID; @@ -148,7 +148,7 @@ namespace Barotrauma.Networking private List kickVoters; - public HashSet GivenAchievements = new HashSet(); + public HashSet GivenAchievements = new HashSet(); public ClientPermissions Permissions = ClientPermissions.None; public List PermittedConsoleCommands diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 01ef58ee2..0c3b25d2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -33,16 +33,16 @@ namespace Barotrauma.Networking { public static List List = new List(); - public readonly string Name; - public readonly string Description; + public readonly LocalizedString Name; + public readonly LocalizedString Description; public readonly ClientPermissions Permissions; public readonly List PermittedCommands; public PermissionPreset(XElement element) { string name = element.GetAttributeString("name", ""); - Name = TextManager.Get("permissionpresetname." + name, true) ?? name; - Description = TextManager.Get("permissionpresetdescription." + name, true) ?? element.GetAttributeString("description", ""); + Name = TextManager.Get("permissionpresetname." + name).Fallback(name); + Description = TextManager.Get("permissionpresetdescription." + name) .Fallback(element.GetAttributeString("description", "")); string permissionsStr = element.GetAttributeString("permissions", ""); if (!Enum.TryParse(permissionsStr, out Permissions)) @@ -53,7 +53,7 @@ namespace Barotrauma.Networking PermittedCommands = new List(); if (Permissions.HasFlag(ClientPermissions.ConsoleCommands)) { - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = subElement.GetAttributeString("name", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 1e3495f4f..f411d9107 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -117,7 +117,7 @@ namespace Barotrauma class CharacterSpawnInfo : IEntitySpawnInfo { - public readonly string identifier; + public readonly Identifier Identifier; public readonly CharacterInfo CharacterInfo; public readonly Vector2 Position; @@ -125,30 +125,32 @@ namespace Barotrauma private readonly Action onSpawned; - public CharacterSpawnInfo(string identifier, Vector2 worldPosition, Action onSpawn = null) + public CharacterSpawnInfo(Identifier identifier, Vector2 worldPosition, Action onSpawn = null) { - this.identifier = identifier ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + this.Identifier = identifier; + if (identifier.IsEmpty) { throw new ArgumentException($"{nameof(CharacterSpawnInfo)} identifier cannot be null."); } Position = worldPosition; this.onSpawned = onSpawn; } - public CharacterSpawnInfo(string identifier, Vector2 position, Submarine sub, Action onSpawn = null) + public CharacterSpawnInfo(Identifier identifier, Vector2 position, Submarine sub, Action onSpawn = null) { - this.identifier = identifier ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + this.Identifier = identifier; + if (identifier.IsEmpty) { throw new ArgumentException($"{nameof(CharacterSpawnInfo)} identifier cannot be null."); } Position = position; Submarine = sub; this.onSpawned = onSpawn; } - public CharacterSpawnInfo(string identifier, Vector2 position, CharacterInfo characterInfo, Action onSpawn = null) : this (identifier, position, onSpawn) + public CharacterSpawnInfo(Identifier identifier, Vector2 position, CharacterInfo characterInfo, Action onSpawn = null) : this (identifier, position, onSpawn) { CharacterInfo = characterInfo; } public Entity Spawn() { - var character = string.IsNullOrEmpty(identifier) ? null : - Character.Create(identifier, + var character = Identifier.IsEmpty ? null : + Character.Create(Identifier, Submarine == null ? Position : Submarine.Position + Position, ToolBox.RandomSeed(8), CharacterInfo, createNetworkEvent: false); return character; @@ -199,6 +201,7 @@ namespace Barotrauma public readonly Entity Entity; public readonly UInt16 OriginalID, OriginalInventoryID; + public readonly int OriginalSlotIndex; public readonly byte OriginalItemContainerIndex; @@ -219,6 +222,7 @@ namespace Barotrauma if (entity is Item item && item.ParentInventory?.Owner != null) { OriginalInventoryID = item.ParentInventory.Owner.ID; + OriginalSlotIndex = item.ParentInventory.FindIndex(item); //find the index of the ItemContainer this item is inside to get the item to //spawn in the correct inventory in multi-inventory items like fabricators if (item.Container != null) @@ -250,7 +254,7 @@ namespace Barotrauma return "EntitySpawner"; } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition = null, int? quality = null, Action onSpawned = null) + public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition = null, int? quality = null, Action onSpawned = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -263,7 +267,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) + public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action onSpawned = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -276,7 +280,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) + public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -294,10 +298,10 @@ namespace Barotrauma }); } - public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, Action onSpawn = null) + public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action onSpawn = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (string.IsNullOrEmpty(speciesName)) + if (speciesName.IsEmpty) { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); @@ -307,10 +311,10 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); } - public void AddToSpawnQueue(string speciesName, Vector2 position, Submarine sub, Action onSpawn = null) + public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action onSpawn = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (string.IsNullOrEmpty(speciesName)) + if (speciesName.IsEmpty) { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); @@ -320,10 +324,10 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); } - public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) + public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action onSpawn = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (string.IsNullOrEmpty(speciesName)) + if (speciesName.IsEmpty) { string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace.CleanupStackTrace(); DebugConsole.ThrowError(errorMsg); @@ -333,10 +337,11 @@ namespace Barotrauma spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); } - public void AddToRemoveQueue(Entity entity) + public void AddEntityToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (removeQueue.Contains(entity) || entity.Removed || entity == null || entity.IdFreed) { return; } + if (entity is Item item) { AddItemToRemoveQueue(item); return; } if (entity is Character) { Character character = entity as Character; @@ -352,7 +357,7 @@ namespace Barotrauma removeQueue.Enqueue(entity); } - public void AddToRemoveQueue(Item item) + public void AddItemToRemoveQueue(Item item) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (removeQueue.Contains(item) || item.Removed) { return; } @@ -364,7 +369,7 @@ namespace Barotrauma { if (containedItem != null) { - AddToRemoveQueue(containedItem); + AddItemToRemoveQueue(containedItem); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs index a5d48f5eb..86c2600bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/FileTransfer/FileTransfer.cs @@ -12,6 +12,6 @@ enum FileTransferType { - Submarine, CampaignSave + Submarine, CampaignSave, Mod } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index a30e4476c..a1631780f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -77,6 +77,7 @@ namespace Barotrauma { typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) }, { typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) }, { typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) }, + { typeof(Identifier), new ReadWriteBehavior(ReadIdentifier, WriteDynamic) }, { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } }.ToImmutableDictionary(); @@ -86,7 +87,7 @@ namespace Barotrauma private static readonly ImmutableDictionary, ReadWriteBehavior> TypePredicates = new Dictionary, ReadWriteBehavior> { // Arrays - { type => type.BaseType?.IsAssignableFrom(typeof(Array)) ?? false, new ReadWriteBehavior(ReadArray, WriteArray) }, + { type => typeof(Array).IsAssignableFrom(type.BaseType), new ReadWriteBehavior(ReadArray, WriteArray) }, // Nested INetSerializableStructs { type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) }, @@ -94,14 +95,77 @@ namespace Barotrauma // Enums { type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) }, - // Nullable / Optional types - { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) } + // Nullable + { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) }, + + // Option + { type => type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } }.ToImmutableDictionary(); + private static readonly Dictionary cachedSomeCreateMethods = new Dictionary(); + private static readonly Dictionary cachedNoneCreateMethod = new Dictionary(); + private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?"); private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?"); + private static void WriteOption(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + Type type = obj.GetType(); + Type optionType = type.GetGenericTypeDefinition(); + Type underlyingType = type.GetGenericArguments()[0]; + + if (optionType == typeof(None<>)) + { + msg.Write(false); + } + else if (optionType == typeof(Some<>)) + { + msg.Write(true); + if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) + { + behavior.WriteAction(obj.Value, attribute, msg); + } + } + else + { + throw new InvalidOperationException("Option type was neither None<> or Some<>"); + } + } + + private static dynamic? ReadOption(IReadMessage inc, Type type, NetworkSerialize attribute) + { + Type underlyingType = type.GetGenericArguments()[0]; + bool hasValue = inc.ReadBoolean(); + if (!hasValue) + { + return GetCreateMethod(typeof(None<>), underlyingType, cachedNoneCreateMethod).Invoke(null, null); + } + + if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) + { + dynamic? value = behavior.ReadAction(inc, underlyingType, attribute); + return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new []{ value }); + } + + throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadOption)}"); + + static MethodInfo GetCreateMethod(Type optionType, Type type, Dictionary cache) + { + if (cache.TryGetValue(type, out MethodInfo? foundInfo)) + { + return foundInfo; + } + + Type genericType = optionType.MakeGenericType(type); + MethodInfo info = genericType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public)!; + cache.Add(type, info); + return info; + } + } + private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) { if (obj is { } notNull) @@ -152,9 +216,9 @@ namespace Barotrauma Range range = GetEnumRange(type); int enumIndex = inc.ReadRangedInteger(range.Start, range.End); - foreach (dynamic e in Enum.GetValues(type)) + foreach (dynamic? e in Enum.GetValues(type)) { - if (Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; } + if (Convert.ChangeType(e, e!.GetTypeCode()) == enumIndex) { return e; } } throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); @@ -213,9 +277,9 @@ namespace Barotrauma msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize); - foreach (dynamic o in array) + foreach (dynamic? o in array) { - if (TryFindBehavior(o.GetType(), out ReadWriteBehavior behavior)) + if (TryFindBehavior(o!.GetType(), out ReadWriteBehavior behavior)) { behavior.WriteAction(o, attribute, msg); } @@ -285,6 +349,8 @@ namespace Barotrauma private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble(); private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString(); + + private static dynamic ReadIdentifier(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadIdentifier(); private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); @@ -408,8 +474,8 @@ namespace Barotrauma /// string
///
///
- /// In addition arrays, enums and are supported.
- /// Using will make the field or property optional + /// In addition arrays, enums, and are supported.
+ /// Using or will make the field or property optional. /// /// public interface INetSerializableStruct diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index 00dea4a4b..1fed8194f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -13,105 +13,105 @@ namespace Barotrauma public string Name => "KarmaManager"; - public Dictionary SerializableProperties { get; private set; } + public Dictionary SerializableProperties { get; private set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool ResetKarmaBetweenRounds { get; set; } - [Serialize(0.1f, true)] + [Serialize(0.1f, IsPropertySaveable.Yes)] public float KarmaDecay { get; set; } - [Serialize(50.0f, true)] + [Serialize(50.0f, IsPropertySaveable.Yes)] public float KarmaDecayThreshold { get; set; } - [Serialize(0.15f, true)] + [Serialize(0.15f, IsPropertySaveable.Yes)] public float KarmaIncrease { get; set; } - [Serialize(50.0f, true)] + [Serialize(50.0f, IsPropertySaveable.Yes)] public float KarmaIncreaseThreshold { get; set; } - [Serialize(0.05f, true)] + [Serialize(0.05f, IsPropertySaveable.Yes)] public float StructureRepairKarmaIncrease { get; set; } - [Serialize(0.1f, true)] + [Serialize(0.1f, IsPropertySaveable.Yes)] public float StructureDamageKarmaDecrease { get; set; } - [Serialize(15.0f, true)] + [Serialize(15.0f, IsPropertySaveable.Yes)] public float MaxStructureDamageKarmaDecreasePerSecond { get; set; } - [Serialize(0.03f, true)] + [Serialize(0.03f, IsPropertySaveable.Yes)] public float ItemRepairKarmaIncrease { get; set; } - [Serialize(0.5f, true)] + [Serialize(0.5f, IsPropertySaveable.Yes)] public float ReactorOverheatKarmaDecrease { get; set; } - [Serialize(30.0f, true)] + [Serialize(30.0f, IsPropertySaveable.Yes)] public float ReactorMeltdownKarmaDecrease { get; set; } - [Serialize(0.1f, true)] + [Serialize(0.1f, IsPropertySaveable.Yes)] public float DamageEnemyKarmaIncrease { get; set; } - [Serialize(0.2f, true)] + [Serialize(0.2f, IsPropertySaveable.Yes)] public float HealFriendlyKarmaIncrease { get; set; } - [Serialize(0.25f, true)] + [Serialize(0.25f, IsPropertySaveable.Yes)] public float DamageFriendlyKarmaDecrease { get; set; } - [Serialize(0.25f, true)] + [Serialize(0.25f, IsPropertySaveable.Yes)] public float StunFriendlyKarmaDecrease { get; set; } - [Serialize(0.3f, true)] + [Serialize(0.3f, IsPropertySaveable.Yes)] public float StunFriendlyKarmaDecreaseThreshold { get; set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float ExtinguishFireKarmaIncrease { get; set; } - [Serialize(defaultValue: 15.0f, true)] + [Serialize(defaultValue: 15.0f, IsPropertySaveable.Yes)] public float DangerousItemStealKarmaDecrease { get; set; } - [Serialize(defaultValue: false, true)] + [Serialize(defaultValue: false, IsPropertySaveable.Yes)] public bool DangerousItemStealBots { get; set; } - [Serialize(defaultValue: 0.05f, true)] + [Serialize(defaultValue: 0.05f, IsPropertySaveable.Yes)] public float BallastFloraKarmaIncrease { get; set; } private int allowedWireDisconnectionsPerMinute; - [Serialize(5, true)] + [Serialize(5, IsPropertySaveable.Yes)] public int AllowedWireDisconnectionsPerMinute { get { return allowedWireDisconnectionsPerMinute; } set { allowedWireDisconnectionsPerMinute = Math.Max(0, value); } } - [Serialize(6.0f, true)] + [Serialize(6.0f, IsPropertySaveable.Yes)] public float WireDisconnectionKarmaDecrease { get; set; } - [Serialize(0.15f, true)] + [Serialize(0.15f, IsPropertySaveable.Yes)] public float SteerSubKarmaIncrease { get; set; } - [Serialize(15.0f, true)] + [Serialize(15.0f, IsPropertySaveable.Yes)] public float SpamFilterKarmaDecrease { get; set; } - [Serialize(40.0f, true)] + [Serialize(40.0f, IsPropertySaveable.Yes)] public float HerpesThreshold { get; set; } - [Serialize(1.0f, true)] + [Serialize(1.0f, IsPropertySaveable.Yes)] public float KickBanThreshold { get; set; } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int KicksBeforeBan { get; set; } - [Serialize(10.0f, true)] + [Serialize(10.0f, IsPropertySaveable.Yes)] public float KarmaNotificationInterval { get; set; } - [Serialize(120.0f, true)] + [Serialize(120.0f, IsPropertySaveable.Yes)] public float AllowedRetaliationTime { get; set; } - [Serialize(5.0f, true)] + [Serialize(5.0f, IsPropertySaveable.Yes)] public float DangerousItemContainKarmaDecrease { get; set; } - [Serialize(defaultValue: true, true)] + [Serialize(defaultValue: true, IsPropertySaveable.Yes)] public bool IsDangerousItemContainKarmaDecreaseIncremental { get; set; } - [Serialize(30.0f, true)] + [Serialize(30.0f, IsPropertySaveable.Yes)] public float MaxDangerousItemContainKarmaDecrease { get; set; } private readonly AfflictionPrefab herpesAffliction; @@ -141,7 +141,7 @@ namespace Barotrauma if (doc?.Root != null) { Presets["custom"] = doc.Root; - foreach (XElement subElement in doc.Root.Elements()) + foreach (var subElement in doc.Root.Elements()) { string presetName = subElement.GetAttributeString("name", ""); Presets[presetName.ToLowerInvariant()] = subElement; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 9314d093d..8580b9ce5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -13,8 +13,6 @@ namespace Barotrauma.Networking public const int ServerNameMaxLength = 60; public const int ServerMessageMaxLength = 2000; - public static string MasterServerUrl = GameMain.Config.MasterServerUrl; - public const float MaxPhysicsBodyVelocity = 64.0f; public const float MaxPhysicsBodyAngularVelocity = 16.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 81170bc0c..af78cac5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -11,17 +11,17 @@ namespace Barotrauma.Networking public readonly Character TargetCharacter; //which entity is this order referring to (hull, reactor, railgun controller, etc) - public readonly ISpatialEntity TargetEntity; + public ISpatialEntity TargetEntity => Order.TargetSpatialEntity; //additional instructions (power up, fire at will, etc) - public readonly string OrderOption; + public Identifier OrderOption => Order.Option; - public readonly int OrderPriority; + public int OrderPriority => Order.ManualPriority; /// /// Used when the order targets a wall /// - public int? WallSectionIndex { get; set; } + public int? WallSectionIndex => Order.WallSectionIndex; public bool IsNewOrder { get; } @@ -29,55 +29,50 @@ namespace Barotrauma.Networking /// Same as calling , /// but the text parameter is set using ///
- public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender, bool isNewOrder = true) - : this(order, orderOption, priority, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, targetCharacter == sender, orderOption, isNewOrder), - targetEntity, targetCharacter, sender, isNewOrder) + public OrderChatMessage(Order order, Character targetCharacter, Character sender, bool isNewOrder = true) + : this(order, + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: targetCharacter == sender, orderOption: order.Option, isNewOrder: isNewOrder), + targetCharacter, sender, isNewOrder) { - + } - public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, - Character targetCharacter, Character sender, bool isNewOrder = true) + public OrderChatMessage(Order order, string text, Character targetCharacter, Character sender, bool isNewOrder = true) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; - OrderOption = orderOption; - OrderPriority = priority; TargetCharacter = targetCharacter; - TargetEntity = targetEntity; IsNewOrder = isNewOrder; } - public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, - string orderOption, int orderPriority, int? wallSectionIndex, bool isNewOrder) + public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, bool isNewOrder) { - msg.Write((byte)Order.PrefabList.IndexOf(order.Prefab)); + msg.Write(order.Prefab.Identifier); msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); - msg.Write(targetEntity is Entity ? (targetEntity as Entity).ID : (UInt16)0); + msg.Write(order.TargetSpatialEntity is Entity ? (order.TargetEntity as Entity).ID : (UInt16)0); // The option of a Dismiss order is written differently so we know what order we target // now that the game supports multiple current orders simultaneously - if (order.Prefab.Identifier != "dismissed") + if (!order.IsDismissal) { - msg.Write((byte)Array.IndexOf(order.Prefab.Options, orderOption)); + msg.Write((byte)order.Options.IndexOf(order.Option)); } else { - if (!string.IsNullOrEmpty(orderOption)) + if (order.Option != Identifier.Empty) { msg.Write(true); - string[] dismissedOrder = orderOption.Split('.'); + string[] dismissedOrder = order.Option.Value.Split('.'); msg.Write((byte)dismissedOrder.Length); if (dismissedOrder.Length > 0) { - string dismissedOrderIdentifier = dismissedOrder[0]; - var orderPrefab = Order.GetPrefab(dismissedOrderIdentifier); - msg.Write((byte)Order.PrefabList.IndexOf(orderPrefab)); + Identifier dismissedOrderIdentifier = dismissedOrder[0].ToIdentifier(); + var orderPrefab = OrderPrefab.Prefabs[dismissedOrderIdentifier]; + msg.Write(dismissedOrderIdentifier); if (dismissedOrder.Length > 1) { - string dismissedOrderOption = dismissedOrder[1]; - msg.Write((byte)Array.IndexOf(orderPrefab.Options, dismissedOrderOption)); + Identifier dismissedOrderOption = dismissedOrder[1].ToIdentifier(); + msg.Write((byte)orderPrefab.Options.IndexOf(dismissedOrderOption)); } } } @@ -89,9 +84,9 @@ namespace Barotrauma.Networking } } - msg.Write((byte)orderPriority); + msg.Write((byte)order.ManualPriority); msg.Write((byte)order.TargetType); - if (order.TargetType == Order.OrderTargetType.Position && targetEntity is OrderTarget orderTarget) + if (order.TargetType == Order.OrderTargetType.Position && order.TargetSpatialEntity is OrderTarget orderTarget) { msg.Write(true); msg.Write(orderTarget.Position.X); @@ -103,7 +98,7 @@ namespace Barotrauma.Networking msg.Write(false); if (order.TargetType == Order.OrderTargetType.WallSection) { - msg.Write((byte)(wallSectionIndex ?? order.WallSectionIndex ?? 0)); + msg.Write((byte)(order.WallSectionIndex ?? 0)); } } @@ -112,14 +107,14 @@ namespace Barotrauma.Networking private void WriteOrder(IWriteMessage msg) { - WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex, IsNewOrder); + WriteOrder(msg, Order, TargetCharacter, IsNewOrder); } public struct OrderMessageInfo { - public int OrderIndex { get; } - public Order OrderPrefab { get; } - public string OrderOption { get; } + public Identifier OrderIdentifier { get; } + public OrderPrefab OrderPrefab => OrderPrefab.Prefabs[OrderIdentifier]; + public Identifier OrderOption { get; } public int? OrderOptionIndex { get; } public Character TargetCharacter { get; } public Order.OrderTargetType TargetType { get; } @@ -129,11 +124,10 @@ namespace Barotrauma.Networking public int Priority { get; } public bool IsNewOrder { get; } - public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, + public OrderMessageInfo(Identifier orderIdentifier, Identifier orderOption, int? orderOptionIndex, Character targetCharacter, Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority, bool isNewOrder) { - OrderIndex = orderIndex; - OrderPrefab = orderPrefab; + OrderIdentifier = orderIdentifier; OrderOption = orderOption; OrderOptionIndex = orderOptionIndex; TargetCharacter = targetCharacter; @@ -148,21 +142,20 @@ namespace Barotrauma.Networking public static OrderMessageInfo ReadOrder(IReadMessage msg) { - int orderIndex = msg.ReadByte(); + Identifier orderIdentifier = msg.ReadIdentifier(); ushort targetCharacterId = msg.ReadUInt16(); Character targetCharacter = targetCharacterId != Entity.NullEntityID ? Entity.FindEntityByID(targetCharacterId) as Character : null; ushort targetEntityId = msg.ReadUInt16(); Entity targetEntity = targetEntityId != Entity.NullEntityID ? Entity.FindEntityByID(targetEntityId) : null; - Order orderPrefab = null; int? optionIndex = null; - string orderOption = null; + Identifier orderOption = Identifier.Empty; // The option of a Dismiss order is written differently so we know what order we target // now that the game supports multiple current orders simultaneously - if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + if (orderIdentifier != Identifier.Empty) { - orderPrefab = Order.PrefabList[orderIndex]; - if (orderPrefab.Identifier != "dismissed") + var orderPrefab = OrderPrefab.Prefabs[orderIdentifier]; + if (!orderPrefab.IsDismissal) { optionIndex = msg.ReadByte(); } @@ -172,11 +165,11 @@ namespace Barotrauma.Networking int identifierCount = msg.ReadByte(); if (identifierCount > 0) { - int dismissedOrderIndex = msg.ReadByte(); - Order dismissedOrderPrefab = null; - if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + Identifier dismissedOrderIdentifier = msg.ReadIdentifier(); + OrderPrefab dismissedOrderPrefab = null; + if (dismissedOrderIdentifier != Identifier.Empty) { - dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + dismissedOrderPrefab = OrderPrefab.Prefabs[dismissedOrderIdentifier]; orderOption = dismissedOrderPrefab.Identifier; } if (identifierCount > 1) @@ -187,7 +180,7 @@ namespace Barotrauma.Networking var options = dismissedOrderPrefab.Options; if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) { - orderOption += $".{options[dismissedOrderOptionIndex]}"; + orderOption = $"{orderOption.Value}.{options[dismissedOrderOptionIndex]}".ToIdentifier(); } } } @@ -217,9 +210,8 @@ namespace Barotrauma.Networking } bool isNewOrder = msg.ReadBoolean(); - - return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, - orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder); + return new OrderMessageInfo(orderIdentifier, orderOption, optionIndex, targetCharacter, + orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs index f7e5e730c..f9e6b81a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -19,6 +19,7 @@ namespace Barotrauma.Networking Double ReadDouble(); UInt32 ReadVariableUInt32(); String ReadString(); + Identifier ReadIdentifier(); Microsoft.Xna.Framework.Color ReadColorR8G8B8(); Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(); int ReadRangedInteger(int min, int max); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs index 16146f8bf..ae32f3bbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -19,11 +19,12 @@ namespace Barotrauma.Networking void WriteColorR8G8B8A8(Microsoft.Xna.Framework.Color val); void WriteVariableUInt32(UInt32 val); void Write(string val); + void Write(Identifier val); void WriteRangedInteger(int val, int min, int max); void WriteRangedSingle(Single val, Single min, Single max, int bitCount); void Write(byte[] val, int startIndex, int length); - void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int outLength); + void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int outLength); int BitPosition { get; set; } int BytePosition { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index def9c670e..b39783e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -454,6 +454,7 @@ namespace Barotrauma.Networking { lengthBits = value; seekPos = seekPos > lengthBits ? lengthBits : seekPos; + MsgWriter.EnsureBufferSize(ref buf, lengthBits); } } @@ -540,6 +541,11 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void Write(Identifier val) + { + Write(val.Value); + } + public void WriteRangedInteger(int val, int min, int max) { MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); @@ -555,9 +561,9 @@ namespace Barotrauma.Networking MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); } - public void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int length) + public void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int length) { - if (LengthBytes <= MsgConstants.CompressionThreshold) + if (LengthBytes <= MsgConstants.CompressionThreshold || !compressPastThreshold) { isCompressed = false; if (LengthBytes > outBuf.Length) { Array.Resize(ref outBuf, LengthBytes); } @@ -764,6 +770,11 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Identifier ReadIdentifier() + { + return ReadString().ToIdentifier(); + } + public Color ReadColorR8G8B8() { return MsgReader.ReadColorR8G8B8(buf, ref seekPos); @@ -773,7 +784,6 @@ namespace Barotrauma.Networking { return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); } - public int ReadRangedInteger(int min, int max) { @@ -938,6 +948,10 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void Write(Identifier val) + { + Write(val.Value); + } public void WriteRangedInteger(int val, int min, int max) { @@ -1019,6 +1033,11 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Identifier ReadIdentifier() + { + return ReadString().ToIdentifier(); + } + public Color ReadColorR8G8B8() { return MsgReader.ReadColorR8G8B8(buf, ref seekPos); @@ -1044,7 +1063,7 @@ namespace Barotrauma.Networking return MsgReader.ReadBytes(buf, ref seekPos, numberOfBytes); } - public void PrepareForSending(ref byte[] outBuf, out bool isCompressed, out int outLength) + public void PrepareForSending(ref byte[] outBuf, bool compressPastThreshold, out bool isCompressed, out int outLength) { throw new InvalidOperationException("ReadWriteMessages are not to be sent"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 0c1569709..f78e60edd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -33,7 +33,7 @@ namespace Barotrauma.Networking protected set; } - public string Language + public LanguageIdentifier Language { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 8f73cfc90..50a94dbe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -236,7 +236,7 @@ namespace Barotrauma.Networking //remove respawn items that have been left in the shuttle if (respawnItems.Contains(item)) { - Spawner.AddToRemoveQueue(item); + Spawner.AddItemToRemoveQueue(item); continue; } @@ -272,7 +272,7 @@ namespace Barotrauma.Networking } } - foreach (Hull hull in Hull.hullList) + foreach (Hull hull in Hull.HullList) { if (hull.Submarine != RespawnShuttle) { continue; } hull.OxygenPercentage = 100.0f; @@ -295,12 +295,12 @@ namespace Barotrauma.Networking c.Kill(CauseOfDeathType.Unknown, null, true); c.Enabled = false; - Spawner.AddToRemoveQueue(c); + Spawner.AddEntityToRemoveQueue(c); if (c.Inventory != null) { foreach (Item item in c.Inventory.AllItems) { - Spawner.AddToRemoveQueue(item); + Spawner.AddItemToRemoveQueue(item); } } } @@ -322,7 +322,7 @@ namespace Barotrauma.Networking public static Affliction GetRespawnPenaltyAffliction() { - var respawnPenaltyAffliction = AfflictionPrefab.List.FirstOrDefault(a => a.AfflictionType.Equals("respawnpenalty", StringComparison.OrdinalIgnoreCase)); + var respawnPenaltyAffliction = AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty"); return respawnPenaltyAffliction?.Instantiate(10.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 8367fdce9..a86284f23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -10,23 +10,20 @@ namespace Barotrauma.Networking { private struct LogMessage { - public readonly string Text; - public readonly string SanitizedText; + public readonly RichString Text; public readonly MessageType Type; - public readonly List RichData; public LogMessage(string text, MessageType type) { if (type.HasFlag(MessageType.Chat)) { - Text = $"[{DateTime.Now}]\n {text}"; + text = $"[{DateTime.Now}]\n {text}"; } else { - Text = $"[{DateTime.Now}]\n {TextManager.GetServerMessage(text)}"; + text = $"[{DateTime.Now}]\n {TextManager.GetServerMessage(text)}"; } - RichData = RichTextData.GetRichTextData(Text, out SanitizedText); - + Text = RichString.Rich(text); Type = type; } } @@ -113,7 +110,7 @@ namespace Barotrauma.Networking var newText = new LogMessage(line, messageType); #if SERVER - DebugConsole.NewMessage(newText.SanitizedText, messageColor[messageType]); //TODO: REMOVE + DebugConsole.NewMessage(newText.Text.SanitizedValue, messageColor[messageType]); //TODO: REMOVE #endif lines.Enqueue(newText); @@ -173,7 +170,7 @@ namespace Barotrauma.Networking try { - File.WriteAllLines(filePath, unsavedLines.Select(l => l.SanitizedText)); + File.WriteAllLines(filePath, unsavedLines.Select(l => l.Text.SanitizedValue)); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index d4031ebc6..00fe33585 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -97,11 +97,8 @@ namespace Barotrauma.Networking private readonly SerializableProperty property; private readonly string typeString; private readonly object parentObject; - - public string Name - { - get { return property.Name; } - } + + public Identifier Name => property.Name.ToIdentifier(); public object Value { @@ -266,7 +263,7 @@ namespace Barotrauma.Networking } }; - public Dictionary SerializableProperties + public Dictionary SerializableProperties { get; private set; @@ -313,7 +310,7 @@ namespace Barotrauma.Networking if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); - UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5); + UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5); if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); @@ -330,7 +327,7 @@ namespace Barotrauma.Networking if (typeName != null || property.PropertyType.IsEnum) { NetPropertyData netPropertyData = new NetPropertyData(networkMember.KarmaManager, property, typeName); - UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5); + UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5); if (netProperties.ContainsKey(key)) { throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); } @@ -382,7 +379,7 @@ namespace Barotrauma.Networking public Voting Voting; - public Dictionary MonsterEnabled { get; private set; } + public Dictionary MonsterEnabled { get; private set; } public const int MaxExtraCargoItemsOfType = 10; public const int MaxExtraCargoItemTypes = 20; @@ -405,63 +402,63 @@ namespace Barotrauma.Networking public WhiteList Whitelist { get; private set; } - [Serialize(20, true)] + [Serialize(20, IsPropertySaveable.Yes)] public int TickRate { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool RandomizeSeed { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool UseRespawnShuttle { get; private set; } - [Serialize(300.0f, true)] + [Serialize(300.0f, IsPropertySaveable.Yes)] public float RespawnInterval { get; private set; } - [Serialize(180.0f, true)] + [Serialize(180.0f, IsPropertySaveable.Yes)] public float MaxTransportTime { get; private set; } - [Serialize(0.2f, true)] + [Serialize(0.2f, IsPropertySaveable.Yes)] public float MinRespawnRatio { get; private set; } - [Serialize(60.0f, true)] + [Serialize(60.0f, IsPropertySaveable.Yes)] public float AutoRestartInterval { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool StartWhenClientsReady { get; set; } - [Serialize(0.8f, true)] + [Serialize(0.8f, IsPropertySaveable.Yes)] public float StartWhenClientsReadyRatio { get; @@ -469,7 +466,7 @@ namespace Barotrauma.Networking } private bool allowSpectating; - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowSpectating { get { return allowSpectating; } @@ -481,21 +478,28 @@ namespace Barotrauma.Networking } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool SaveServerLogs { get; private set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] + public bool AllowModDownloads + { + get; + private set; + } = true; + + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRagdollButton { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowFileTransfers { get; @@ -503,7 +507,7 @@ namespace Barotrauma.Networking } private bool voiceChatEnabled; - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool VoiceChatEnabled { get { return voiceChatEnabled; } @@ -516,7 +520,7 @@ namespace Barotrauma.Networking } private PlayStyle playstyleSelection; - [Serialize(PlayStyle.Casual, true)] + [Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)] public PlayStyle PlayStyle { get { return playstyleSelection; } @@ -527,14 +531,14 @@ namespace Barotrauma.Networking } } - [Serialize(Barotrauma.LosMode.Opaque, true)] + [Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)] public LosMode LosMode { get; set; } - [Serialize(800, true)] + [Serialize(800, IsPropertySaveable.Yes)] public int LinesPerLogFile { get @@ -569,7 +573,7 @@ namespace Barotrauma.Networking #endif } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowVoteKick { get @@ -582,7 +586,7 @@ namespace Barotrauma.Networking } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowEndVoting { get @@ -596,7 +600,7 @@ namespace Barotrauma.Networking } private bool allowRespawn; - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRespawn { get { return allowRespawn; ; } @@ -608,28 +612,28 @@ namespace Barotrauma.Networking } } - [Serialize(0, true)] + [Serialize(0, IsPropertySaveable.Yes)] public int BotCount { get; set; } - [Serialize(16, true)] + [Serialize(16, IsPropertySaveable.Yes)] public int MaxBotCount { get; set; } - [Serialize(BotSpawnMode.Normal, true)] + [Serialize(BotSpawnMode.Normal, IsPropertySaveable.Yes)] public BotSpawnMode BotSpawnMode { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DisableBotConversations { get; @@ -642,76 +646,76 @@ namespace Barotrauma.Networking set { selectedLevelDifficulty = MathHelper.Clamp(value, 0.0f, 100.0f); } } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowDisguises { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRewiring { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool LockAllDefaultWires { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool AllowLinkingWifiToChat { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool AllowFriendlyFire { get; set; } - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool DestructibleOutposts { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool KillableNPCs { get; set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool BanAfterWrongPassword { get; set; } - [Serialize(3, true)] + [Serialize(3, IsPropertySaveable.Yes)] public int MaxPasswordRetriesBeforeBan { get; private set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string SelectedSubmarine { get; set; } - [Serialize("", true)] + [Serialize("", IsPropertySaveable.Yes)] public string SelectedShuttle { get; @@ -719,7 +723,7 @@ namespace Barotrauma.Networking } private YesNoMaybe traitorsEnabled; - [Serialize(YesNoMaybe.No, true)] + [Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)] public YesNoMaybe TraitorsEnabled { get { return traitorsEnabled; } @@ -731,35 +735,35 @@ namespace Barotrauma.Networking } } - [Serialize(defaultValue: 1, isSaveable: true)] + [Serialize(defaultValue: 1, isSaveable: IsPropertySaveable.Yes)] public int TraitorsMinPlayerCount { get; set; } - [Serialize(defaultValue: 90.0f, isSaveable: true)] + [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMinStartDelay { get; set; } - [Serialize(defaultValue: 180.0f, isSaveable: true)] + [Serialize(defaultValue: 180.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMaxStartDelay { get; set; } - [Serialize(defaultValue: 30.0f, isSaveable: true)] + [Serialize(defaultValue: 30.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMinRestartDelay { get; set; } - [Serialize(defaultValue: 90.0f, isSaveable: true)] + [Serialize(defaultValue: 90.0f, isSaveable: IsPropertySaveable.Yes)] public float TraitorsMaxRestartDelay { get; @@ -767,7 +771,7 @@ namespace Barotrauma.Networking } private SelectionMode subSelectionMode; - [Serialize(SelectionMode.Manual, true)] + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] public SelectionMode SubSelectionMode { get { return subSelectionMode; } @@ -780,7 +784,7 @@ namespace Barotrauma.Networking } private SelectionMode modeSelectionMode; - [Serialize(SelectionMode.Manual, true)] + [Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)] public SelectionMode ModeSelectionMode { get { return modeSelectionMode; } @@ -794,42 +798,42 @@ namespace Barotrauma.Networking public BanList BanList { get; private set; } - [Serialize(0.6f, true)] + [Serialize(0.6f, IsPropertySaveable.Yes)] public float EndVoteRequiredRatio { get; private set; } - [Serialize(0.6f, true)] + [Serialize(0.6f, IsPropertySaveable.Yes)] public float SubmarineVoteRequiredRatio { get; private set; } - [Serialize(30f, true)] + [Serialize(30f, IsPropertySaveable.Yes)] public float SubmarineVoteTimeout { get; private set; } - [Serialize(0.6f, true)] + [Serialize(0.6f, IsPropertySaveable.Yes)] public float KickVoteRequiredRatio { get; private set; } - [Serialize(300.0f, true)] + [Serialize(300.0f, IsPropertySaveable.Yes)] public float KillDisconnectedTime { get; private set; } - [Serialize(600.0f, true)] + [Serialize(600.0f, IsPropertySaveable.Yes)] public float KickAFKTime { get; @@ -837,7 +841,7 @@ namespace Barotrauma.Networking } private bool karmaEnabled; - [Serialize(false, true)] + [Serialize(false, IsPropertySaveable.Yes)] public bool KarmaEnabled { get { return karmaEnabled; } @@ -851,7 +855,7 @@ namespace Barotrauma.Networking } private string karmaPreset = "default"; - [Serialize("default", true)] + [Serialize("default", IsPropertySaveable.Yes)] public string KarmaPreset { get { return karmaPreset; } @@ -866,21 +870,21 @@ namespace Barotrauma.Networking } } - [Serialize("sandbox", true)] - public string GameModeIdentifier + [Serialize("sandbox", IsPropertySaveable.Yes)] + public Identifier GameModeIdentifier { get; set; } - [Serialize("All", true)] + [Serialize("All", IsPropertySaveable.Yes)] public string MissionType { get; set; } - [Serialize(8, true)] + [Serialize(8, IsPropertySaveable.Yes)] public int MaxPlayers { get { return maxPlayers; } @@ -893,21 +897,21 @@ namespace Barotrauma.Networking set; } - [Serialize(60f * 60.0f, true)] + [Serialize(60f * 60.0f, IsPropertySaveable.Yes)] public float AutoBanTime { get; private set; } - [Serialize(60.0f * 60.0f * 24.0f, true)] + [Serialize(60.0f * 60.0f * 24.0f, IsPropertySaveable.Yes)] public float MaxAutoBanTime { get; private set; } - [Serialize(true, true)] + [Serialize(true, IsPropertySaveable.Yes)] public bool RadiationEnabled { get; @@ -916,7 +920,7 @@ namespace Barotrauma.Networking private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount; - [Serialize(CampaignSettings.DefaultMaxMissionCount, true)] + [Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)] public int MaxMissionCount { get { return maxMissionCount; } @@ -962,45 +966,40 @@ namespace Barotrauma.Networking /// /// A list of int pairs that represent the ranges of UTF-16 codes allowed in client names /// - public List> AllowedClientNameChars + public List> AllowedClientNameChars { get; private set; - } = new List>(); + } = new List>(); private void InitMonstersEnabled() { //monster spawn settings - if (MonsterEnabled == null) - { - List monsterNames1 = CharacterPrefab.Prefabs.Select(p => p.Identifier).ToList(); - - MonsterEnabled = new Dictionary(); - foreach (string s in monsterNames1) - { - if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true); - } - } + MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); } public void ReadMonsterEnabled(IReadMessage inc) { InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys.ToList(); - foreach (string s in monsterNames) + List monsterNames = MonsterEnabled.Keys + .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) + .ToList(); + foreach (Identifier s in monsterNames) { MonsterEnabled[s] = inc.ReadBoolean(); } inc.ReadPadBits(); } - public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) + public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) { //monster spawn settings - if (monsterEnabled == null) monsterEnabled = MonsterEnabled; + if (monsterEnabled == null) { monsterEnabled = MonsterEnabled; } - List monsterNames = monsterEnabled.Keys.ToList(); - foreach (string s in monsterNames) + List monsterNames = monsterEnabled.Keys + .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) + .ToList(); + foreach (Identifier s in monsterNames) { msg.Write(monsterEnabled[s]); } @@ -1015,7 +1014,7 @@ namespace Barotrauma.Networking Dictionary extraCargo = new Dictionary(); for (int i = 0; i < count; i++) { - string prefabIdentifier = msg.ReadString(); + Identifier prefabIdentifier = msg.ReadIdentifier(); byte amount = msg.ReadByte(); if (MapEntityPrefab.Find(null, prefabIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab && amount > 0) @@ -1041,7 +1040,7 @@ namespace Barotrauma.Networking msg.Write((UInt32)ExtraCargo.Count); foreach (KeyValuePair kvp in ExtraCargo) { - msg.Write(kvp.Key.Identifier ?? ""); + msg.Write(kvp.Key.Identifier); msg.Write((byte)kvp.Value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 8baf4ad07..ae1f8adf7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -564,7 +564,7 @@ namespace Barotrauma } errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); - if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, GameAnalyticsManager.ErrorSeverity.Error, @@ -591,7 +591,7 @@ namespace Barotrauma } errorMsg += "\n" + Environment.StackTrace.CleanupStackTrace(); - if (GameSettings.VerboseLogging) DebugConsole.ThrowError(errorMsg); + if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "PhysicsBody.SetPosition:InvalidPosition" + userData, GameAnalyticsManager.ErrorSeverity.Error, diff --git a/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs b/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs index d47ce7e16..b151d94af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PlayerInput.cs @@ -17,7 +17,7 @@ namespace Barotrauma #if CLIENT private KeyOrMouse binding { - get { return GameMain.Config.KeyBind(inputType); } + get { return GameSettings.CurrentConfig.KeyMap.Bindings[inputType]; } } private static bool AllowOnGUI(InputType input) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs new file mode 100644 index 000000000..495b5fd86 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IImplementsVariants.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public interface IImplementsVariants where T : Prefab + { + public Identifier VariantOf { get; } + + public void InheritFrom(T parent); + } + + public static class VariantExtensions + { + public static ContentXElement CreateVariantXML(this ContentXElement variantElement, ContentXElement baseElement) + { + #warning TODO: fix %ModDir% instances in the base element such that they become %ModDir:BaseMod% if necessary + return variantElement.Element.CreateVariantXML(baseElement.Element).FromPackage(variantElement.ContentPackage); + } + + public static XElement CreateVariantXML(this XElement variantElement, XElement baseElement) + { + XElement newElement = new XElement(variantElement.Name); + newElement.Add(baseElement.Attributes()); + newElement.Add(baseElement.Elements()); + + ReplaceElement(newElement, variantElement); + + void ReplaceElement(XElement element, XElement replacement) + { + List elementsToRemove = new List(); + foreach (XAttribute attribute in replacement.Attributes()) + { + ReplaceAttribute(element, attribute); + } + foreach (XElement replacementSubElement in replacement.Elements()) + { + int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement); + System.Diagnostics.Debug.Assert(index > -1); + + int i = 0; + bool matchingElementFound = false; + foreach (var subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } + if (i == index) + { + if (!replacementSubElement.HasAttributes && !replacementSubElement.HasElements) + { + //if the replacement is empty (no attributes or child elements) + //remove the element from the variant + elementsToRemove.Add(subElement); + } + else + { + ReplaceElement(subElement, replacementSubElement); + } + matchingElementFound = true; + break; + } + i++; + } + if (!matchingElementFound) + { + element.Add(replacementSubElement); + } + } + elementsToRemove.ForEach(e => e.Remove()); + } + + void ReplaceAttribute(XElement element, XAttribute newAttribute) + { + XAttribute existingAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals(newAttribute.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (existingAttribute == null) + { + element.Add(newAttribute); + return; + } + float.TryParse(existingAttribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out float value); + if (newAttribute.Value.StartsWith('*')) + { + string multiplierStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(multiplierStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float multiplier); + if (multiplierStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value * multiplier).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value * multiplier)).ToString(); + } + } + else if (newAttribute.Value.StartsWith('+')) + { + string additionStr = newAttribute.Value.Substring(1, newAttribute.Value.Length - 1); + float.TryParse(additionStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float addition); + if (additionStr.Contains('.') || existingAttribute.Value.Contains('.')) + { + existingAttribute.Value = (value + addition).ToString("G", CultureInfo.InvariantCulture); + } + else + { + existingAttribute.Value = ((int)(value + addition)).ToString(); + } + } + else + { + existingAttribute.Value = newAttribute.Value; + } + } + + return newElement; + } + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs deleted file mode 100644 index def8398f7..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; - -namespace Barotrauma -{ - public interface IPrefab - { - string OriginalName { get; } - string Identifier { get; } - string FilePath { get; } - ContentPackage ContentPackage { get; } - } - - public interface IHasUintIdentifier - { - uint UIntIdentifier { get; set; } - } - - public static class PrefabExtensions - { - public static void CalculatePrefabUIntIdentifier(this T prefab, PrefabCollection prefabs) where T : class, IPrefab, IHasUintIdentifier, IDisposable - { - using (MD5 md5 = MD5.Create()) - { - prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); - - //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small - var collision = prefabs.Find(p => p.Identifier != prefab.Identifier && p.UIntIdentifier == prefab.UIntIdentifier); - if (collision != null) - { - DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})"); - collision.UIntIdentifier++; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs new file mode 100644 index 000000000..1b40954db --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/Prefab.cs @@ -0,0 +1,63 @@ +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Xml.Linq; + +namespace Barotrauma +{ + public abstract class Prefab : IDisposable + { + public readonly static ImmutableHashSet Types; + static Prefab() + { + Types = ReflectionUtils.GetDerivedNonAbstract().ToImmutableHashSet(); + } + + private static bool potentialCallFromConstructor = false; + public static void DisallowCallFromConstructor() + { + if (!potentialCallFromConstructor) { return; } + StackTrace st = new StackTrace(skipFrames: 2, fNeedFileInfo: false); + for (int i = st.FrameCount-1; i >= 0; i--) + { + 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!"); + } + } + potentialCallFromConstructor = false; + } + + public readonly Identifier Identifier; + public readonly ContentFile ContentFile; + + public ContentPackage? ContentPackage => ContentFile?.ContentPackage; + public ContentPath FilePath => ContentFile.Path; + + public Prefab(ContentFile file, Identifier identifier) + { + potentialCallFromConstructor = true; + ContentFile = file; + Identifier = identifier; + if (Identifier.IsEmpty) { throw new ArgumentException($"Error creating {GetType().Name}: Identifier cannot be empty"); } + } + + public Prefab(ContentFile file, ContentXElement element) + { + potentialCallFromConstructor = true; + ContentFile = file; + Identifier = DetermineIdentifier(element!); + if (Identifier.IsEmpty) { throw new ArgumentException($"Error creating {GetType().Name}: Identifier cannot be empty"); } + } + + protected virtual Identifier DetermineIdentifier(XElement element) + { + return element.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + public abstract void Dispose(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index a238947bb..5c3a16671 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -1,13 +1,70 @@ -using System; +#nullable enable +using Barotrauma.Extensions; +using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Security.Cryptography; namespace Barotrauma { - public class PrefabCollection : IEnumerable where T : class, IPrefab, IDisposable + public class PrefabCollection : IEnumerable where T : notnull, Prefab { + /// + /// Default constructor. + /// + public PrefabCollection() + { + var interfaces = typeof(T).GetInterfaces(); + implementsVariants = interfaces.Any(i => i.Name.Contains(nameof(IImplementsVariants))); + } + + /// + /// Constructor with OnAdd and OnRemove callbacks provided. + /// + public PrefabCollection( + Action? onAdd, + Action? onRemove, + Action? onSort, + Action? onAddOverrideFile, + Action? onRemoveOverrideFile) : this() + { + OnAdd = onAdd; + OnRemove = onRemove; + OnSort = onSort; + OnAddOverrideFile = onAddOverrideFile; + OnRemoveOverrideFile = onRemoveOverrideFile; + } + + /// + /// Method to be called when calling Add(T prefab, bool override). + /// If provided, the method is called only if Add succeeds. + /// + private readonly Action? OnAdd = null; + + /// + /// Method to be called when calling Remove(T prefab). + /// If provided, the method is called before success + /// or failure can be determined within the body of Remove. + /// + private readonly Action? OnRemove = null; + + /// + /// Method to be called when calling SortAll(). + /// + private readonly Action? OnSort = null; + + /// + /// Method to be called when calling AddOverrideFile(ContentFile file). + /// + private readonly Action? OnAddOverrideFile = null; + + /// + /// Method to be called when calling RemoveOverrideFile(ContentFile file). + /// + private readonly Action? OnRemoveOverrideFile = null; + /// /// Dictionary containing all prefabs of the same type. /// Key is the identifier. @@ -18,12 +75,132 @@ namespace Barotrauma /// The last element of the list is the prefab that is effectively used /// (hereby called "active prefab") /// - private readonly Dictionary> prefabs = new Dictionary>(); +#if DEBUG && MODBREAKER + private readonly CursedDictionary> prefabs = new CursedDictionary>(); +#else + private readonly ConcurrentDictionary> prefabs = new ConcurrentDictionary>(); +#endif + + /// + /// Collection of content files that override all previous prefabs + /// i.e. anything set to load before these effectively doesn't exist + /// + private readonly HashSet overrideFiles = new HashSet(); + private ContentFile? topMostOverrideFile = null; + + private readonly bool implementsVariants; + + private bool IsPrefabOverriddenByFile(T prefab) + { + return topMostOverrideFile != null && + topMostOverrideFile.ContentPackage.Index > prefab.ContentFile.ContentPackage.Index; + } + + private class InheritanceTreeCollection + { + public class Node + { + public Node(Identifier identifier) { Identifier = identifier; } + + public readonly Identifier Identifier; + public Node? Parent = null; + public readonly HashSet Inheritors = new HashSet(); + } + + private readonly PrefabCollection prefabCollection; + + public InheritanceTreeCollection(PrefabCollection collection) { prefabCollection = collection; } + + public readonly Dictionary IdToNode = new Dictionary(); + public readonly HashSet RootNodes = new HashSet(); + + public Node? AddNodeAndInheritors(Identifier id) + { + if (!prefabCollection.TryGet(id, out T? prefab)) { return null; } + + if (!IdToNode.TryGetValue(id, out var node)) + { + node = new Node(id); + RootNodes.Add(node); + IdToNode.Add(id, node); + } + else + { + //if the node already exists, it already contains + //all inheritors so let's just return this immediately + return node; + } + + prefabCollection + .Cast>() + .Where(p => p.VariantOf == id) + .Cast() + .ForEach(p => + { + var inheritorNode = AddNodeAndInheritors(p.Identifier); + if (inheritorNode is null) { return; } + RootNodes.Remove(inheritorNode); + inheritorNode.Parent = node; + node.Inheritors.Add(inheritorNode); + }); + + return node; + } + + private void FindCycles(in Node node, HashSet uncheckedNodes) + { + HashSet checkedNodes = new HashSet(); + List hierarchyPositions = new List(); + Node? currNode = node; + do + { + if (!uncheckedNodes.Contains(currNode)) { break; } + if (checkedNodes.Contains(currNode)) + { + int index = hierarchyPositions.IndexOf(currNode); + throw new Exception("Inheritance cycle detected: " + +string.Join(", ", hierarchyPositions.Skip(index).Select(n => n.Identifier))); + } + checkedNodes.Add(currNode); + hierarchyPositions.Add(currNode); + currNode = currNode.Parent; + } while (currNode != null); + uncheckedNodes.RemoveWhere(i => checkedNodes.Contains(i)); + } + + public void AddNodesAndInheritors(IEnumerable ids) + => ids.ForEach(id => AddNodeAndInheritors(id)); + + public void InvokeCallbacks() + { + HashSet uncheckedNodes = IdToNode.Values.ToHashSet(); + IdToNode.Values.ForEach(v => FindCycles(v, uncheckedNodes)); + void invokeCallbacksForNode(Node node) + { + if (!prefabCollection.TryGet(node.Identifier, out var p) || + !(p is IImplementsVariants prefab)) { return; } + if (!prefab.VariantOf.IsEmpty && prefabCollection.TryGet(prefab.VariantOf, out T? parent)) { prefab.InheritFrom(parent!); } + node.Inheritors.ForEach(invokeCallbacksForNode); + } + RootNodes.ForEach(invokeCallbacksForNode); + } + } + + private void HandleInheritance(Identifier prefabIdentifier) + => HandleInheritance(prefabIdentifier.ToEnumerable()); + + private void HandleInheritance(IEnumerable identifiers) + { + if (!implementsVariants) { return; } + InheritanceTreeCollection inheritanceTreeCollection = new InheritanceTreeCollection(this); + inheritanceTreeCollection.AddNodesAndInheritors(identifiers); + inheritanceTreeCollection.InvokeCallbacks(); + } /// /// AllPrefabs exposes all prefabs instead of just the active ones. /// - public IEnumerable>> AllPrefabs + public IEnumerable>> AllPrefabs { get { @@ -35,58 +212,107 @@ namespace Barotrauma } /// - /// Returns the active prefab with the identifier. + /// Returns the active prefab with the given identifier. /// /// Prefab identifier - /// Active prefab with the identifier + /// Active prefab with the given identifier + public T this[Identifier identifier] + { + get + { + Prefab.DisallowCallFromConstructor(); + var prefab = prefabs[identifier].ActivePrefab; + if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + { + return prefab; + } + throw new IndexOutOfRangeException($"Prefab of identifier \"{identifier}\" cannot be returned because it was overridden by \"{topMostOverrideFile!.Path}\""); + } + } + public T this[string identifier] { - get { return prefabs[identifier].Last(); } + get + { + //this exists because I don't want implicit + //string to Identifier conversion for the most + //part, but it's useful and fairly safe to do + //in this particular instance + return this[identifier.ToIdentifier()]; + } } + /// + /// Returns true if a prefab with the identifier exists, false otherwise. + /// + /// Prefab identifier + /// The matching prefab (if one is found) + /// Whether a prefab with the identifier exists or not + public bool TryGet(Identifier identifier, out T? result) + { + Prefab.DisallowCallFromConstructor(); + if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) + { + result = selector!.ActivePrefab; + return true; + } + else + { + result = null; + return false; + } + } + + public bool TryGet(string identifier, out T? result) + => TryGet(identifier.ToIdentifier(), out result); + + public IEnumerable Keys => prefabs.Keys; + /// /// Finds the first active prefab that returns true given the predicate, /// or null if no such prefab is found. /// /// Predicate to perform the search with. /// - public T Find(Predicate predicate) + public T? Find(Predicate predicate) { + Prefab.DisallowCallFromConstructor(); foreach (var kpv in prefabs) { - if (predicate(kpv.Value.Last())) + if (kpv.Value.ActivePrefab is T p && predicate(p)) { - return kpv.Value.Last(); + return p; } } return null; } /// - /// Returns true if a prefab with the identifier exists, false otherwise. + /// Returns true if a prefab with the given identifier exists, false otherwise. /// /// Prefab identifier - /// Whether a prefab with the identifier exists or not - public bool ContainsKey(string identifier) + /// Whether a prefab with the given identifier exists or not + public bool ContainsKey(Identifier identifier) { + Prefab.DisallowCallFromConstructor(); return prefabs.ContainsKey(identifier); } + public bool ContainsKey(string k) => prefabs.ContainsKey(k.ToIdentifier()); + /// - /// Returns true if a prefab with the identifier exists, false otherwise. + /// Determines whether a prefab is implemented as an override or not. /// - /// Prefab identifier - /// The matching prefab (if one is found) - /// Whether a prefab with the identifier exists or not - public bool TryGetValue(string identifier, out T prefab) + /// Prefab in this collection + /// Whether a prefab is implemented as an override or not + public bool IsOverride(T prefab) { - if (!ContainsKey(identifier)) + Prefab.DisallowCallFromConstructor(); + if (ContainsKey(prefab.Identifier)) { - prefab = default; - return false; + return prefabs[prefab.Identifier].IsOverride(prefab); } - prefab = this[identifier]; - return true; + return false; } ///
@@ -100,34 +326,51 @@ namespace Barotrauma /// Is marked as override public void Add(T prefab, bool isOverride) { - if (string.IsNullOrWhiteSpace(prefab.Identifier)) + Prefab.DisallowCallFromConstructor(); + if (prefab.Identifier.IsEmpty) { - DebugConsole.ThrowError($"Prefab \"{prefab.OriginalName}\" has no identifier!"); + throw new ArgumentException($"Prefab has no identifier!"); } - bool basePrefabExists = prefabs.TryGetValue(prefab.Identifier, out List list); - - //Handle bad overrides and duplicates - if (basePrefabExists && !isOverride) - { - DebugConsole.ThrowError($"Failed to add the prefab \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T)}): a prefab with the same identifier already exists; try overriding\n{Environment.StackTrace}"); - return; - } + bool selectorExists = prefabs.TryGetValue(prefab.Identifier, out PrefabSelector? selector); //Add to list - if (!basePrefabExists) + selector ??= new PrefabSelector(); + + if (prefab is PrefabWithUintIdentifier prefabWithUintIdentifier) { - list = new List(); + if (!selector.IsEmpty) + { + prefabWithUintIdentifier.UintIdentifier = (selector.ActivePrefab as PrefabWithUintIdentifier)!.UintIdentifier; + } + else + { + using (MD5 md5 = MD5.Create()) + { + prefabWithUintIdentifier.UintIdentifier = ToolBox.IdentifierToUint32Hash(prefab.Identifier, md5); + + //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small + T? findCollision() + => Find(p => + p.Identifier != prefab.Identifier + && p is PrefabWithUintIdentifier otherPrefab + && otherPrefab.UintIdentifier == prefabWithUintIdentifier.UintIdentifier); + for (T? collision = findCollision(); collision != null; collision = findCollision()) + { + DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {typeof(T).Name}: {prefab.Identifier} has the same UintIdentifier as {collision.Identifier} ({prefabWithUintIdentifier.UintIdentifier})"); + prefabWithUintIdentifier.UintIdentifier++; + } + } + } } + selector.Add(prefab, isOverride); - list.Add(prefab); - - Sort(list); - - if (!basePrefabExists) + if (!selectorExists) { - prefabs.Add(prefab.Identifier, list); + if (!prefabs.TryAdd(prefab.Identifier, selector)) { throw new Exception($"Failed to add selector for \"{prefab.Identifier}\""); } } + OnAdd?.Invoke(prefab, isOverride); + HandleInheritance(prefab.Identifier); } /// @@ -136,62 +379,63 @@ namespace Barotrauma /// Prefab public void Remove(T prefab) { + Prefab.DisallowCallFromConstructor(); + OnRemove?.Invoke(prefab); if (!ContainsKey(prefab.Identifier)) { return; } if (!prefabs[prefab.Identifier].Contains(prefab)) { return; } - if (prefabs[prefab.Identifier].IndexOf(prefab)==0) - { - prefabs[prefab.Identifier][0] = null; - } - else - { - prefabs[prefab.Identifier].Remove(prefab); - } - prefab.Dispose(); + prefabs[prefab.Identifier].Remove(prefab); - if (prefabs[prefab.Identifier].Count <= 0 || - (prefabs[prefab.Identifier].Count == 1 && prefabs[prefab.Identifier][0] == null)) + if (prefabs[prefab.Identifier].IsEmpty) { - prefabs.Remove(prefab.Identifier); + prefabs.TryRemove(prefab.Identifier, out _); } + HandleInheritance(prefab.Identifier); } /// /// Removes all prefabs that were loaded from a certain file. /// - /// File path - public void RemoveByFile(string filePath) + public void RemoveByFile(ContentFile file) { - List prefabsToRemove = new List(); + Prefab.DisallowCallFromConstructor(); + HashSet clearedIdentifiers = new HashSet(); foreach (var kpv in prefabs) { - foreach (var prefab in kpv.Value) - { - if (prefab != null && prefab.FilePath == filePath) - { - prefabsToRemove.Add(prefab); - } - } + kpv.Value.RemoveByFile(file, OnRemove); + if (kpv.Value.IsEmpty) { clearedIdentifiers.Add(kpv.Key); } } - foreach (var prefab in prefabsToRemove) + foreach (var identifier in clearedIdentifiers) { - Remove(prefab); + prefabs.TryRemove(identifier, out _); } + RemoveOverrideFile(file); } /// - /// Sorts a list of prefabs based on the content package load order. + /// Adds an override file to the collection. /// - /// List of prefabs - private void Sort(List list) + public void AddOverrideFile(ContentFile file) { - if (list.Count <= 1) { return; } + Prefab.DisallowCallFromConstructor(); + if (!overrideFiles.Contains(file)) + { + overrideFiles.Add(file); + } + OnAddOverrideFile?.Invoke(file); + } - var newList = list.Skip(1) - .OrderByDescending(p => GameMain.Config.EnabledRegularPackages.IndexOf(p.ContentPackage)).ToList(); - - list.RemoveRange(1, list.Count - 1); - list.AddRange(newList); + /// + /// Removes an override file from the collection. + /// + public void RemoveOverrideFile(ContentFile file) + { + Prefab.DisallowCallFromConstructor(); + if (overrideFiles.Contains(file)) + { + overrideFiles.Remove(file); + } + OnRemoveOverrideFile?.Invoke(file); } /// @@ -199,10 +443,14 @@ namespace Barotrauma /// public void SortAll() { + Prefab.DisallowCallFromConstructor(); foreach (var kvp in prefabs) { - Sort(kvp.Value); + kvp.Value.Sort(); } + topMostOverrideFile = overrideFiles.Any() ? overrideFiles.First(f1 => overrideFiles.All(f2 => f1.ContentPackage.Index >= f2.ContentPackage.Index)) : null; + OnSort?.Invoke(); + HandleInheritance(this.Select(p => p.Identifier)); } /// @@ -211,9 +459,14 @@ namespace Barotrauma /// IEnumerator public IEnumerator GetEnumerator() { + Prefab.DisallowCallFromConstructor(); foreach (var kpv in prefabs) { - yield return kpv.Value.Last(); + var prefab = kpv.Value.ActivePrefab; + if (prefab != null && !IsPrefabOverriddenByFile(prefab)) + { + yield return prefab; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs new file mode 100644 index 000000000..631543c0a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabSelector.cs @@ -0,0 +1,170 @@ +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class PrefabSelector : IEnumerable where T : notnull, Prefab + { + public T? BasePrefab + { + get + { + lock (overrides) { return basePrefabInternal; } + } + } + + public T? ActivePrefab + { + get + { + lock (overrides) { return activePrefabInternal; } + } + } + + public void Add(T prefab, bool isOverride) + { + lock (overrides) { AddInternal(prefab, isOverride); } + } + + public void RemoveIfContains(T prefab) + { + lock (overrides) { RemoveIfContainsInternal(prefab); } + } + + public void Remove(T prefab) + { + lock (overrides) { RemoveInternal(prefab); } + } + + public void RemoveByFile(ContentFile file, Action? callback = null) + { + lock (overrides) { RemoveByFileInternal(file, callback); } + } + + public void Sort() + { + lock (overrides) { SortInternal(); } + } + + public bool IsEmpty + { + get + { + lock (overrides) { return isEmptyInternal; } + } + } + + public bool Contains(T prefab) + { + lock (overrides) { return ContainsInternal(prefab); } + } + + public bool IsOverride(T prefab) + { + lock (overrides) { return IsOverrideInternal(prefab); } + } + + + #region Underlying implementations of the public methods, done separately to avoid nested locking + private T? basePrefabInternal; + private readonly List overrides = new List(); + + private T? activePrefabInternal => overrides.Any() ? overrides.First() : basePrefabInternal; + + private void AddInternal(T prefab, bool isOverride) + { + if (isOverride) + { + if (overrides.Contains(prefab)) { throw new InvalidOperationException($"Duplicate prefab in PrefabSelector ({typeof(T)}, {prefab.Identifier}, {prefab.ContentFile.ContentPackage.Name})"); } + overrides.Add(prefab); + } + else + { + if (BasePrefab != null) + { + string prefabName + = prefab is MapEntityPrefab mapEntityPrefab + ? $"\"{mapEntityPrefab.OriginalName}\", \"{prefab.Identifier}\"" + : $"\"{prefab.Identifier}\""; + throw new InvalidOperationException( + $"Failed to add the prefab {prefabName} ({prefab.GetType()}) from \"{prefab.ContentPackage?.Name ?? "[NULL]"}\" ({prefab.ContentPackage?.Dir ?? ""}): " + + $"a prefab with the same identifier from \"{ActivePrefab!.ContentPackage?.Name ?? "[NULL]"}\" ({ActivePrefab!.ContentPackage?.Dir ?? ""}) already exists; try overriding"); + } + basePrefabInternal = prefab; + } + SortInternal(); + } + + private void RemoveIfContainsInternal(T prefab) + { + if (!ContainsInternal(prefab)) { return; } + RemoveInternal(prefab); + } + + private void RemoveInternal(T prefab) + { + if (basePrefabInternal == prefab) { basePrefabInternal = null; } + else if (overrides.Contains(prefab)) { overrides.Remove(prefab); } + else { throw new InvalidOperationException($"Can't remove prefab from PrefabSelector ({typeof(T)}, {prefab.Identifier}, {prefab.ContentFile.ContentPackage.Name})"); } + prefab.Dispose(); + SortInternal(); + } + + private void RemoveByFileInternal(ContentFile file, Action? callback) + { + for (int i = overrides.Count-1; i >= 0; i--) + { + var prefab = overrides[i]; + if (prefab.ContentFile == file) + { + RemoveInternal(prefab); + callback?.Invoke(prefab); + } + } + + if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file) + { + RemoveInternal(basePrefabInternal); + callback?.Invoke(p); + } + } + + private void SortInternal() + { + overrides.Sort((p1, p2) => (p1.ContentPackage?.Index ?? int.MaxValue) - (p2.ContentPackage?.Index ?? int.MaxValue)); + } + + private bool isEmptyInternal => basePrefabInternal is null && !overrides.Any(); + + private bool ContainsInternal(T prefab) => basePrefabInternal == prefab || overrides.Contains(prefab); + + private int IndexOfInternal(T prefab) => basePrefabInternal == prefab + ? overrides.Count + : overrides.IndexOf(prefab); + + private bool IsOverrideInternal(T prefab) => IndexOfInternal(prefab) > 0; + #endregion + + public IEnumerator GetEnumerator() + { + T? basePrefab; + ImmutableArray overrideClone; + lock (overrides) + { + basePrefab = basePrefabInternal; + overrideClone = overrides.ToImmutableArray(); + } + if (basePrefab != null) { yield return basePrefab; } + foreach (T prefab in overrideClone) + { + yield return prefab; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs new file mode 100644 index 000000000..541f15ed4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabWithUintIdentifier.cs @@ -0,0 +1,20 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma +{ + /// + /// Prefab that has a property serves as a deterministic hash of + /// a prefab's identifier. This member is filled automatically + /// by PrefabCollection.Add. Required for GetRandom to work on + /// arbitrary Prefab enumerables, recommended for network synchronization. + /// + public abstract class PrefabWithUintIdentifier : Prefab + { + public UInt32 UintIdentifier { get; set; } + + protected PrefabWithUintIdentifier(ContentFile file, Identifier identifier) : base(file, identifier) { } + + protected PrefabWithUintIdentifier(ContentFile file, ContentXElement element) : base(file, element) { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 93f0a0a79..9b4584325 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -78,7 +78,7 @@ namespace Barotrauma base.Deselect(); #if CLIENT - GameMain.Config.SaveNewPlayerConfig(); + GameSettings.SaveCurrentConfig(); GameMain.SoundManager.SetCategoryMuffle("default", false); GUI.ClearMessages(); #endif @@ -116,37 +116,6 @@ namespace Barotrauma } } } - -#if LINUX - // disgusting - if (PlayerInput.KeyDown(Keys.RightShift) && Character.Controlled is { CharacterHealth: { } health } && PlayerInput.MouseSpeed != Vector2.Zero) - { - AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; - float afflictionAmount = (PlayerInput.MousePosition.X / GameMain.GraphicsWidth) * radiationPrefab.MaxStrength; - Affliction affliction = health.GetAffliction(radiationPrefab.Identifier, true); - - if (affliction == null) - { - health.ApplyAffliction(null, new Affliction(radiationPrefab, Math.Abs(afflictionAmount))); - } - else - { - float diff = affliction.Strength - afflictionAmount; - - if (!MathUtils.NearlyEqual(diff, 0)) - { - if (diff > 0) - { - health.ReduceAffliction(null, radiationPrefab.Identifier, Math.Abs(diff)); - } - else if (diff < 0) - { - health.ApplyAffliction(null, new Affliction(radiationPrefab, Math.Abs(diff))); - } - } - } - } -#endif #endif #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs index 8ddb1ca8c..869d4ccff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs @@ -1,17 +1,12 @@ namespace Barotrauma { - partial class Screen + abstract partial class Screen { - private static Screen selected; - - public static Screen Selected - { - get { return selected; } - } + public static Screen Selected { get; private set; } public static void SelectNull() { - selected = null; + Selected = null; } public virtual void Deselect() @@ -20,9 +15,9 @@ public virtual void Select() { - if (selected != null && selected != this) + if (Selected != null && Selected != this) { - selected.Deselect(); + Selected.Deselect(); #if CLIENT GUIContextMenu.CurrentContextMenu = null; GUI.ClearCursorWait(); @@ -41,14 +36,17 @@ } #endif } - selected = this; + +#if CLIENT + GUI.SettingsMenuOpen = false; +#endif + Selected = this; } - public virtual Camera Cam - { - get { return null; } - } - + public virtual Camera Cam => null; + + public virtual bool IsEditor => false; + public virtual void Update(double deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs index eb93b9d5c..c1a63bb65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/ISerializableEntity.cs @@ -2,14 +2,14 @@ namespace Barotrauma { - interface ISerializableEntity + public interface ISerializableEntity { string Name { get; } - Dictionary SerializableProperties + Dictionary SerializableProperties { get; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index f5cf90bc5..4809ed0ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -3,6 +3,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using System.Linq; @@ -10,6 +11,8 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Networking; +//TODO: come back to this later, clever use of reflection would make this nicer >:) + namespace Barotrauma { [AttributeUsage(AttributeTargets.Property)] @@ -77,7 +80,8 @@ namespace Barotrauma //I would love to see a better way to do this AllowLinkingWifiToChat, IsSwappableItem, - AllowRotating + AllowRotating, + Attachable } public bool IsEditable(ISerializableEntity entity) @@ -94,18 +98,27 @@ namespace Barotrauma { return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen; } + case ConditionType.Attachable: + { + return entity is Holdable holdable && holdable.Attachable; + } } return false; } } + public enum IsPropertySaveable + { + Yes, + No + } [AttributeUsage(AttributeTargets.Property)] public class Serialize : Attribute { - public object defaultValue; - public bool isSaveable; - public string translationTextTag; + public readonly object DefaultValue; + public readonly IsPropertySaveable IsSaveable; + public readonly Identifier TranslationTextTag; /// /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. @@ -122,48 +135,49 @@ namespace Barotrauma /// If set to anything else than null, SerializableEntityEditors will show what the text gets translated to or warn if the text is not found in the language files. /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. /// Setting the value to a non-empty string will let the user select the text from one whose tag starts with the given string (e.g. RoomName. would show all texts with a RoomName.* tag) - public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null, bool alwaysUseInstanceValues = false) + public Serialize(object defaultValue, IsPropertySaveable isSaveable, string description = "", string translationTextTag = "", bool alwaysUseInstanceValues = false) { - this.defaultValue = defaultValue; - this.isSaveable = isSaveable; - this.translationTextTag = translationTextTag; + this.DefaultValue = defaultValue; + this.IsSaveable = isSaveable; + this.TranslationTextTag = translationTextTag.ToIdentifier(); Description = description; AlwaysUseInstanceValues = alwaysUseInstanceValues; } } - class SerializableProperty + public class SerializableProperty { - private static Dictionary supportedTypes = new Dictionary + private readonly static ImmutableDictionary supportedTypes = new Dictionary { { typeof(bool), "bool" }, { typeof(int), "int" }, { typeof(float), "float" }, { typeof(string), "string" }, + { typeof(Identifier), "identifier" }, + { typeof(LocalizedString), "localizedstring" }, { typeof(Point), "point" }, { typeof(Vector2), "vector2" }, { typeof(Vector3), "vector3" }, { typeof(Vector4), "vector4" }, { typeof(Rectangle), "rectangle" }, { typeof(Color), "color" }, - { typeof(string[]), "stringarray" } - }; + { typeof(string[]), "stringarray" }, + { typeof(Identifier[]), "identifierarray" } + }.ToImmutableDictionary(); - private static readonly Dictionary> cachedProperties = - new Dictionary>(); + private static readonly Dictionary> cachedProperties = + new Dictionary>(); public readonly string Name; - public readonly string NameToLowerInvariant; public readonly AttributeCollection Attributes; public readonly Type PropertyType; public readonly bool OverridePrefabValues; - public PropertyInfo PropertyInfo { get; private set; } + public readonly PropertyInfo PropertyInfo; public SerializableProperty(PropertyDescriptor property) { Name = property.Name; - NameToLowerInvariant = Name.ToLowerInvariant(); PropertyInfo = property.ComponentType.GetProperty(property.Name); PropertyType = property.PropertyType; Attributes = property.Attributes; @@ -215,8 +229,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value); - DebugConsole.ThrowError("(Type not supported)"); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (Type \"{PropertyType.Name}\" not supported)"); return false; } @@ -274,12 +287,20 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect(value, true)); break; + case "identifier": + PropertyInfo.SetValue(parentObject, value.ToIdentifier()); + break; + case "localizedstring": + PropertyInfo.SetValue(parentObject, new RawLString(value)); + break; case "stringarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); break; + case "identifierarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value).ToIdentifiers().ToArray()); + break; } } - catch (Exception e) { DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString(), e); @@ -307,7 +328,8 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value + " (not a valid " + PropertyInfo.PropertyType + ")", e); + DebugConsole.ThrowError( + $"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (not a valid {PropertyInfo.PropertyType})", e); return false; } PropertyInfo.SetValue(parentObject, enumVal); @@ -315,8 +337,7 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject + "\" to " + value); - DebugConsole.ThrowError("(Type not supported)"); + DebugConsole.ThrowError($"Failed to set the value of the property \"{Name}\" of \"{parentObject}\" to {value} (Type \"{PropertyType.Name}\" not supported)"); return false; } @@ -349,9 +370,18 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect((string)value, false)); return true; + case "identifier": + PropertyInfo.SetValue(parentObject, new Identifier((string)value)); + return true; + case "localizedstring": + PropertyInfo.SetValue(parentObject, new RawLString((string)value)); + return true; case "stringarray": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); - break; + return true; + case "identifierarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value).ToIdentifiers().ToArray()); + return true; default: DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); @@ -527,6 +557,35 @@ namespace Barotrauma return typeName; } + private readonly ImmutableDictionary> valueGetters = + new Dictionary>() + { + {"Voltage".ToIdentifier(), (obj) => obj is Powered p ? p.Voltage : (object) null}, + {"Charge".ToIdentifier(), (obj) => obj is PowerContainer p ? p.Charge : (object) null}, + {"Overload".ToIdentifier(), (obj) => obj is PowerTransfer p ? p.Overload : (object) null}, + {"AvailableFuel".ToIdentifier(), (obj) => obj is Reactor r ? r.AvailableFuel : (object) null}, + {"FissionRate".ToIdentifier(), (obj) => obj is Reactor r ? r.FissionRate : (object) null}, + {"OxygenFlow".ToIdentifier(), (obj) => obj is Vent v ? v.OxygenFlow : (object) null}, + { + "CurrFlow".ToIdentifier(), + (obj) => obj is Pump p ? (object) p.CurrFlow : + obj is OxygenGenerator o ? (object)o.CurrFlow : + null + }, + {"CurrentVolume".ToIdentifier(), (obj) => obj is Engine e ? e.CurrentVolume : (object)null}, + {"MotionDetected".ToIdentifier(), (obj) => obj is MotionSensor m ? m.MotionDetected : (object)null}, + {"Oxygen".ToIdentifier(), (obj) => obj is Character c ? c.Oxygen : (object)null}, + {"Health".ToIdentifier(), (obj) => obj is Character c ? c.Health : (object)null}, + {"OxygenAvailable".ToIdentifier(), (obj) => obj is Character c ? c.OxygenAvailable : (object)null}, + {"PressureProtection".ToIdentifier(), (obj) => obj is Character c ? c.PressureProtection : (object)null}, + {"IsDead".ToIdentifier(), (obj) => obj is Character c ? c.IsDead : (object)null}, + {"IsHuman".ToIdentifier(), (obj) => obj is Character c ? c.IsHuman : (object)null}, + {"IsOn".ToIdentifier(), (obj) => obj is LightComponent l ? l.IsOn : (object)null}, + {"Condition".ToIdentifier(), (obj) => obj is Item i ? i.Condition : (object)null}, + {"ContainerIdentifier".ToIdentifier(), (obj) => obj is Item i ? i.ContainerIdentifier : (object)null}, + {"PhysicsBodyActive".ToIdentifier(), (obj) => obj is Item i ? i.PhysicsBodyActive : (object)null}, + }.ToImmutableDictionary(); + /// /// Try getting the values of some commonly used properties directly without reflection /// @@ -683,7 +742,7 @@ namespace Barotrauma { case nameof(Item.ContainerIdentifier): { - if (parentObject is Item item) { value = item.ContainerIdentifier; return true; } + if (parentObject is Item item) { value = item.ContainerIdentifier.Value; return true; } } break; } @@ -761,7 +820,7 @@ namespace Barotrauma return editableProperties; } - public static Dictionary GetProperties(object obj) + public static Dictionary GetProperties(object obj) { Type objType = obj.GetType(); if (cachedProperties.ContainsKey(objType)) @@ -770,11 +829,11 @@ namespace Barotrauma } var properties = TypeDescriptor.GetProperties(obj.GetType()).Cast(); - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property in properties) { var serializableProperty = new SerializableProperty(property); - dictionary.Add(serializableProperty.NameToLowerInvariant, serializableProperty); + dictionary.Add(serializableProperty.Name.ToIdentifier(), serializableProperty); } cachedProperties[objType] = dictionary; @@ -782,16 +841,16 @@ namespace Barotrauma return dictionary; } - public static Dictionary DeserializeProperties(object obj, XElement element = null) + public static Dictionary DeserializeProperties(object obj, XElement element = null) { - Dictionary dictionary = GetProperties(obj); + Dictionary dictionary = GetProperties(obj); foreach (var property in dictionary.Values) { //set the value of the property to the default value if there is one foreach (var ini in property.Attributes.OfType()) { - property.TrySetValue(obj, ini.defaultValue); + property.TrySetValue(obj, ini.DefaultValue); break; } } @@ -802,7 +861,7 @@ namespace Barotrauma //and set the value of the matching property if it is initializable foreach (XAttribute attribute in element.Attributes()) { - if (!dictionary.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } + if (!dictionary.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; } if (!property.Attributes.OfType().Any()) { continue; } property.TrySetValue(obj, attribute.Value); } @@ -827,7 +886,7 @@ namespace Barotrauma bool save = false; foreach (var attribute in property.Attributes.OfType()) { - if ((attribute.isSaveable && !attribute.defaultValue.Equals(value)) || + if ((attribute.IsSaveable == IsPropertySaveable.Yes && !attribute.DefaultValue.Equals(value)) || (!ignoreEditable && property.Attributes.OfType().Any())) { save = true; @@ -881,6 +940,10 @@ namespace Barotrauma string[] stringArray = (string[])value; stringValue = stringArray != null ? string.Join(';', stringArray) : ""; break; + case "identifierarray": + Identifier[] identifierArray = (Identifier[])value; + stringValue = identifierArray != null ? string.Join(';', identifierArray) : ""; + break; default: stringValue = value.ToString(); break; @@ -888,7 +951,7 @@ namespace Barotrauma } element.Attribute(property.Name)?.Remove(); - element.SetAttributeValue(property.NameToLowerInvariant, stringValue); + element.SetAttributeValue(property.Name, stringValue); } } @@ -899,9 +962,9 @@ namespace Barotrauma /// The entity to upgrade /// The XML element to get the upgrade instructions from (e.g. the config of an item prefab) /// The game version the entity was saved with - public static void UpgradeGameVersion(ISerializableEntity entity, XElement configElement, Version savedVersion) + public static void UpgradeGameVersion(ISerializableEntity entity, ContentXElement configElement, Version savedVersion) { - foreach (XElement subElement in configElement.Elements()) + foreach (var subElement in configElement.Elements()) { if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); @@ -909,7 +972,7 @@ namespace Barotrauma foreach (XAttribute attribute in subElement.Attributes()) { - string attributeName = attribute.Name.ToString().ToLowerInvariant(); + var attributeName = attribute.NameAsIdentifier(); if (attributeName == "gameversion") { continue; } if (attributeName == "refreshrect") @@ -1024,19 +1087,19 @@ namespace Barotrauma if (entity is Item item2) { - XElement componentElement = subElement.FirstElement(); + var componentElement = subElement.FirstElement(); if (componentElement == null) { continue; } ItemComponent itemComponent = item2.Components.First(c => c.Name == componentElement.Name.ToString()); if (itemComponent == null) { continue; } foreach (XAttribute attribute in componentElement.Attributes()) { - string attributeName = attribute.Name.ToString().ToLowerInvariant(); + var attributeName = attribute.NameAsIdentifier(); if (itemComponent.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { FixValue(property, itemComponent, attribute); } } - foreach (XElement element in componentElement.Elements()) + foreach (var element in componentElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs new file mode 100644 index 000000000..409e5d5b1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/StructSerialization.cs @@ -0,0 +1,233 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using System.Xml.Schema; + +namespace Barotrauma +{ + public static class StructSerialization + { + private static readonly ImmutableDictionary deserializeMethods; + private static readonly ImmutableDictionary serializeMethods; + + public class SkipAttribute : Attribute { } + + private static bool ShouldSkip(this FieldInfo field) + => field.GetCustomAttribute() != null; + + private static HandlerAttribute? ExtractHandler(this FieldInfo field) + => field.GetCustomAttribute(); + + public class HandlerAttribute : Attribute + { + public readonly Func Read; + public readonly Func Write; + + public HandlerAttribute(Type handlerType) + { + var readAction = + handlerType.GetMethod(nameof(Read), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Type {handlerType.Name} does not have a static {nameof(Read)} method"); + var writeAction = + handlerType.GetMethod(nameof(Write), BindingFlags.Public | BindingFlags.Static) + ?? throw new Exception($"Type {handlerType.Name} does not have a static {nameof(Write)} method"); + var paramArray = new object?[1]; + Read = (s) => + { + paramArray[0] = s; + return readAction.Invoke(null, paramArray); + }; + Write = (o) => + { + paramArray[0] = o; + return writeAction.Invoke(null, paramArray)?.ToString(); + }; + } + } + + static StructSerialization() + { + deserializeMethods = + typeof(StructSerialization) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => + { + if (!m.Name.StartsWith("Deserialize")) { return false; } + var parameters = m.GetParameters(); + if (parameters.Length < 1 || parameters.Length > 2 || + parameters[0].ParameterType != typeof(string)) + { + return false; + } + return true; + }) + .Select(m => (m.ReturnType, m)) + .ToImmutableDictionary(); + + serializeMethods = + typeof(StructSerialization) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => + { + if (!m.Name.StartsWith("Serialize")) { return false; } + var parameters = m.GetParameters(); + if (parameters.Length != 1 || + m.ReturnType != typeof(string)) + { + return false; + } + return true; + }) + .Select(m => (m.GetParameters()[0].ParameterType, m)) + .ToImmutableDictionary(); + } + + public static void CopyPropertiesFrom(this ref T self, in T other) where T : struct + { + var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).Where(f => !f.IsInitOnly).ToArray(); + foreach (var field in fields) + { + if (field.ShouldSkip()) { continue; } + field.SetValue(self, field.GetValue(other)); + } + } + + public static void DeserializeElement(this ref T self, XElement element) where T : struct + { + var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).ToArray(); + + //box the struct here so we don't end up + //making a copy every time we feed this to reflection + object boxedSelf = self; + foreach (var field in fields) + { + if (field.ShouldSkip()) { continue; } + boxedSelf.TryDeserialize(field, element); + } + //copy the boxed struct into the original + self = (T)boxedSelf; + } + + public static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element) + { + string fieldName = field.Name.ToLowerInvariant(); + string valueStr = element.GetAttributeString(fieldName, field.GetValue(boxedSelf)?.ToString() ?? ""); + + var handler = field.ExtractHandler(); + + if (handler != null) + { + field.SetValue(boxedSelf, handler.Read(valueStr)); + } + else if (deserializeMethods.TryGetValue(field.FieldType, out MethodInfo? deserializeMethod)) + { + object?[] parameters = { valueStr }; + if (deserializeMethod.GetParameters().Length > 1) + { + Array.Resize(ref parameters, 2); + parameters[1] = field.GetValue(boxedSelf); + } + field.SetValue(boxedSelf, deserializeMethod.Invoke(boxedSelf, parameters)); + } + else if (field.FieldType.IsEnum) + { + field.SetValue(boxedSelf, DeserializeEnum(field.FieldType, valueStr, (Enum)field.GetValue(boxedSelf)!)); + } + } + + public static string DeserializeString(string str) + { + return str; + } + + public static bool DeserializeBool(string str, bool defaultValue) + { + if (bool.TryParse(str, out bool result)) { return result; } + return defaultValue; + } + + public static float DeserializeFloat(string str, float defaultValue) + { + if (float.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out float result)) { return result; } + return defaultValue; + } + + public static Int32 DeserializeInt32(string str, Int32 defaultValue) + { + if (Int32.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out Int32 result)) { return result; } + return defaultValue; + } + + public static Identifier DeserializeIdentifier(string str) + { + return str.ToIdentifier(); + } + + public static LanguageIdentifier DeserializeLanguageIdentifier(string str) + { + return str.ToLanguageIdentifier(); + } + + public static Color DeserializeColor(string str) + { + return XMLExtensions.ParseColor(str); + } + + public static Enum DeserializeEnum(Type enumType, string str, Enum defaultValue) + { + if (Enum.TryParse(enumType, str, out object? result)) { return (Enum)result!; } + return defaultValue; + } + + public static void SerializeElement(this ref T self, XElement element) where T : struct + { + var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).ToArray(); + + foreach (var field in fields) + { + if (field.ShouldSkip()) { continue; } + self.TrySerialize(field, element); + } + } + + public static void TrySerialize(this T self, FieldInfo field, XElement element) where T : struct + { + string fieldName = field.Name.ToLowerInvariant(); + object? fieldValue = field.GetValue(self); + + string valueStr = fieldValue?.ToString() ?? ""; + + var handler = field.ExtractHandler(); + + if (handler != null) + { + valueStr = handler.Write(valueStr) ?? ""; + } + else if (serializeMethods.TryGetValue(field.FieldType, out MethodInfo? method)) + { + object?[] parameters = { fieldValue }; + valueStr = (string)method.Invoke(self, parameters)!; + } + + element.SetAttributeValue(fieldName, valueStr); + } + + public static string SerializeBool(bool val) + => val ? "true" : "false"; + + public static string SerializeInt32(Int32 val) + => val.ToString(CultureInfo.InvariantCulture); + + public static string SerializeFloat(float val) + => val.ToString(CultureInfo.InvariantCulture); + + public static string SerializeColor(Color val) + => val.ToStringHex(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 91e9b82a5..43b51b551 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -69,8 +70,25 @@ namespace Barotrauma return doc; } + public static XDocument TryLoadXml(ContentPath path) => TryLoadXml(path.Value); + public static XDocument TryLoadXml(string filePath) { + var doc = TryLoadXml(filePath, out var exception); + if (exception != null) + { + DebugConsole.ThrowError($"Couldn't load xml document \"{filePath}\"!", exception); + } + else if (doc is null) + { + DebugConsole.ThrowError($"File \"{filePath}\" could not be loaded: Document or the root element is invalid!"); + } + return doc; + } + + public static XDocument TryLoadXml(string filePath, out Exception exception) + { + exception = null; XDocument doc; try { @@ -81,42 +99,16 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Couldn't load xml document \"" + filePath + "\"!", e); + exception = e; return null; } if (doc?.Root == null) { - DebugConsole.ThrowError("File \"" + filePath + "\" could not be loaded: Document or the root element is invalid!"); return null; } return doc; } - public static XDocument LoadXml(string filePath) - { - XDocument doc = null; - - ToolBox.IsProperFilenameCase(filePath); - - if (File.Exists(filePath)) - { - try - { - using FileStream stream = File.Open(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read); - using XmlReader reader = CreateReader(stream, Path.GetFullPath(filePath)); - doc = XDocument.Load(reader); - } - catch - { - return null; - } - - if (doc.Root == null) { return null; } - } - - return doc; - } - public static object GetAttributeObject(XAttribute attribute) { if (attribute == null) { return null; } @@ -151,8 +143,48 @@ namespace Barotrauma public static string GetAttributeString(this XElement element, string name, string defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return GetAttributeString(element.Attribute(name), defaultValue); + if (element?.GetAttribute(name) == null) { return defaultValue; } + string str = GetAttributeString(element.GetAttribute(name), defaultValue); +#if DEBUG + if (!str.IsNullOrEmpty() && + (str.Contains("%ModDir", StringComparison.OrdinalIgnoreCase) + || str.CleanUpPathCrossPlatform(correctFilenameCase: false).StartsWith("Content/", StringComparison.OrdinalIgnoreCase))) + { + DebugConsole.ThrowError($"Use {nameof(GetAttributeContentPath)} instead of {nameof(GetAttributeString)}\n{Environment.StackTrace.CleanupStackTrace()}"); + if (Debugger.IsAttached) { Debugger.Break(); } + } +#endif + return str; + } + + public static string GetAttributeStringUnrestricted(this XElement element, string name, string defaultValue) + { + #warning TODO: remove? + if (element?.GetAttribute(name) == null) { return defaultValue; } + return GetAttributeString(element.GetAttribute(name), defaultValue); + } + + public static bool DoesAttributeReferenceFileNameAlone(this XElement element, string name) + { + string texName = element.GetAttributeStringUnrestricted(name, ""); + return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase); + } + + public static ContentPath GetAttributeContentPath(this XElement element, string name, + ContentPackage contentPackage) + { + if (element?.GetAttribute(name) == null) { return null; } + return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null)); + } + + public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue) + { + return element.GetAttributeString(name, defaultValue).ToIdentifier(); + } + + public static Identifier GetAttributeIdentifier(this XElement element, string name, Identifier defaultValue) + { + return element.GetAttributeIdentifier(name, defaultValue.Value); } private static string GetAttributeString(XAttribute attribute, string defaultValue) @@ -163,43 +195,41 @@ namespace Barotrauma public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(',', ','); - if (convertToLowerInvariant) + for (int i = 0; i < splitValue.Length; i++) { - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].ToLowerInvariant(); - } - } - if (trim) - { - for (int i = 0; i < splitValue.Length; i++) - { - splitValue[i] = splitValue[i].Trim(); - } + if (convertToLowerInvariant) { splitValue[i] = splitValue[i].ToLowerInvariant(); } + if (trim) { splitValue[i] = splitValue[i].Trim(); } } return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) + { + return element.GetAttributeStringArray(name, null, trim: trim, convertToLowerInvariant: false) + ?.ToIdentifiers() + ?? defaultValue; + } + public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { if (element == null) { return defaultValue; } foreach (string name in matchingAttributeName) { - if (element.Attribute(name) == null) { continue; } + if (element.GetAttribute(name) == null) { continue; } float val; try { - string strVal = element.Attribute(name).Value; + string strVal = element.GetAttribute(name).Value; if (strVal.LastOrDefault() == 'f') { strVal = strVal.Substring(0, strVal.Length - 1); @@ -219,12 +249,12 @@ namespace Barotrauma public static float GetAttributeFloat(this XElement element, string name, float defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } float val = defaultValue; try { - string strVal = element.Attribute(name).Value; + string strVal = element.GetAttribute(name).Value; if (strVal.LastOrDefault() == 'f') { strVal = strVal.Substring(0, strVal.Length - 1); @@ -286,9 +316,9 @@ namespace Barotrauma public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -315,15 +345,15 @@ namespace Barotrauma public static int GetAttributeInt(this XElement element, string name, int defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } int val = defaultValue; try { - if (!Int32.TryParse(element.Attribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) + if (!Int32.TryParse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) { - val = (int)float.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); + val = (int)float.Parse(element.GetAttribute(name).Value, CultureInfo.InvariantCulture); } } catch (Exception e) @@ -336,13 +366,13 @@ namespace Barotrauma public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } uint val = defaultValue; try { - val = UInt32.Parse(element.Attribute(name).Value); + val = UInt32.Parse(element.GetAttribute(name).Value); } catch (Exception e) { @@ -354,13 +384,31 @@ namespace Barotrauma public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = UInt64.Parse(element.Attribute(name).Value); + val = UInt64.Parse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + + return val; + } + + public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue) + { + if (element?.GetAttribute(name) == null) return defaultValue; + + Version val = defaultValue; + + try + { + val = Version.Parse(element.GetAttribute(name).Value); } catch (Exception e) { @@ -372,13 +420,13 @@ namespace Barotrauma public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = Steam.SteamManager.SteamIDStringToUInt64(element.Attribute(name).Value); + val = Steam.SteamManager.SteamIDStringToUInt64(element.GetAttribute(name).Value); } catch (Exception e) { @@ -390,9 +438,9 @@ namespace Barotrauma public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -414,9 +462,9 @@ namespace Barotrauma } public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -448,8 +496,8 @@ namespace Barotrauma public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return element.Attribute(name).GetAttributeBool(defaultValue); + if (element?.GetAttribute(name) == null) { return defaultValue; } + return element.GetAttribute(name).GetAttributeBool(defaultValue); } public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue) @@ -472,45 +520,45 @@ namespace Barotrauma public static Point GetAttributePoint(this XElement element, string name, Point defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return ParsePoint(element.Attribute(name).Value); + if (element?.GetAttribute(name) == null) { return defaultValue; } + return ParsePoint(element.GetAttribute(name).Value); } public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } - return ParseVector2(element.Attribute(name).Value); + if (element?.GetAttribute(name) == null) { return defaultValue; } + return ParseVector2(element.GetAttribute(name).Value); } public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseVector3(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseVector3(element.GetAttribute(name).Value); } public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseVector4(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseVector4(element.GetAttribute(name).Value); } public static Color GetAttributeColor(this XElement element, string name, Color defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseColor(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseColor(element.GetAttribute(name).Value); } public static Color? GetAttributeColor(this XElement element, string name) { - if (element == null || element.Attribute(name) == null) { return null; } - return ParseColor(element.Attribute(name).Value); + if (element == null || element.GetAttribute(name) == null) { return null; } + return ParseColor(element.GetAttribute(name).Value); } public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (element?.GetAttribute(name) == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = element.GetAttribute(name).Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(';'); @@ -531,10 +579,31 @@ namespace Barotrauma return colorValue; } +#if CLIENT + public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue) + { + string strValue = element.GetAttributeString(name, defaultValue?.ToString() ?? ""); + if (Enum.TryParse(strValue, true, out Microsoft.Xna.Framework.Input.Keys key)) + { + return key; + } + else if (Enum.TryParse(strValue, out MouseButton mouseButton)) + { + return mouseButton; + } + else if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out int mouseButtonInt) && + (Enum.GetValues(typeof(MouseButton)) as MouseButton[]).Contains((MouseButton)mouseButtonInt)) + { + return (MouseButton)mouseButtonInt; + } + return defaultValue; + } +#endif + public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue) { - if (element == null || element.Attribute(name) == null) { return defaultValue; } - return ParseRect(element.Attribute(name).Value, false); + if (element == null || element.GetAttribute(name) == null) { return defaultValue; } + return ParseRect(element.GetAttribute(name).Value, false); } //TODO: nested tuples and and n-uples where n!=2 are unsupported @@ -703,16 +772,10 @@ namespace Barotrauma if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase)) { #if CLIENT - if (GUI.Style != null) + Identifier colorName = stringColor.Substring(4).ToIdentifier(); + if (GUIStyle.Colors.TryGetValue(colorName, out GUIColor guiColor)) { - string colorName = stringColor.Substring(4); - var property = GUI.Style.GetType().GetProperties().FirstOrDefault( - p => p.PropertyType == typeof(Color) && - p.Name.Equals(colorName, StringComparison.OrdinalIgnoreCase)); - if (property != null) - { - return (Color)property?.GetValue(GUI.Style); - } + return guiColor.Value; } #endif return Color.White; @@ -727,7 +790,7 @@ namespace Barotrauma if (strComponents.Length == 1) { - bool hexFailed = true; + bool altParseFailed = true; stringColor = stringColor.Trim(); if (stringColor.Length > 0 && stringColor[0] == '#') { @@ -744,11 +807,40 @@ namespace Barotrauma components[2] = ((float)((colorInt & 0x0000ff00) >> 8)) / 255.0f; components[3] = ((float)(colorInt & 0x000000ff)) / 255.0f; - hexFailed = false; + altParseFailed = false; + } + } + else if (stringColor.Length > 0 && stringColor[0] == '{') + { + stringColor = stringColor.Substring(1, stringColor.Length-2); + + string[] mgComponents = stringColor.Split(' '); + if (mgComponents.Length == 4) + { + altParseFailed = false; + + string[] expectedPrefixes = {"R:", "G:", "B:", "A:"}; + for (int i = 0; i < 4; i++) + { + if (mgComponents[i].StartsWith(expectedPrefixes[i], StringComparison.OrdinalIgnoreCase)) + { + string strToParse = mgComponents[i] + .Remove(expectedPrefixes[i], StringComparison.OrdinalIgnoreCase) + .Trim(); + int val = 0; + altParseFailed |= !int.TryParse(strToParse, out val); + components[i] = ((float) val) / 255f; + } + else + { + altParseFailed = true; + break; + } + } } } - if (hexFailed) + if (altParseFailed) { if (errorMessages) { DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); } return Color.White; @@ -807,18 +899,27 @@ namespace Barotrauma return floatArray; } + public static Identifier VariantOf(this XElement element) => + element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", "")); + public static string[] ParseStringArray(string stringArrayValues) { - return string.IsNullOrEmpty(stringArrayValues) ? new string[0] : stringArrayValues.Split(';'); + return string.IsNullOrEmpty(stringArrayValues) ? Array.Empty() : stringArrayValues.Split(';'); + } + + public static Identifier[] ParseIdentifierArray(string stringArrayValues) + { + return ParseStringArray(stringArrayValues).ToIdentifiers().ToArray(); } - public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase); - public static bool IsCharacterVariant(this XElement element) => element.Name.ToString().Equals("charactervariant", StringComparison.OrdinalIgnoreCase); + public static bool IsOverride(this XElement element) => element.NameAsIdentifier() == "override"; public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault(); public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); + public static XAttribute GetAttribute(this XElement element, Identifier name) => element.GetAttribute(name.Value, StringComparison.OrdinalIgnoreCase); + public static XAttribute GetAttribute(this XElement element, Func predicate) => element.Attributes().FirstOrDefault(predicate); /// @@ -830,5 +931,31 @@ namespace Barotrauma /// Returns all child elements that match the name using the provided comparison method. /// public static IEnumerable GetChildElements(this XContainer container, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => container.Elements().Where(e => e.Name.ToString().Equals(name, comparisonMethod)); + + public static IEnumerable GetChildElements(this XContainer container, params string[] names) + { + return names.SelectMany(name => container.GetChildElements(name)); + } + + public static bool ComesAfter(this XElement element, XElement other) + { + if (element.Parent != other.Parent) { return false; } + foreach (var child in element.Parent.Elements()) + { + if (child == element) { return false; } + if (child == other) { return true; } + } + return false; + } + + public static Identifier NameAsIdentifier(this XElement elem) + { + return elem.Name.LocalName.ToIdentifier(); + } + + public static Identifier NameAsIdentifier(this XAttribute attr) + { + return attr.Name.LocalName.ToIdentifier(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs new file mode 100644 index 000000000..5886c8ba3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/CreatureMetrics.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Barotrauma +{ + public class CreatureMetrics + { + public readonly HashSet RecentlyEncountered = new HashSet(); + public readonly HashSet Encountered = new HashSet(); + public readonly HashSet Killed = new HashSet(); + + public readonly static CreatureMetrics Instance = new CreatureMetrics(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs new file mode 100644 index 000000000..b917b844a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -0,0 +1,562 @@ +#nullable enable +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +#if CLIENT +using Barotrauma.ClientSource.Settings; +using Microsoft.Xna.Framework.Input; +#endif + +namespace Barotrauma +{ + public enum WindowMode + { + Windowed, Fullscreen, BorderlessWindowed + } + + public enum LosMode + { + None = 0, + Transparent = 1, + Opaque = 2 + } + + public enum VoiceMode + { + Disabled, + PushToTalk, + Activity + } + + public static class GameSettings + { + public struct Config + { + public static Config GetDefault() + { + Config config = new Config + { + Language = LanguageIdentifier.None, + SubEditorUndoBuffer = 32, + MaxAutoSaves = 8, + AutoSaveIntervalSeconds = 300, + SubEditorBackground = new Color(13, 37, 69, 255), + EnableSplashScreen = true, + PauseOnFocusLost = true, + AimAssistAmount = 0.5f, + EnableMouseLook = true, + ChatOpen = true, + CrewMenuOpen = true, + CampaignDisclaimerShown = false, + EditorDisclaimerShown = false, + ShowOffensiveServerPrompt = true, + TutorialSkipWarning = true, + CorpseDespawnDelay = 600, + CorpsesPerSubDespawnThreshold = 5, + #if OSX + UseDualModeSockets = false, + #else + UseDualModeSockets = true, + #endif + DisableInGameHints = false, + EnableSubmarineAutoSave = true, + Graphics = GraphicsSettings.GetDefault(), + Audio = AudioSettings.GetDefault(), +#if CLIENT + KeyMap = KeyMapping.GetDefault(), + InventoryKeyMap = InventoryKeyMapping.GetDefault() +#endif + + }; +#if DEBUG + config.UseSteamMatchmaking = true; + config.QuickStartSub = "Humpback".ToIdentifier(); + config.RequireSteamAuthentication = true; + config.AutomaticQuickStartEnabled = false; + config.AutomaticCampaignLoadEnabled = false; + config.TextManagerDebugModeEnabled = false; + config.ModBreakerMode = false; +#endif + return config; + } + + public static Config FromFile(string configFile, in Config? fallback = null) + { + XDocument doc = XMLExtensions.TryLoadXml(configFile); + + return FromElement(doc.Root ?? throw new InvalidOperationException("Unable to load config file: XML document is null."), fallback); + } + + public static Config FromElement(XElement element, in Config? fallback = null) + { + Config retVal = fallback ?? GetDefault(); + + retVal.DeserializeElement(element); + + retVal.Graphics = GraphicsSettings.FromElements(element.GetChildElements("graphicsmode", "graphicssettings"), retVal.Graphics); + retVal.Audio = AudioSettings.FromElements(element.GetChildElements("audio"), retVal.Audio); +#if CLIENT + retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap); + retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap); +#endif + + return retVal; + } + + public LanguageIdentifier Language; + public bool VerboseLogging; + public bool SaveDebugConsoleLogs; + public int SubEditorUndoBuffer; + public int MaxAutoSaves; + public int AutoSaveIntervalSeconds; + public Color SubEditorBackground; + public bool EnableSplashScreen; + public bool PauseOnFocusLost; + public float AimAssistAmount; + public bool EnableMouseLook; + public bool ChatOpen; + public bool CrewMenuOpen; + public bool CampaignDisclaimerShown; + public bool EditorDisclaimerShown; + public bool ShowOffensiveServerPrompt; + public bool TutorialSkipWarning; + public int CorpseDespawnDelay; + public int CorpsesPerSubDespawnThreshold; + public bool UseDualModeSockets; + public bool DisableInGameHints; + public bool EnableSubmarineAutoSave; + public Identifier QuickStartSub; +#if DEBUG + public bool UseSteamMatchmaking; + public bool RequireSteamAuthentication; + public bool AutomaticQuickStartEnabled; + public bool AutomaticCampaignLoadEnabled; + public bool TestScreenEnabled; + public bool TextManagerDebugModeEnabled; + public bool ModBreakerMode; +#else + public bool UseSteamMatchmaking => true; + public bool RequireSteamAuthentication => true; +#endif + + public struct GraphicsSettings + { + public static GraphicsSettings GetDefault() + { + GraphicsSettings gfxSettings = new GraphicsSettings + { + RadialDistortion = true, + InventoryScale = 1.0f, + LightMapScale = 1.0f, + TextScale = 1.0f, + HUDScale = 1.0f, + Specularity = true, + ChromaticAberration = true, + ParticleLimit = 1500, + LosMode = LosMode.Transparent + }; + gfxSettings.RadialDistortion = true; + gfxSettings.CompressTextures = true; + gfxSettings.FrameLimit = 300; + gfxSettings.VSync = true; +#if DEBUG + gfxSettings.DisplayMode = WindowMode.Windowed; +#else + gfxSettings.DisplayMode = WindowMode.BorderlessWindowed; +#endif + return gfxSettings; + } + + public static GraphicsSettings FromElements(IEnumerable elements, in GraphicsSettings? fallback = null) + { + GraphicsSettings retVal = fallback ?? GetDefault(); + elements.ForEach(element => retVal.DeserializeElement(element)); + return retVal; + } + + public int Width; + public int Height; + public bool VSync; + public bool CompressTextures; + public int FrameLimit; + public WindowMode DisplayMode; + public int ParticleLimit; + public bool Specularity; + public bool ChromaticAberration; + public LosMode LosMode; + public float HUDScale; + public float InventoryScale; + public float LightMapScale; + public float TextScale; + public bool RadialDistortion; + } + + [StructSerialization.Skip] + public GraphicsSettings Graphics; + + public struct AudioSettings + { + public static class DeviceNameHandler + { + public static string Read(string s) + => System.Xml.XmlConvert.DecodeName(s)!; + + public static string Write(string s) + => System.Xml.XmlConvert.EncodeName(s)!; + } + + public static AudioSettings GetDefault() + { + AudioSettings audioSettings = new AudioSettings + { + MusicVolume = 0.3f, + SoundVolume = 0.5f, + VoiceChatVolume = 0.5f, + VoiceChatCutoffPrevention = 0, + MicrophoneVolume = 5, + MuteOnFocusLost = false, + DynamicRangeCompressionEnabled = true, + UseDirectionalVoiceChat = true, + VoipAttenuationEnabled = true, + VoiceSetting = VoiceMode.PushToTalk, + UseLocalVoiceByDefault = false, + DisableVoiceChatFilters = false + }; + return audioSettings; + } + + public static AudioSettings FromElements(IEnumerable elements, in AudioSettings? fallback = null) + { + AudioSettings retVal = fallback ?? GetDefault(); + elements.ForEach(element => retVal.DeserializeElement(element)); + return retVal; + } + + public float MusicVolume; + public float SoundVolume; + public float VoiceChatVolume; + public int VoiceChatCutoffPrevention; + public float MicrophoneVolume; + public bool MuteOnFocusLost; + public bool DynamicRangeCompressionEnabled; + public bool UseDirectionalVoiceChat; + public bool VoipAttenuationEnabled; + public VoiceMode VoiceSetting; + + [StructSerialization.Handler(typeof(DeviceNameHandler))] + public string AudioOutputDevice; + [StructSerialization.Handler(typeof(DeviceNameHandler))] + public string VoiceCaptureDevice; + + public float NoiseGateThreshold; + public bool UseLocalVoiceByDefault; + public bool DisableVoiceChatFilters; + } + + [StructSerialization.Skip] + public AudioSettings Audio; + +#if CLIENT + public struct KeyMapping + { + private readonly static ImmutableDictionary DefaultsQwerty = + new Dictionary() + { + { InputType.Run, Keys.LeftShift }, + { InputType.Attack, Keys.R }, + { InputType.Crouch, Keys.LeftControl }, + { InputType.Grab, Keys.G }, + { InputType.Health, Keys.H }, + { InputType.Ragdoll, Keys.Space }, + { InputType.Aim, MouseButton.SecondaryMouse }, + + { InputType.InfoTab, Keys.Tab }, + { InputType.Chat, Keys.T }, + { InputType.RadioChat, Keys.R }, + { InputType.CrewOrders, Keys.C }, + + { InputType.Voice, Keys.V }, + { InputType.LocalVoice, Keys.B }, + { InputType.Command, MouseButton.MiddleMouse }, + { InputType.PreviousFireMode, MouseButton.MouseWheelDown }, + { InputType.NextFireMode, MouseButton.MouseWheelUp }, + + { InputType.TakeHalfFromInventorySlot, Keys.LeftShift }, + { InputType.TakeOneFromInventorySlot, Keys.LeftControl }, + + { InputType.Up, Keys.W }, + { InputType.Down, Keys.S }, + { InputType.Left, Keys.A }, + { InputType.Right, Keys.D }, + { InputType.ToggleInventory, Keys.Q }, + + { InputType.SelectNextCharacter, Keys.Z }, + { InputType.SelectPreviousCharacter, Keys.X }, + + { InputType.Use, Keys.E }, + { InputType.Select, MouseButton.PrimaryMouse }, + { InputType.Deselect, MouseButton.SecondaryMouse }, + { InputType.Shoot, MouseButton.PrimaryMouse } + }.ToImmutableDictionary(); + + public static KeyMapping GetDefault() => new KeyMapping + { + Bindings = DefaultsQwerty + .Select(kvp => + (kvp.Key, kvp.Value.MouseButton == MouseButton.None + ? (KeyOrMouse)Keyboard.QwertyToCurrentLayout(kvp.Value.Key) + : (KeyOrMouse)kvp.Value.MouseButton)) + .ToImmutableDictionary() + }; + + public KeyMapping(IEnumerable elements, in KeyMapping? fallback) + { + var defaultBindings = GetDefault().Bindings; + Dictionary bindings = fallback?.Bindings?.ToMutable() ?? defaultBindings.ToMutable(); + foreach (InputType inputType in (InputType[])Enum.GetValues(typeof(InputType))) + { + if (!bindings.ContainsKey(inputType)) { bindings.Add(inputType, defaultBindings[inputType]); } + } + + foreach (XElement element in elements) + { + foreach (XAttribute attribute in element.Attributes()) + { + if (Enum.TryParse(attribute.Name.LocalName, out InputType result)) + { + bindings[result] = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]); + } + } + } + + Bindings = bindings.ToImmutableDictionary(); + } + + public KeyMapping WithBinding(InputType type, KeyOrMouse bind) + { + KeyMapping newMapping = this; + newMapping.Bindings = newMapping.Bindings + .Select(kvp => + kvp.Key == type + ? (type, bind) + : (kvp.Key, kvp.Value)) + .ToImmutableDictionary(); + return newMapping; + } + + public ImmutableDictionary Bindings; + + public LocalizedString KeyBindText(InputType inputType) => Bindings[inputType].Name; + } + + [StructSerialization.Skip] + public KeyMapping KeyMap; + + public struct InventoryKeyMapping + { + public ImmutableArray Bindings; + + public static InventoryKeyMapping GetDefault() => new InventoryKeyMapping + { + Bindings = new KeyOrMouse[] + { + Keys.D1, + Keys.D2, + Keys.D3, + Keys.D4, + Keys.D5, + Keys.D6, + Keys.D7, + Keys.D8, + Keys.D9, + Keys.D0, + }.ToImmutableArray() + }; + + public InventoryKeyMapping WithBinding(int index, KeyOrMouse keyOrMouse) + { + var thisBindings = Bindings; + return new InventoryKeyMapping() + { + Bindings = Enumerable.Range(0, thisBindings.Length) + .Select(i => i == index ? keyOrMouse : thisBindings[i]) + .ToImmutableArray() + }; + } + + public InventoryKeyMapping(IEnumerable elements, InventoryKeyMapping? fallback) + { + var bindings = (fallback?.Bindings ?? GetDefault().Bindings).ToArray(); + foreach (XElement element in elements) + { + for (int i = 0; i < bindings.Length; i++) + { + bindings[i] = element.GetAttributeKeyOrMouse($"slot{i}", bindings[i]); + } + } + Bindings = bindings.ToImmutableArray(); + } + } + + [StructSerialization.Skip] + public InventoryKeyMapping InventoryKeyMap; +#endif + } + + public const string PlayerConfigPath = "config_player.xml"; + + private static Config currentConfig; + public static ref readonly Config CurrentConfig => ref currentConfig; + + public static void Init() + { + XDocument? currentConfigDoc = null; + + if (File.Exists(PlayerConfigPath)) + { + currentConfigDoc = XMLExtensions.TryLoadXml(PlayerConfigPath); + } + + if (currentConfigDoc != null) + { + currentConfig = Config.FromElement(currentConfigDoc.Root ?? throw new NullReferenceException("Config XML element is invalid: document is null.")); +#if CLIENT + ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); + MultiplayerPreferences.Init( + currentConfigDoc.Root.GetChildElement("player"), + currentConfigDoc.Root.GetChildElement("gameplay")?.GetChildElement("jobpreferences")); + IgnoredHints.Init(currentConfigDoc.Root.GetChildElement("ignoredhints")); + DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement("debugconsolemapping")); + CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement("tutorials")); +#endif + } + else + { + currentConfig = Config.GetDefault(); + SaveCurrentConfig(); + } + } + + 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 languageChanged = currentConfig.Language != newConfig.Language; + + currentConfig = newConfig; +#warning TODO: Implement program state updates; + +#if CLIENT + if (setGraphicsMode) + { + GameMain.Instance.ApplyGraphicsSettings(); + } + + GameMain.SoundManager?.ApplySettings(); +#endif + if (languageChanged) { TextManager.ClearCache(); } + } + + public static void SaveCurrentConfig() + { + XDocument configDoc = new XDocument(); + XElement root = new XElement("config"); configDoc.Add(root); + currentConfig.SerializeElement(root); + + 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); + + XElement regularPackagesElement = new XElement(ContentPackageManager.RegularPackagesElementName); contentPackagesElement.Add(regularPackagesElement); + foreach (var regularPackage in ContentPackageManager.EnabledPackages.Regular) + { + XComment packageComment = new XComment(regularPackage.Name); regularPackagesElement.Add(packageComment); + XElement packageElement = new XElement(ContentPackageManager.RegularPackagesSubElementName); regularPackagesElement.Add(packageElement); + packageElement.SetAttributeValue("path", regularPackage.Path); + } + +#if CLIENT + XElement serverFiltersElement = new XElement("serverfilters"); root.Add(serverFiltersElement); + ServerListFilters.Instance.SaveTo(serverFiltersElement); + + XElement characterElement = new XElement("player"); root.Add(characterElement); + MultiplayerPreferences.Instance.SaveTo(characterElement); + + XElement ignoredHintsElement = new XElement("ignoredhints"); root.Add(ignoredHintsElement); + IgnoredHints.Instance.SaveTo(ignoredHintsElement); + + XElement debugConsoleMappingElement = new XElement("debugconsolemapping"); root.Add(debugConsoleMappingElement); + DebugConsoleMapping.Instance.SaveTo(debugConsoleMappingElement); + + XElement tutorialsElement = new XElement("tutorials"); root.Add(tutorialsElement); + CompletedTutorials.Instance.SaveTo(tutorialsElement); + + XElement keyMappingElement = new XElement("keymapping", + currentConfig.KeyMap.Bindings.Select(kvp + => new XAttribute(kvp.Key.ToString(), kvp.Value.ToString()))); + root.Add(keyMappingElement); + + XElement inventoryKeyMappingElement = new XElement("inventorykeymapping", + Enumerable.Range(0, currentConfig.InventoryKeyMap.Bindings.Length) + .Zip(currentConfig.InventoryKeyMap.Bindings) + .Cast<(int Index, KeyOrMouse Bind)>() + .Select(kvp + => new XAttribute($"slot{kvp.Index.ToString(CultureInfo.InvariantCulture)}", kvp.Bind.ToString()))); + root.Add(inventoryKeyMappingElement); +#endif + + configDoc.SaveSafe(PlayerConfigPath); + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true, + NewLineOnAttributes = true + }; + + try + { + using (var writer = XmlWriter.Create(PlayerConfigPath, settings)) + { + configDoc.WriteTo(writer); + writer.Flush(); + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving game settings failed.", e); + GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error, + "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index b1f25b2c0..9cc87f0cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -17,7 +17,7 @@ namespace Barotrauma public DeformableSprite DeformableSprite { get; private set; } public Sprite ActiveSprite => Sprite ?? DeformableSprite.Sprite; - public ConditionalSprite(XElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) + public ConditionalSprite(ContentXElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) { Target = target; Exclusive = element.GetAttributeBool("exclusive", Exclusive); @@ -26,7 +26,7 @@ namespace Barotrauma { Enum.TryParse(comparison, ignoreCase: true, out Comparison); } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs index 7fcc37437..502cfa9e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs @@ -18,7 +18,7 @@ namespace Barotrauma public Sprite Sprite { get; private set; } - public DeformableSprite(XElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) + public DeformableSprite(ContentXElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) { Sprite = new Sprite(element, file: filePath, lazyLoad: lazyLoad); InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad, invert); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 4cbd51d6c..55e75b67d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -39,7 +39,7 @@ namespace Barotrauma /// /// Reference to the xml element from where the sprite was created. Can be null if the sprite was not defined in xml! /// - public XElement SourceElement { get; private set; } + public ContentXElement SourceElement { get; private set; } //the area in the texture that is supposed to be drawn private Rectangle sourceRect; @@ -108,9 +108,9 @@ namespace Barotrauma public Vector2 RelativeSize { get; private set; } - public string FilePath { get; private set; } + public ContentPath FilePath { get; private set; } - public string FullPath { get; private set; } + public string FullPath => FilePath.FullPath; public bool Compress { get; private set; } @@ -119,11 +119,11 @@ namespace Barotrauma return FilePath + ": " + sourceRect; } - public string ID { get; private set; } + public Identifier Identifier { get; private set; } /// - /// ID of the Map Entity so that we can link the sprite to it's owner. + /// Identifier of the Map Entity so that we can link the sprite to its owner. /// - public string EntityID { get; set; } + public Identifier EntityIdentifier { get; set; } public string Name { get; set; } partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn); @@ -138,9 +138,9 @@ namespace Barotrauma } } - public Sprite(XElement element, string path = "", string file = "", bool lazyLoad = false) + public Sprite(ContentXElement element, string path = "", string file = "", bool lazyLoad = false) { - if (element == null) { return; } + if (element is null) { return; } this.LazyLoad = lazyLoad; SourceElement = element; if (!ParseTexturePath(path, file)) { return; } @@ -171,7 +171,7 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); - ID = GetID(SourceElement); + Identifier = GetIdentifier(SourceElement); AddToList(this); } @@ -201,11 +201,7 @@ namespace Barotrauma private void Init(string newFile, Rectangle? sourceRectangle = null, Vector2? newOrigin = null, Vector2? newOffset = null, float newRotation = 0) { - FilePath = newFile; - if (!string.IsNullOrEmpty(FilePath)) - { - FullPath = Path.GetFullPath(FilePath); - } + FilePath = ContentPath.FromRaw(newFile); Vector4 sourceVector = Vector4.Zero; bool shouldReturn = false; LoadTexture(ref sourceVector, ref shouldReturn); @@ -228,14 +224,15 @@ namespace Barotrauma } /// - /// Creates a supposedly unique id from the parent element. If the parent element is not found, uses the sprite element. + /// Creates a supposedly unique identifier from the parent element. If the parent element is not found, uses the sprite element. /// TODO: If there are multiple elements with exactly the same data, the ids will fail. -> Is there a better way to identify the sprites? + /// ALSO TODO: delete :) /// - public static string GetID(XElement sourceElement) + public static Identifier GetIdentifier(XElement sourceElement) { - if (sourceElement == null) { return string.Empty; } + if (sourceElement == null) { return "".ToIdentifier(); } var parentElement = sourceElement.Parent; - return parentElement != null ? sourceElement.ToString() + parentElement.ToString() : sourceElement.ToString(); + return $"{sourceElement}{parentElement?.ToString() ?? ""}".ToIdentifier(); } public void Remove() @@ -268,13 +265,13 @@ namespace Barotrauma } var doc = XMLExtensions.TryLoadXml(path); if (doc == null) { return; } - if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(EntityID)) { return; } + if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(EntityIdentifier.Value)) { return; } var spriteElements = doc.Descendants("sprite").Concat(doc.Descendants("Sprite")); var sourceElements = spriteElements.Where(e => e.GetAttributeString("name", null) == Name); if (sourceElements.None()) { // Try parents by first comparing the entity id and then the name, if no match was found. - sourceElements = spriteElements.Where(e => e.Parent?.GetAttributeString("identifier", null) == EntityID); + sourceElements = spriteElements.Where(e => e.Parent?.GetAttributeString("identifier", null) == EntityIdentifier); if (sourceElements.None()) { sourceElements = spriteElements.Where(e => e.Parent?.GetAttributeString("name", null) == Name); @@ -282,15 +279,15 @@ namespace Barotrauma } if (sourceElements.Multiple()) { - DebugConsole.NewMessage($"[Sprite] Multiple matching elements found by name ({Name}) or identifier ({EntityID})!: {SourceElement.ToString()}", Color.Yellow); + DebugConsole.NewMessage($"[Sprite] Multiple matching elements found by name ({Name}) or identifier ({EntityIdentifier})!: {SourceElement}", Color.Yellow); } else if (sourceElements.None()) { - DebugConsole.NewMessage($"[Sprite] Cannot find matching source element by comparing the name attribute ({Name}) or identifier ({EntityID})! Cannot reload the xml for sprite element \"{SourceElement.ToString()}\"!", Color.Yellow); + DebugConsole.NewMessage($"[Sprite] Cannot find matching source element by comparing the name attribute ({Name}) or identifier ({EntityIdentifier})! Cannot reload the xml for sprite element \"{SourceElement.ToString()}\"!", Color.Yellow); } else { - SourceElement = sourceElements.Single(); + SourceElement = sourceElements.Single().FromPackage(SourceElement.ContentPackage); } if (SourceElement != null) { @@ -311,7 +308,7 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); Depth = SourceElement.GetAttributeFloat("depth", 0.001f); - ID = GetID(SourceElement); + Identifier = GetIdentifier(SourceElement); } } @@ -319,11 +316,11 @@ namespace Barotrauma { if (file == "") { - file = SourceElement.GetAttributeString("texture", ""); + file = SourceElement.GetAttributeStringUnrestricted("texture", ""); var overrideElement = GetLocalizationOverrideElement(); if (overrideElement != null) { - string overrideFile = overrideElement.GetAttributeString("texture", ""); + string overrideFile = overrideElement.GetAttributeStringUnrestricted("texture", ""); if (!string.IsNullOrEmpty(overrideFile)) { file = overrideFile; } } } @@ -336,22 +333,18 @@ namespace Barotrauma { if (!path.EndsWith("/")) path += "/"; } - FilePath = (path + file).CleanUpPathCrossPlatform(correctFilenameCase: true); - if (!string.IsNullOrEmpty(FilePath)) - { - FullPath = Path.GetFullPath(FilePath); - } + FilePath = ContentPath.FromRaw(SourceElement.ContentPackage, (path + file).CleanUpPathCrossPlatform(correctFilenameCase: true)); return true; } private XElement GetLocalizationOverrideElement() { - foreach (XElement subElement in SourceElement.Elements()) + foreach (var subElement in SourceElement.Elements()) { if (subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { - string language = subElement.GetAttributeString("language", ""); - if (TextManager.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase)) + LanguageIdentifier language = subElement.GetAttributeIdentifier("language", "").ToLanguageIdentifier(); + if (GameSettings.CurrentConfig.Language == language) { return subElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs index a186f716a..b9164a09a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/SpriteSheet.cs @@ -19,8 +19,8 @@ namespace Barotrauma get; private set; } - - public SpriteSheet(XElement element, string path = "", string file = "") + + public SpriteSheet(ContentXElement element, string path = "", string file = "") : base(element, path, file) { int columnCount = Math.Max(element.GetAttributeInt("columns", 1), 1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index e6d051abf..025127e42 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -36,7 +36,7 @@ namespace Barotrauma private readonly DelayTypes delayType; private readonly float delay; - public DelayedEffect(XElement element, string parentDebugName) + public DelayedEffect(ContentXElement element, string parentDebugName) : base(element, parentDebugName) { string delayTypeStr = element.GetAttributeString("delaytype", "timer"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index b8ca1a030..bd86e6618 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -15,12 +15,14 @@ namespace Barotrauma { public enum ConditionType { + Uncertain, PropertyValue, Name, SpeciesName, SpeciesGroup, HasTag, HasStatusTag, + HasSpecifierTag, Affliction, EntityType, LimbType @@ -34,18 +36,18 @@ namespace Barotrauma public enum OperatorType { + None, Equals, NotEquals, LessThan, LessThanEquals, GreaterThan, - GreaterThanEquals, - None + GreaterThanEquals } public readonly ConditionType Type; public readonly OperatorType Operator; - public readonly string AttributeName; + public readonly Identifier AttributeName; public readonly string AttributeValue; public readonly string[] SplitAttributeValue; public readonly float? FloatValue; @@ -79,35 +81,22 @@ namespace Barotrauma // TODO: use XElement instead of XAttribute (how to do without breaking the existing content?) public PropertyConditional(XAttribute attribute) { - AttributeName = attribute.Name.ToString().ToLowerInvariant(); - string attributeValueString = attribute.Value.ToString(); + AttributeName = attribute.NameAsIdentifier(); + string attributeValueString = attribute.Value; if (string.IsNullOrWhiteSpace(attributeValueString)) { - DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent.ToString()}"); + DebugConsole.ThrowError($"Conditional attribute value is empty: {attribute.Parent}"); return; } string valueString = attributeValueString; - string[] splitString = valueString.Split(' '); - if (splitString.Length > 0) - { - for (int i = 1; i < splitString.Length; i++) - { - valueString = splitString[i] + (i > 1 && i < splitString.Length ? " " : ""); - } - } - string op = splitString[0]; - OperatorType operatorType = GetOperatorType(op); + string[] splitString = valueString.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (splitString.Length > 1) { valueString = string.Join(' ', splitString.Skip(1)); } + Operator = GetOperatorType(splitString[0]); - if (operatorType != OperatorType.None) + if (Operator == OperatorType.None) { - Operator = operatorType; - } - else - { - if (op != "==" && op != "!=" && op != ">" && op != "<" && op != ">=" && op != "<=") //Didn't use escape strings or anything - { - valueString = attributeValueString; //We probably don't even have an operator - } + Operator = OperatorType.Equals; + valueString = attributeValueString; } TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); @@ -116,16 +105,9 @@ namespace Barotrauma TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); TargetContainedItem = attribute.Parent.GetAttributeBool("targetcontaineditem", false); - if (!Enum.TryParse(AttributeName, true, out Type)) + if (!Enum.TryParse(AttributeName.Value, true, out Type)) { - if (AfflictionPrefab.Prefabs.Any(p => p.Identifier.Equals(AttributeName, StringComparison.OrdinalIgnoreCase))) - { - Type = ConditionType.Affliction; - } - else - { - Type = ConditionType.PropertyValue; - } + Type = ConditionType.Uncertain; } AttributeValue = valueString; @@ -172,15 +154,36 @@ namespace Barotrauma } } + public bool Matches(ISerializableEntity target) - { - if (TargetContainedItem) + { + return Matches(target, TargetContainedItem); + } + + public bool Matches(ISerializableEntity target, bool checkContained) + { + var type = Type; + if (type == ConditionType.Uncertain) + { + if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName)) + { + type = ConditionType.Affliction; + } + else + { + type = (target?.SerializableProperties?.ContainsKey(AttributeName) ?? false) + ? ConditionType.PropertyValue + : ConditionType.HasSpecifierTag; + } + } + + if (checkContained) { if (target is Item item) { foreach (var containedItem in item.ContainedItems) { - if (Matches(containedItem)) { return true; } + if (Matches(containedItem, checkContained: false)) { return true; } } return false; } @@ -188,7 +191,7 @@ namespace Barotrauma { foreach (var containedItem in ic.Item.ContainedItems) { - if (Matches(containedItem)) { return true; } + if (Matches(containedItem, checkContained: false)) { return true; } } return false; } @@ -197,13 +200,13 @@ namespace Barotrauma if (character.Inventory == null) { return false; } foreach (var containedItem in character.Inventory.AllItems) { - if (Matches(containedItem)) { return true; } + if (Matches(containedItem, checkContained: false)) { return true; } } return false; } } - switch (Type) + switch (type) { case ConditionType.PropertyValue: SerializableProperty property; @@ -244,18 +247,18 @@ namespace Barotrauma } } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; case ConditionType.SpeciesName: { if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return Operator == OperatorType.Equals == targetCharacter.SpeciesName.Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); + return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == AttributeValue); } case ConditionType.SpeciesGroup: { if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return Operator == OperatorType.Equals == targetCharacter.Params.CompareGroup(AttributeValue); + return (Operator == OperatorType.Equals) == targetCharacter.Params.CompareGroup(AttributeValue.ToIdentifier()); } case ConditionType.EntityType: switch (AttributeValue) @@ -298,7 +301,7 @@ namespace Barotrauma { var health = targetChar.CharacterHealth; if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName); + var affliction = health.GetAffliction(AttributeName.ToIdentifier()); float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; if (FloatValue.HasValue) { @@ -343,14 +346,14 @@ namespace Barotrauma return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - public bool MatchesTagCondition(string targetTag) + public bool MatchesTagCondition(Identifier targetTag) { - if (string.IsNullOrEmpty(targetTag) || Type != ConditionType.HasTag) { return false; } + if (targetTag.IsEmpty || Type != ConditionType.HasTag) { return false; } int matches = 0; foreach (string tag in SplitAttributeValue) { - if (targetTag.Equals(tag, StringComparison.OrdinalIgnoreCase)) + if (targetTag == tag) { matches++; } @@ -358,7 +361,7 @@ namespace Barotrauma //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - + // TODO: refactor and add tests private bool Matches(ISerializableEntity target, SerializableProperty property) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index f4d193768..bc1da8b5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -5,6 +5,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; @@ -44,24 +45,24 @@ namespace Barotrauma { public string Name => "ai trigger"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } - [Serialize(AIState.Idle, false)] + [Serialize(AIState.Idle, IsPropertySaveable.No)] public AIState State { get; private set; } - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float Duration { get; private set; } - [Serialize(1f, false)] + [Serialize(1f, IsPropertySaveable.No)] public float Probability { get; private set; } - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float MinDamage { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool AllowToOverride { get; private set; } - [Serialize(true, false)] + [Serialize(true, IsPropertySaveable.No)] public bool AllowToBeOverridden { get; private set; } public bool IsTriggered { get; private set; } @@ -152,6 +153,8 @@ namespace Barotrauma public readonly float AimSpread; public readonly bool Equip; + public readonly float Condition; + public ItemSpawnInfo(XElement element, string parentDebugName) { if (element.Attribute("name") != null) @@ -185,6 +188,8 @@ namespace Barotrauma SpawnIfCantBeContained = element.GetAttributeBool("spawnifcantbecontained", true); Speed = element.GetAttributeFloat("speed", 0.0f); + Condition = MathHelper.Clamp(element.GetAttributeFloat("condition", 1.0f), 0.0f, 1.0f); + Rotation = element.GetAttributeFloat("rotation", 0.0f); Count = element.GetAttributeInt("count", 1); Spread = element.GetAttributeFloat("spread", 0f); @@ -206,36 +211,36 @@ namespace Barotrauma public class AbilityStatusEffectIdentifier : AbilityObject { - public AbilityStatusEffectIdentifier(string effectIdentifier) + public AbilityStatusEffectIdentifier(Identifier effectIdentifier) { EffectIdentifier = effectIdentifier; } - public string EffectIdentifier { get; set; } + public Identifier EffectIdentifier { get; set; } } public class GiveTalentInfo { - public string[] TalentIdentifiers; + public Identifier[] TalentIdentifiers; public bool GiveRandom; public GiveTalentInfo(XElement element, string _) { - TalentIdentifiers = element.GetAttributeStringArray("talentidentifiers", new string[0], convertToLowerInvariant: true); + TalentIdentifiers = element.GetAttributeIdentifierArray("talentidentifiers", Array.Empty()); GiveRandom = element.GetAttributeBool("giverandom", false); } } public class GiveSkill { - public string SkillIdentifier; - public float Amount; + public readonly Identifier SkillIdentifier; + public readonly float Amount; public GiveSkill(XElement element, string parentDebugName) { - SkillIdentifier = element.GetAttributeString("skillidentifier", string.Empty); + SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty); Amount = element.GetAttributeFloat("amount", 0); - if (SkillIdentifier == string.Empty) + if (SkillIdentifier == Identifier.Empty) { DebugConsole.ThrowError($"GiveSkill StatusEffect did not have a skill identifier defined in {parentDebugName}!"); } @@ -245,24 +250,24 @@ namespace Barotrauma public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; - public Dictionary SerializableProperties { get; set; } + public Dictionary SerializableProperties { get; set; } - [Serialize("", false)] - public string SpeciesName { get; private set; } + [Serialize("", IsPropertySaveable.No)] + public Identifier SpeciesName { get; private set; } - [Serialize(1, false)] + [Serialize(1, IsPropertySaveable.No)] public int Count { get; private set; } - [Serialize(0f, false)] + [Serialize(0f, IsPropertySaveable.No)] public float Spread { get; private set; } - [Serialize("0,0", false)] + [Serialize("0,0", IsPropertySaveable.No)] public Vector2 Offset { get; private set; } public CharacterSpawnInfo(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - if (string.IsNullOrEmpty(SpeciesName)) + if (SpeciesName.IsEmpty) { DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element}\""); } @@ -270,7 +275,6 @@ namespace Barotrauma } private readonly TargetType targetTypes; - protected HashSet targetIdentifiers; /// /// Index of the slot the target must be in when targeting a Contained item @@ -279,7 +283,7 @@ namespace Barotrauma private readonly List requiredItems; - public readonly string[] propertyNames; + public readonly Identifier[] propertyNames; public readonly object[] propertyEffects; private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; @@ -324,8 +328,8 @@ namespace Barotrauma private readonly List aiTriggers; private readonly List triggeredEvents; - private readonly string triggeredEventTargetTag = "statuseffecttarget", - triggeredEventEntityTag = "statuseffectentity"; + private readonly Identifier triggeredEventTargetTag = "statuseffecttarget".ToIdentifier(), + triggeredEventEntityTag = "statuseffectentity".ToIdentifier(); private Character user; @@ -347,15 +351,12 @@ namespace Barotrauma /// public readonly bool AllowWhenBroken = false; - public HashSet TargetIdentifiers - { - get { return targetIdentifiers; } - } + public readonly ImmutableHashSet TargetIdentifiers; /// /// Which type of afflictions the target must receive for the StatusEffect to be applied. Only valid when the type of the effect is OnDamaged. /// - private readonly HashSet<(string affliction, float strength)> requiredAfflictions; + private readonly HashSet<(Identifier affliction, float strength)> requiredAfflictions; public float AfflictionMultiplier = 1.0f; @@ -372,9 +373,9 @@ namespace Barotrauma get { return spawnCharacters; } } - public readonly List<(string affliction, float amount)> ReduceAffliction; + public readonly List<(Identifier AfflictionIdentifier, float ReduceAmount)> ReduceAffliction; - private readonly List talentTriggers; + private readonly List talentTriggers; private readonly List giveExperiences; private readonly List giveSkills; @@ -406,7 +407,7 @@ namespace Barotrauma } } - public static StatusEffect Load(XElement element, string parentDebugName) + public static StatusEffect Load(ContentXElement element, string parentDebugName) { if (element.Attribute("delay") != null || element.Attribute("delaytype") != null) { @@ -416,7 +417,7 @@ namespace Barotrauma return new StatusEffect(element, parentDebugName); } - protected StatusEffect(XElement element, string parentDebugName) + protected StatusEffect(ContentXElement element, string parentDebugName) { requiredItems = new List(); spawnItems = new List(); @@ -427,8 +428,8 @@ namespace Barotrauma Afflictions = new List(); Explosions = new List(); triggeredEvents = new List(); - ReduceAffliction = new List<(string affliction, float amount)>(); - talentTriggers = new List(); + ReduceAffliction = new List<(Identifier affliction, float amount)>(); + talentTriggers = new List(); giveExperiences = new List(); giveSkills = new List(); multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false); @@ -460,7 +461,7 @@ namespace Barotrauma string[] targetTypesStr = element.GetAttributeStringArray("target", null) ?? - element.GetAttributeStringArray("targettype", new string[0]); + element.GetAttributeStringArray("targettype", Array.Empty()); foreach (string s in targetTypesStr) { if (!Enum.TryParse(s, true, out TargetType targetType)) @@ -500,20 +501,15 @@ namespace Barotrauma case "targets": case "targetidentifiers": case "targettags": - string[] identifiers = attribute.Value.Split(','); - targetIdentifiers = new HashSet(); - for (int i = 0; i < identifiers.Length; i++) - { - targetIdentifiers.Add(identifiers[i].Trim().ToLowerInvariant()); - } + TargetIdentifiers = attribute.Value.Split(',').ToIdentifiers().ToImmutableHashSet(); break; case "allowedafflictions": case "requiredafflictions": string[] types = attribute.Value.Split(','); - requiredAfflictions ??= new HashSet<(string, float)>(); + requiredAfflictions ??= new HashSet<(Identifier, float)>(); for (int i = 0; i < types.Length; i++) { - requiredAfflictions.Add((types[i].Trim().ToLowerInvariant(), 0.0f)); + requiredAfflictions.Add((types[i].Trim().ToIdentifier(), 0.0f)); } break; case "duration": @@ -527,10 +523,10 @@ namespace Barotrauma lifeTimer = lifeTime; break; case "eventtargettag": - triggeredEventTargetTag = attribute.Value; + triggeredEventTargetTag = attribute.Value.ToIdentifier(); break; case "evententitytag": - triggeredEventEntityTag = attribute.Value; + triggeredEventEntityTag = attribute.Value.ToIdentifier(); break; case "checkconditionalalways": CheckConditionalAlways = attribute.GetAttributeBool(false); @@ -574,19 +570,19 @@ namespace Barotrauma int count = propertyAttributes.Count; - propertyNames = new string[count]; + propertyNames = new Identifier[count]; propertyEffects = new object[count]; int n = 0; foreach (XAttribute attribute in propertyAttributes) { - propertyNames[n] = attribute.Name.ToString().ToLowerInvariant(); + propertyNames[n] = attribute.NameAsIdentifier(); propertyEffects[n] = XMLExtensions.GetAttributeObject(attribute); n++; } - foreach (XElement subElement in element.Elements()) + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -625,9 +621,9 @@ namespace Barotrauma requiredItems.Add(newRequiredItem); break; case "requiredaffliction": - requiredAfflictions ??= new HashSet<(string, float)>(); - string[] ids = subElement.GetAttributeStringArray("identifier", null) ?? subElement.GetAttributeStringArray("type", new string[0]); - foreach (string afflictionId in ids) + requiredAfflictions ??= new HashSet<(Identifier, float)>(); + Identifier[] ids = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("type", Array.Empty()); + foreach (var afflictionId in ids) { requiredAfflictions.Add(( afflictionId, @@ -648,8 +644,8 @@ namespace Barotrauma if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names."); - string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.ToLowerInvariant() == afflictionName); + string afflictionName = subElement.GetAttributeString("name", ""); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); @@ -658,8 +654,8 @@ namespace Barotrauma } else { - string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found."); @@ -677,13 +673,12 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); ReduceAffliction.Add(( - subElement.GetAttributeString("name", "").ToLowerInvariant(), + subElement.GetAttributeIdentifier("name", ""), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } else { - string name = subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", null); - name = name.ToLowerInvariant(); + Identifier name = subElement.GetAttributeIdentifier("identifier", subElement.GetAttributeIdentifier("type", Identifier.Empty)); if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name)) { @@ -700,8 +695,8 @@ namespace Barotrauma if (newSpawnItem.ItemPrefab != null) { spawnItems.Add(newSpawnItem); } break; case "triggerevent": - string identifier = subElement.GetAttributeString("identifier", null); - if (!string.IsNullOrWhiteSpace(identifier)) + Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); + if (!identifier.IsEmpty) { EventPrefab prefab = EventSet.GetEventPrefab(identifier); if (prefab != null) @@ -709,15 +704,15 @@ namespace Barotrauma triggeredEvents.Add(prefab); } } - foreach (XElement eventElement in subElement.Elements()) + foreach (var eventElement in subElement.Elements()) { - if (!eventElement.Name.ToString().Equals("ScriptedEvent", StringComparison.OrdinalIgnoreCase)) { continue; } - triggeredEvents.Add(new EventPrefab(eventElement)); + if (eventElement.NameAsIdentifier() != "ScriptedEvent") { continue; } + triggeredEvents.Add(new EventPrefab(eventElement, file: null)); } break; case "spawncharacter": var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); - if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } + if (!newSpawnCharacter.SpeciesName.IsEmpty) { spawnCharacters.Add(newSpawnCharacter); } break; case "givetalentinfo": var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName); @@ -727,7 +722,7 @@ namespace Barotrauma aiTriggers.Add(new AITrigger(subElement)); break; case "talenttrigger": - talentTriggers.Add(subElement.GetAttributeString("effectidentifier", string.Empty)); + talentTriggers.Add(subElement.GetAttributeIdentifier("effectidentifier", Identifier.Empty)); break; case "giveexperience": giveExperiences.Add(subElement.GetAttributeInt("amount", 0)); @@ -740,7 +735,7 @@ namespace Barotrauma InitProjSpecific(element, parentDebugName); } - partial void InitProjSpecific(XElement element, string parentDebugName); + partial void InitProjSpecific(ContentXElement element, string parentDebugName); public bool HasTargetType(TargetType targetType) { @@ -832,8 +827,8 @@ namespace Barotrauma if (HasTargetType(TargetType.NearbyItems)) { //optimization for powered components that can be easily fetched from Powered.PoweredList - if (targetIdentifiers?.Count == 1 && - (targetIdentifiers.Contains("powered") || targetIdentifiers.Contains("junctionbox") || targetIdentifiers.Contains("relaycomponent"))) + if (TargetIdentifiers.Count == 1 && + (TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent"))) { foreach (Powered powered in Powered.PoweredList) { @@ -1010,79 +1005,45 @@ namespace Barotrauma } else if (entity is Structure structure) { - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("structure")) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("structure")) { return true; } + if (TargetIdentifiers.Contains(structure.Prefab.Identifier)) { return true; } } else if (entity is Character character) { return IsValidTarget(character); } - if (targetIdentifiers == null) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + return TargetIdentifiers.Contains(entity.Name); } protected bool IsValidTarget(ItemComponent itemComponent) { if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; } if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; } - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("itemcomponent")) { return true; } - if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("itemcomponent")) { return true; } + if (itemComponent.Item.HasTag(TargetIdentifiers)) { return true; } + return TargetIdentifiers.Contains(itemComponent.Item.Prefab.Identifier); } protected bool IsValidTarget(Item item) { if (OnlyInside && item.CurrentHull == null) { return false; } if (OnlyOutside && item.CurrentHull != null) { return false; } - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("item")) { return true; } - if (item.HasTag(targetIdentifiers)) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("item")) { return true; } + if (item.HasTag(TargetIdentifiers)) { return true; } + return TargetIdentifiers.Contains(item.Prefab.Identifier); } protected bool IsValidTarget(Character character) { if (OnlyInside && character.CurrentHull == null) { return false; } if (OnlyOutside && character.CurrentHull != null) { return false; } - if (targetIdentifiers == null) { return true; } - if (targetIdentifiers.Contains("character")) { return true; } - foreach (var id in targetIdentifiers) - { - if (id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - return false; + if (TargetIdentifiers == null) { return true; } + if (TargetIdentifiers.Contains("character")) { return true; } + return TargetIdentifiers.Contains(character.SpeciesName); } public void SetUser(Character user) @@ -1129,7 +1090,7 @@ namespace Barotrauma currentTargets.Add(target); } - if (targetIdentifiers != null && currentTargets.Count == 0) { return; } + if (TargetIdentifiers != null && currentTargets.Count == 0) { return; } bool hasRequiredItems = HasRequiredItems(entity); if (!hasRequiredItems || !HasRequiredConditions(currentTargets)) @@ -1259,14 +1220,14 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + if (targets[i] is Item item) { Entity.Spawner?.AddItemToRemoveQueue(item); } } } if (removeCharacter) { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } + if (targets[i] is Character character) { Entity.Spawner?.AddEntityToRemoveQueue(character); } } } if (breakLimb || hideLimb) @@ -1369,7 +1330,7 @@ namespace Barotrauma RegisterTreatmentResults(entity, limb, affliction, result); } } - + foreach (var (affliction, amount) in ReduceAffliction) { Limb targetLimb = null; @@ -1389,7 +1350,14 @@ namespace Barotrauma if (entity is Item item && item.UseInHealthInterface) { actionType = type; } float reduceAmount = amount * GetAfflictionMultiplier(entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + if (targetLimb != null) + { + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + } + else + { + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); + } targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality); if (user != null && user != targetCharacter) { @@ -1435,7 +1403,7 @@ namespace Barotrauma Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) { - foreach (string talentTrigger in talentTriggers) + foreach (Identifier talentTrigger in talentTriggers) { targetCharacter.CheckTalents(AbilityEffectType.OnStatusEffectIdentifier, new AbilityStatusEffectIdentifier(talentTrigger)); } @@ -1461,13 +1429,13 @@ namespace Barotrauma Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) { - string skillIdentifier = giveSkill.SkillIdentifier.ToLowerInvariant() == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; + Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier; targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount); - string GetRandomSkill() + Identifier GetRandomSkill() { - return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandom(); + return targetCharacter.Info?.Job?.Skills.Select(s => s.Identifier).GetRandomUnsynced() ?? Identifier.Empty; } } } @@ -1477,22 +1445,22 @@ namespace Barotrauma { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } - if (!TalentTree.JobTalentTrees.TryGetValue(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; } + if (!TalentTree.JobTalentTrees.TryGet(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; } // for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well - IEnumerable disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))); + IEnumerable disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier))); foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos) { - IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s)); + IEnumerable viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s)); if (viableTalents.None()) { continue; } if (giveTalentInfo.GiveRandom) { - targetCharacter.GiveTalent(viableTalents.GetRandom(Rand.RandSync.Unsynced), true); + targetCharacter.GiveTalent(viableTalents.GetRandomUnsynced(), true); } else { - foreach (string talent in viableTalents) + foreach (Identifier talent in viableTalents) { targetCharacter.GiveTalent(talent, true); } @@ -1518,7 +1486,7 @@ namespace Barotrauma if (ev is ScriptedEvent scriptedEvent) { - if (!string.IsNullOrWhiteSpace(triggeredEventTargetTag)) + if (!triggeredEventTargetTag.IsEmpty) { List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); @@ -1528,7 +1496,7 @@ namespace Barotrauma } } - if (!string.IsNullOrWhiteSpace(triggeredEventEntityTag) && entity != null) + if (!triggeredEventEntityTag.IsEmpty && entity != null) { scriptedEvent.Targets.Add(triggeredEventEntityTag, new List { entity }); } @@ -1543,7 +1511,7 @@ namespace Barotrauma var characters = new List(); for (int i = 0; i < characterSpawnInfo.Count; i++) { - Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, + Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { if (newCharacter.AIController is EnemyAIController enemyAi && @@ -1564,7 +1532,7 @@ namespace Barotrauma if (spawnItemRandomly) { - SpawnItem(spawnItems.GetRandom(Rand.RandSync.Unsynced)); + SpawnItem(spawnItems.GetRandomUnsynced()); } else { @@ -1583,7 +1551,7 @@ namespace Barotrauma switch (chosenItemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem => { Projectile projectile = newItem.GetComponent(); if (projectile != null && user != null && sourceBody != null && entity != null) @@ -1673,6 +1641,7 @@ namespace Barotrauma body.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); } } + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; }); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: @@ -1693,7 +1662,7 @@ namespace Barotrauma } if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) { - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => { if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) { @@ -1705,6 +1674,7 @@ namespace Barotrauma allowedSlots.Remove(InvSlotType.Any); character.Inventory.TryPutItem(item, null, allowedSlots); } + item.Condition = item.MaxCondition * chosenItemSpawnInfo.Condition; }); } } @@ -1732,7 +1702,10 @@ namespace Barotrauma Inventory containedInventory = item.GetComponent()?.Inventory; if (containedInventory != null && (containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) { - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull); + Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) => + { + newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition; + }); } break; } @@ -1853,7 +1826,7 @@ namespace Barotrauma element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); } } - + foreach (var (affliction, amount) in element.Parent.ReduceAffliction) { Limb targetLimb = null; @@ -1873,7 +1846,14 @@ namespace Barotrauma if (element.Entity is Item item && item.UseInHealthInterface) { actionType = element.Parent.type; } float reduceAmount = amount * element.Parent.GetAfflictionMultiplier(element.Entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); + if (targetLimb != null) + { + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount * deltaTime, treatmentAction: actionType); + } + else + { + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount * deltaTime, treatmentAction: actionType); + } if (element.User != null && element.User != targetCharacter) { targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs new file mode 100644 index 000000000..94676dc7c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/AuthTicket.cs @@ -0,0 +1,18 @@ +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + private static Steamworks.AuthTicket currentTicket = null; + public static Steamworks.AuthTicket GetAuthSessionTicket() + { + if (!IsInitialized) + { + return null; + } + + currentTicket?.Cancel(); + currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); + return currentTicket; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs similarity index 71% rename from Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs rename to Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs index 347af34f7..a3f7cb201 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/SteamManager.cs @@ -1,10 +1,7 @@ -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -#if USE_STEAM namespace Barotrauma.Steam { static partial class SteamManager @@ -13,16 +10,6 @@ namespace Barotrauma.Steam public const uint AppID = 602960; - private static readonly List initializationErrors = new List(); - public static IEnumerable InitializationErrors - { - get { return initializationErrors; } - } - - public const string MetadataFileName = "filelist.xml"; - - public const string CopyIndicatorFileName = ".copying"; - private static readonly Dictionary tagCommonness = new Dictionary() { { "submarine", 10 }, @@ -37,18 +24,17 @@ namespace Barotrauma.Steam { "language", 5 } }; + public static bool IsInitialized { get; private set; } + private static readonly List popularTags = new List(); public static IEnumerable PopularTags { get { - if (!isInitialized) { return Enumerable.Empty(); } + if (!IsInitialized) { return Enumerable.Empty(); } return popularTags; } } - - private static bool isInitialized; - public static bool IsInitialized => isInitialized; public static void Initialize() { @@ -57,7 +43,7 @@ namespace Barotrauma.Steam public static ulong GetSteamID() { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0; } @@ -65,42 +51,35 @@ namespace Barotrauma.Steam return Steamworks.SteamClient.SteamId; } + public static bool IsFamilyShared() + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + + return Steamworks.SteamApps.IsSubscribedFromFamilySharing; + } + + public static bool IsFreeWeekend() + { + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + + return Steamworks.SteamApps.IsSubscribedFromFamilySharing; + } + public static string GetUsername() { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return ""; } return Steamworks.SteamClient.Name; } - private static Steamworks.AuthTicket currentTicket = null; - public static Steamworks.AuthTicket GetAuthSessionTicket() - { - if (!isInitialized) - { - return null; - } + public static bool UnlockAchievement(string achievementIdentifier) => + UnlockAchievement(achievementIdentifier.ToIdentifier()); - currentTicket?.Cancel(); - currentTicket = Steamworks.SteamUser.GetAuthSessionTicket(); - return currentTicket; - } - - public static bool OverlayCustomURL(string url) + public static bool UnlockAchievement(Identifier achievementIdentifier) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) - { - return false; - } - - Steamworks.SteamFriends.OpenWebOverlay(url); - return true; - } - - public static bool UnlockAchievement(string achievementIdentifier) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } @@ -116,24 +95,20 @@ namespace Barotrauma.Steam //SteamAchievementManager tries to unlock achievements that may or may not exist //(discovered[whateverbiomewasentered], kill[withwhateveritem], kill[somemonster] etc) so that we can add //some types of new achievements without the need for client-side changes. -#if DEBUG - DebugConsole.NewMessage("Failed to unlock achievement \"" + achievementIdentifier + "\"."); -#endif + DebugConsole.Log($"Failed to unlock achievement \"{achievementIdentifier}\"."); } return unlocked; } - public static bool IncrementStat(string statName, int increment) + public static bool IncrementStat(Identifier statName, int increment) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } - DebugConsole.Log("Incremented stat \"" + statName + "\" by " + increment); - bool success = Steamworks.SteamUserStats.AddStat(statName, increment); + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment); + bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment); if (!success) { -#if DEBUG - DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); -#endif + DebugConsole.Log("Failed to increment stat \"" + statName + "\"."); } else { @@ -142,16 +117,14 @@ namespace Barotrauma.Steam return success; } - public static bool IncrementStat(string statName, float increment) + public static bool IncrementStat(Identifier statName, float increment) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } - DebugConsole.Log("Incremented stat \"" + statName + "\" by " + increment); - bool success = Steamworks.SteamUserStats.AddStat(statName, increment); + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } + DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment); + bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment); if (!success) { -#if DEBUG - DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); -#endif + DebugConsole.Log("Failed to increment stat \"" + statName + "\"."); } else { @@ -160,29 +133,27 @@ namespace Barotrauma.Steam return success; } - public static int GetStatInt(string statName) + public static int GetStatInt(Identifier statName) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return 0; } - return Steamworks.SteamUserStats.GetStatInt(statName); + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0; } + return Steamworks.SteamUserStats.GetStatInt(statName.Value.ToLowerInvariant()); } public static bool StoreStats() { - if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; } DebugConsole.Log("Storing Steam stats..."); bool success = Steamworks.SteamUserStats.StoreStats(); if (!success) { -#if DEBUG - DebugConsole.NewMessage("Failed to store Steam stats."); -#endif + DebugConsole.Log("Failed to store Steam stats."); } return success; } public static bool TryGetUnlockedAchievements(out List achievements) { - if (!isInitialized || !Steamworks.SteamClient.IsValid) + if (!IsInitialized || !Steamworks.SteamClient.IsValid) { achievements = null; return false; @@ -193,7 +164,7 @@ namespace Barotrauma.Steam public static void Update(float deltaTime) { - if (!isInitialized) { return; } + if (!IsInitialized) { return; } if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.RunCallbacks(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.RunCallbacks(); } @@ -203,11 +174,11 @@ namespace Barotrauma.Steam public static void ShutDown() { - if (!isInitialized) { return; } + if (!IsInitialized) { return; } if (Steamworks.SteamClient.IsValid) { Steamworks.SteamClient.Shutdown(); } if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); } - isInitialized = false; + IsInitialized = false; } public static IEnumerable ParseWorkshopIds(string workshopIdData) @@ -246,7 +217,7 @@ namespace Barotrauma.Steam try { Uri uri = new Uri(url); - string idStr = HttpUtility.ParseQueryString(uri.Query)["id"]; + string idStr = HttpUtility.ParseQueryString(uri.Query)["id".ToIdentifier()]; if (ulong.TryParse(idStr, out ulong id)) { return id; @@ -265,7 +236,7 @@ namespace Barotrauma.Steam if (string.IsNullOrWhiteSpace(str)) { return 0; } UInt64 retVal; if (str.StartsWith("STEAM64_", StringComparison.InvariantCultureIgnoreCase)) { str = str.Substring(8); } - if (UInt64.TryParse(str, out retVal) && retVal >(1<<52)) { return retVal; } + if (UInt64.TryParse(str, out retVal) && retVal > (1 << 52)) { return retVal; } if (!str.StartsWith("STEAM_", StringComparison.InvariantCultureIgnoreCase)) { return 0; } string[] split = str.Substring(6).Split(':'); if (split.Length != 3) { return 0; } @@ -293,4 +264,3 @@ namespace Barotrauma.Steam } } } -#endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs new file mode 100644 index 000000000..382b21282 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -0,0 +1,441 @@ +#nullable enable +using Barotrauma.IO; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Steamworks.Data; +using WorkshopItemSet = System.Collections.Generic.ISet; + +namespace Barotrauma.Steam +{ + static partial class SteamManager + { + public const string WorkshopItemPreviewImageFolder = "Workshop"; + public const string PreviewImageName = "PreviewImage.png"; + public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png"; + + public static partial class Workshop + { + private struct ItemEqualityComparer : IEqualityComparer + { + public static readonly ItemEqualityComparer Instance = new ItemEqualityComparer(); + + public bool Equals(Steamworks.Ugc.Item x, Steamworks.Ugc.Item y) + => x.Id == y.Id; + + public int GetHashCode(Steamworks.Ugc.Item obj) + => (int)obj.Id.Value; + } + + private static async Task GetWorkshopItems(Steamworks.Ugc.Query query, int? maxPages = null) + { + if (!IsInitialized) { return new HashSet(); } + + await Task.Yield(); + query = query.WithKeyValueTags(true).WithLongDescription(true); + var set = new HashSet(ItemEqualityComparer.Instance); + int prevSize = 0; + for (int i = 1; maxPages is null || i <= maxPages; i++) + { + Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i); + if (page is null || !page.Value.Entries.Any()) { break; } + set.UnionWith(page.Value.Entries); + + if (set.Count == prevSize) { break; } + prevSize = set.Count; + } + return set; + } + + public static async Task GetAllSubscribedItems() + { + if (!IsInitialized) { return new HashSet(); } + + return await GetWorkshopItems( + Steamworks.Ugc.Query.Items + .WhereUserSubscribed()); + } + + public static async Task GetPopularItems() + { + if (!IsInitialized) { return new HashSet(); } + + return await GetWorkshopItems( + Steamworks.Ugc.Query.Items + .WithTrendDays(7) + .RankedByTrend(), maxPages: 1); + } + + public static async Task GetPublishedItems() + { + if (!IsInitialized) { return new HashSet(); } + + return await GetWorkshopItems( + Steamworks.Ugc.Query.All + .WhereUserPublished()); + } + + public static async Task GetItem(UInt64 itemId) + { + if (!IsInitialized) { return null; } + + var items = await GetWorkshopItems( + Steamworks.Ugc.Query.All + .WithFileId(itemId)); + return items.Any() ? items.First() : (Steamworks.Ugc.Item?)null; + } + + public static async Task ForceRedownload(UInt64 itemId) + => await ForceRedownload(new Steamworks.Ugc.Item(itemId)); + + public static void NukeDownload(Steamworks.Ugc.Item item) + { + try + { + System.IO.Directory.Delete(item.Directory, recursive: true); + } + catch + { + //don't care in the slightest about what happens here + } + } + + public static void Uninstall(Steamworks.Ugc.Item workshopItem) + { + NukeDownload(workshopItem); + var toUninstall + = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + .ToHashSet(); + toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } + + public static async Task ForceRedownload(Steamworks.Ugc.Item item) + { + NukeDownload(item); + await item.DownloadAsync(); + } + + /// + /// This class creates a file called ".copying" that + /// serves to keep mod copy operations in the same + /// directory from overlapping. + /// + private class CopyIndicator : IDisposable + { + private readonly string path; + + public CopyIndicator(string path) + { + this.path = path; + using (var f = File.Create(path)) + { + if (f is null) + { + throw new Exception($"File.Create returned null"); + } + f.WriteByte((byte)0); + } + } + + public void Dispose() + { + try + { + File.Delete(path); + } + catch + { + //don't care! + } + } + } + + /// + /// This class serves the purpose of preventing + /// more than 10 mod install tasks from proceeding + /// at the same time. + /// + private class InstallTaskCounter : IDisposable + { + private static readonly HashSet installers = new HashSet(); + private readonly static object mutex = new object(); + private const int MaxTasks = 7; + + private readonly UInt64 itemId; + private InstallTaskCounter(UInt64 id) { itemId = id; } + + public static bool IsInstalling(Steamworks.Ugc.Item item) + { + lock (mutex) + { + return installers.Any(i => i.itemId == item.Id); + } + } + + private async Task Init() + { + await Task.Yield(); + while (true) + { + lock (mutex) + { + if (installers.Count < MaxTasks) { installers.Add(this); return; } + } + await Task.Delay(5000); + } + } + + public static async Task Create(Steamworks.Ugc.Item item) + { + var retVal = new InstallTaskCounter(item.Id); + await retVal.Init(); + return retVal; + } + + public void Dispose() + { + lock (mutex) { installers.Remove(this); } + } + } + + public static bool IsItemDirectoryUpToDate(in Steamworks.Ugc.Item item) + { + string itemDirectory = item.Directory; + return Directory.Exists(itemDirectory) + && File.GetLastWriteTime(itemDirectory).ToUniversalTime() >= item.LatestUpdateTime; + } + + public static bool CanBeInstalled(ulong itemId) + => CanBeInstalled(new Steamworks.Ugc.Item(itemId)); + + public static bool CanBeInstalled(in Steamworks.Ugc.Item item) + { + bool needsUpdate = item.NeedsUpdate; + bool isDownloading = item.IsDownloading; + bool isInstalled = item.IsInstalled; + bool directoryIsUpToDate = IsItemDirectoryUpToDate(item); + + return !needsUpdate + && !isDownloading + && isInstalled + && directoryIsUpToDate; + } + + public static async Task DownloadModThenEnqueueInstall(Steamworks.Ugc.Item item) + { + if (!CanBeInstalled(item)) + { + if (!item.IsDownloading && !item.IsDownloadPending) { await ForceRedownload(item); } + } +#if CLIENT + else + { + OnItemDownloadComplete(item.Id); + } +#endif + } + + public static void DeleteFailedCopies() + { + foreach (var dir in Directory.EnumerateDirectories(ContentPackage.WorkshopModsDir, "**")) + { + string copyingIndicatorPath = Path.Combine(dir, ContentPackageManager.CopyIndicatorFileName); + if (File.Exists(copyingIndicatorPath)) + { + Directory.Delete(dir, recursive: true); + } + } + } + + public static bool IsInstallingToPath(string path) + => File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName)); + + public static bool IsInstalling(Steamworks.Ugc.Item item) + => InstallTaskCounter.IsInstalling(item); + + private static async Task InstallMod(ulong id) + { + var item = await GetItem(id); + if (item is null) { return; } + await InstallMod(item.Value); + } + + private static async Task InstallMod(Steamworks.Ugc.Item item) + { + await Task.Yield(); + using var installCounter = await InstallTaskCounter.Create(item); + + string itemTitle = item.Title.Trim(); + UInt64 itemId = item.Id; + string itemDirectory = item.Directory; + DateTime updateTime = item.LatestUpdateTime; + + if (!CanBeInstalled(item)) + { + ForceRedownload(item); + throw new InvalidOperationException($"Item {itemTitle} (id {itemId}) is not available for copying"); + } + + const string workshopModDirReadme = + "DO NOT MODIFY THE CONTENTS OF THIS FOLDER, EVEN IF\n" + + "YOU ARE EDITING A MOD YOU PUBLISHED YOURSELF.\n" + + "\n" + + "If you do you may run into networking issues and\n" + + "unexpected deletion of your hard work.\n" + + "Instead, modify a copy of your mod in LocalMods.\n"; + + string workshopModDirReadmeLocation = Path.Combine(SaveUtil.SaveFolder, "WorkshopMods", "README.txt"); + if (!File.Exists(workshopModDirReadmeLocation)) + { + Directory.CreateDirectory(Path.GetDirectoryName(workshopModDirReadmeLocation)!); + File.WriteAllText( + path: workshopModDirReadmeLocation, + contents: workshopModDirReadme); + } + + string installDir = Path.Combine(ContentPackage.WorkshopModsDir, itemId.ToString()); + Directory.CreateDirectory(installDir); + + string copyIndicatorPath = Path.Combine(installDir, ContentPackageManager.CopyIndicatorFileName); + + XDocument fileListSrc = XMLExtensions.TryLoadXml(Path.Combine(itemDirectory, ContentPackage.FileListFileName)); + string modName = fileListSrc.Root.GetAttributeString("name", item.Title).Trim(); + string modVersion = fileListSrc.Root.GetAttributeString("modversion", ContentPackage.DefaultModVersion); + Version gameVersion = fileListSrc.Root.GetAttributeVersion("gameversion", GameMain.Version); + bool isCorePackage = fileListSrc.Root.GetAttributeBool("corepackage", false); + string expectedHash = fileListSrc.Root.GetAttributeString("expectedhash", ""); + + using (var copyIndicator = new CopyIndicator(copyIndicatorPath)) + { + await CopyDirectory(itemDirectory, modName, itemDirectory, installDir); + + string fileListDestPath = Path.Combine(installDir, ContentPackage.FileListFileName); + XDocument fileListDest = XMLExtensions.TryLoadXml(fileListDestPath); + XElement root = fileListDest.Root ?? throw new NullReferenceException("Unable to install mod: file list root is null."); + root.Attributes().Remove(); + + root.Add( + new XAttribute("name", itemTitle), + new XAttribute("steamworkshopid", itemId), + new XAttribute("corepackage", isCorePackage), + new XAttribute("modversion", modVersion), + new XAttribute("gameversion", gameVersion), + new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime))); + if (modName.ToIdentifier() != itemTitle) + { + root.Add(new XAttribute("altnames", modName)); + } + if (!expectedHash.IsNullOrEmpty()) + { + root.Add(new XAttribute("expectedhash", expectedHash)); + } + fileListDest.SaveSafe(fileListDestPath); + } + } + + private static async Task CorrectPaths(string fileListDir, string modName, XElement element) + { + foreach (var attribute in element.Attributes()) + { + await Task.Yield(); + + string val = attribute.Value.CleanUpPathCrossPlatform(correctFilenameCase: false); + + //Handle really old mods (0.9.0.4-era) that might be structured as + //%ModDir%/Mods/[NAME]/[RESOURCE] + string fullSrcPath = Path.Combine(fileListDir, val).CleanUpPath(); + if (File.Exists(fullSrcPath)) + { + val = $"{ContentPath.ModDirStr}/{val}"; + } + + //Handle old mods that installed to the fixed Mods directory + //that no longer exists + string oldModDir = $"Mods/{modName}"; + if (val.StartsWith(oldModDir, StringComparison.OrdinalIgnoreCase)) + { + val = $"{ContentPath.ModDirStr}{val.Remove(0, oldModDir.Length)}"; + } + //Handle old mods that depend on other mods + else if (val.StartsWith("Mods/", StringComparison.OrdinalIgnoreCase)) + { + 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)}"; + } + //Handle really old mods that installed Submarines in the wrong place + else if (val.StartsWith("Submarines/", StringComparison.OrdinalIgnoreCase)) + { + val = $"{ContentPath.ModDirStr}/{val}"; + } + attribute.Value = val; + } + await Task.WhenAll( + element.Elements() + .Select(subElement => CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: subElement))); + } + + private static async Task CopyFile(string fileListDir, string modName, string from, string to) + { + await Task.Yield(); + Identifier extension = Path.GetExtension(from).ToIdentifier(); + if (extension == ".xml") + { + try + { + XDocument? doc = XMLExtensions.TryLoadXml(from, out var exception); + if (exception is { Message: string exceptionMsg }) + { + throw new Exception($"Could not load \"{from}\": {exceptionMsg}"); + } + if (doc is null) + { + throw new Exception($"Could not load \"{from}\": doc is null"); + } + await CorrectPaths( + fileListDir: fileListDir, + modName: modName, + element: doc.Root ?? throw new NullReferenceException()); + doc.SaveSafe(to); + return; + } + catch (Exception e) + { + DebugConsole.ThrowError( + $"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}"); + } + } + File.Copy(from, to, overwrite: true); + } + + private static async Task CopyDirectory(string fileListDir, string modName, string from, string to) + { + from = Path.GetFullPath(from); to = Path.GetFullPath(to); + Directory.CreateDirectory(to); + + string convertFromTo(string from) + => Path.Combine(to, Path.GetFileName(from)); + + string[] files = Directory.GetFiles(from); + string[] subDirs = Directory.GetDirectories(from); + foreach (var file in files) + { + await CopyFile(fileListDir, modName, file, convertFromTo(file)); + } + + foreach (var dir in subDirs) { await CopyDirectory(fileListDir, modName, dir, convertFromTo(dir)); } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index c6b96185c..4a5731b4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -14,7 +14,7 @@ namespace Barotrauma { private const float UpdateInterval = 1.0f; - private static HashSet unlockedAchievements = new HashSet(); + private static HashSet unlockedAchievements = new HashSet(); public static bool CheatsEnabled = false; @@ -68,7 +68,7 @@ namespace Barotrauma { if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f) { - UnlockAchievement("maxintensity", true, c => c != null && !c.IsDead && !c.IsUnconscious); + UnlockAchievement("maxintensity".ToIdentifier(), true, c => c != null && !c.IsDead && !c.IsUnconscious); } foreach (Character c in Character.CharacterList) @@ -84,7 +84,7 @@ namespace Barotrauma else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth * 0.5f) { //all characters that have entered crush depth and are still alive get an achievement - if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); + if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); } } } @@ -110,7 +110,7 @@ namespace Barotrauma if (Math.Abs(submarineVel.X) > 100.0f) { //all conscious characters inside the sub get an achievement - UnlockAchievement("subhighvelocity", true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); + UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); } //achievement for descending ridiculously deep @@ -118,7 +118,7 @@ namespace Barotrauma if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { //all conscious characters inside the sub get an achievement - UnlockAchievement("subdeep", true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); + UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); } } @@ -151,13 +151,13 @@ namespace Barotrauma { if (c == null || c.Removed) { return; } - if (c.HasEquippedItem("clownmask") && - c.HasEquippedItem("clowncostume")) + if (c.HasEquippedItem("clownmask".ToIdentifier()) && + c.HasEquippedItem("clowncostume".ToIdentifier())) { - UnlockAchievement(c, "clowncostume"); + UnlockAchievement(c, "clowncostume".ToIdentifier()); } - if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) + if (Submarine.MainSub != null && c.Submarine == null && c.SpeciesName == CharacterPrefab.HumanSpeciesName) { float requiredDist = 500 / Physics.DisplayToRealWorldRatio; float distSquared = Vector2.DistanceSquared(c.WorldPosition, Submarine.MainSub.WorldPosition); @@ -187,7 +187,7 @@ namespace Barotrauma } if (distSquared > requiredDist * requiredDist) { - UnlockAchievement(c, "crewaway"); + UnlockAchievement(c, "crewaway".ToIdentifier()); } static CachedDistance CalculateNewCachedDistance(Character c) @@ -218,7 +218,7 @@ namespace Barotrauma public static void OnBiomeDiscovered(Biome biome) { - UnlockAchievement("discover" + biome.Identifier.ToLowerInvariant().Replace(" ", "")); + UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier()); } public static void OnItemRepaired(Item item, Character fixer) @@ -228,13 +228,13 @@ namespace Barotrauma #endif if (fixer == null) { return; } - UnlockAchievement(fixer, "repairdevice"); - UnlockAchievement(fixer, "repair" + item.Prefab.Identifier); + UnlockAchievement(fixer, "repairdevice".ToIdentifier()); + UnlockAchievement(fixer, $"repair{item.Prefab.Identifier}".ToIdentifier()); } public static void OnAfflictionRemoved(Affliction affliction, Character character) { - if (string.IsNullOrEmpty(affliction.Prefab.AchievementOnRemoved)) { return; } + if (affliction.Prefab.AchievementOnRemoved.IsEmpty) { return; } #if CLIENT if (GameMain.Client != null) { return; } @@ -248,7 +248,7 @@ namespace Barotrauma if (GameMain.Client != null) { return; } #endif if (reviver == null) { return; } - UnlockAchievement(reviver, "healcrit"); + UnlockAchievement(reviver, "healcrit".ToIdentifier()); } public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath) @@ -260,68 +260,67 @@ namespace Barotrauma causeOfDeath.Killer != null && causeOfDeath.Killer == Character.Controlled) { - IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); + IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1); } #elif SERVER if (character != causeOfDeath.Killer && causeOfDeath.Killer != null) { - IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); + IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1); } #endif roundData?.Casualties.Add(character); - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier()); if (character.CurrentHull != null) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName + "indoors"); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier()); } if (character.SpeciesName.EndsWith("boss")) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "")); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier()); if (character.CurrentHull != null) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("boss", "") + "indoors"); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier()); } } if (character.SpeciesName.EndsWith("_m")) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("_m", "")); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier()); if (character.CurrentHull != null) { - UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName.Replace("_m", "") + "indoors"); + UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier()); } } - if (character.HasEquippedItem("clownmask") && - character.HasEquippedItem("clowncostume") && + if (character.HasEquippedItem("clownmask".ToIdentifier()) && + character.HasEquippedItem("clowncostume".ToIdentifier()) && causeOfDeath.Killer != character) { - UnlockAchievement(causeOfDeath.Killer, "killclown"); + UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier()); } if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null) { - UnlockAchievement(causeOfDeath.Killer, "killpoison"); + UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); } if (causeOfDeath.DamageSource is Item item) { if (item.HasTag("tool")) { - UnlockAchievement(causeOfDeath.Killer, "killtool"); + UnlockAchievement(causeOfDeath.Killer, "killtool".ToIdentifier()); } else { - switch (item.Prefab.Identifier) + if (item.Prefab.Identifier == "morbusine") { - case "morbusine": - UnlockAchievement(causeOfDeath.Killer, "killpoison"); - break; - case "nuclearshell": - case "nucleardepthcharge": - UnlockAchievement(causeOfDeath.Killer, "killnuke"); - break; + UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier()); + } + else if (item.Prefab.Identifier == "nuclearshell" || + item.Prefab.Identifier == "nucleardepthcharge") + { + UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier()); } } } @@ -331,7 +330,7 @@ namespace Barotrauma { if (GameMain.Server.TraitorManager.IsTraitor(character)) { - UnlockAchievement(causeOfDeath.Killer, "killtraitor"); + UnlockAchievement(causeOfDeath.Killer, "killtraitor".ToIdentifier()); } } #endif @@ -342,7 +341,7 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null || GameMain.GameSession == null) { return; } #endif - UnlockAchievement(character, "traitorwin"); + UnlockAchievement(character, "traitorwin".ToIdentifier()); } public static void OnRoundEnded(GameSession gameSession) @@ -363,14 +362,14 @@ namespace Barotrauma !myCharacter.IsDead && (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost))) { - IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers); } #endif } else { //in sp making it to the end is enough - IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers); } } @@ -384,7 +383,10 @@ namespace Barotrauma if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) { //all characters that are alive and in the winning team get an achievement - UnlockAchievement(mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, + var achvIdentifier = + $"{mission.Prefab.AchievementIdentifier}{(int) GameMain.GameSession.WinningTeam}" + .ToIdentifier(); + UnlockAchievement(achvIdentifier, true, c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c)); } else if (mission.Completed) @@ -410,18 +412,18 @@ namespace Barotrauma if (GameMain.Server != null) { //in MP all characters that were inside the sub during reactor meltdown and still alive at the end of the round get an achievement - UnlockAchievement("survivereactormeltdown", true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c)); + UnlockAchievement("survivereactormeltdown".ToIdentifier(), true, c => c != null && !c.IsDead && roundData.ReactorMeltdown.Contains(c)); if (noDamageRun) { - UnlockAchievement("nodamagerun", true, c => c != null && !c.IsDead); + UnlockAchievement("nodamagerun".ToIdentifier(), true, c => c != null && !c.IsDead); } } #endif #if CLIENT - if (noDamageRun) { UnlockAchievement("nodamagerun"); } + if (noDamageRun) { UnlockAchievement("nodamagerun".ToIdentifier()); } if (roundData.ReactorMeltdown.Any()) //in SP getting to the destination after a meltdown is enough { - UnlockAchievement("survivereactormeltdown"); + UnlockAchievement("survivereactormeltdown".ToIdentifier()); } #endif var charactersInSub = Character.CharacterList.FindAll(c => @@ -435,12 +437,12 @@ namespace Barotrauma //there must be some non-enemy casualties to get the last mant standing achievement if (roundData.Casualties.Any(c => !(c.AIController is EnemyAIController) && c.TeamID == charactersInSub[0].TeamID)) { - UnlockAchievement(charactersInSub[0], "lastmanstanding"); + UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier()); } #if CLIENT else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1) { - UnlockAchievement(charactersInSub[0], "lonesailor"); + UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier()); } #else //lone sailor achievement if alone in the sub and there are no other characters with the same team ID @@ -449,7 +451,7 @@ namespace Barotrauma c.TeamID == charactersInSub[0].TeamID && !(c.AIController is EnemyAIController))) { - UnlockAchievement(charactersInSub[0], "lonesailor"); + UnlockAchievement(charactersInSub[0], "lonesailor".ToIdentifier()); } #endif @@ -457,14 +459,14 @@ namespace Barotrauma foreach (Character character in charactersInSub) { if (character.Info.Job == null) { continue; } - UnlockAchievement(character, character.Info.Job.Prefab.Identifier + "round"); + UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier()); } } pathFinder = null; } - private static void UnlockAchievement(Character recipient, string identifier) + private static void UnlockAchievement(Character recipient, Identifier identifier) { if (CheatsEnabled || recipient == null) { return; } #if CLIENT @@ -477,7 +479,7 @@ namespace Barotrauma #endif } - private static void IncrementStat(Character recipient, string identifier, int amount) + private static void IncrementStat(Character recipient, Identifier identifier, int amount) { if (CheatsEnabled || recipient == null) { return; } #if CLIENT @@ -490,23 +492,22 @@ namespace Barotrauma #endif } - public static void IncrementStat(string identifier, int amount) + public static void IncrementStat(Identifier identifier, int amount) { if (CheatsEnabled) { return; } SteamManager.IncrementStat(identifier, amount); } - public static void IncrementStat(string identifier, float amount) + public static void IncrementStat(Identifier identifier, float amount) { if (CheatsEnabled) { return; } SteamManager.IncrementStat(identifier, amount); } - public static void UnlockAchievement(string identifier, bool unlockClients = false, Func conditions = null) + public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func conditions = null) { if (CheatsEnabled) { return; } - identifier = identifier.ToLowerInvariant(); - + #if SERVER if (unlockClients && GameMain.Server != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs new file mode 100644 index 000000000..21ba71b98 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/AddedPunctuationLString.cs @@ -0,0 +1,35 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class AddedPunctuationLString : LocalizedString + { + private readonly ImmutableArray nestedStrs; + private readonly char punctuationSymbol; + + public AddedPunctuationLString(char symbol, params LocalizedString[] nStrs) { nestedStrs = nStrs.ToImmutableArray(); punctuationSymbol = symbol; } + + public override bool Loaded => nestedStrs.All(s => s.Loaded); + public override void RetrieveValue() + { + string separator = ""; + if (GameSettings.CurrentConfig.Language == "French".ToLanguageIdentifier()) + { + bool addNonBreakingSpace = + punctuationSymbol == ':' || punctuationSymbol == ';' || + punctuationSymbol == '!' || punctuationSymbol == '?'; + separator = addNonBreakingSpace ? + new string(new char[] { (char)(0xA0), punctuationSymbol, ' ' }) : + new string(new char[] { punctuationSymbol, ' ' }); + } + else + { + separator = new string(new char[] { punctuationSymbol, ' ' }); + } + cachedValue = string.Join(separator, nestedStrs.Select(str => str.Value)); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs new file mode 100644 index 000000000..a056f31dd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/CapitalizeLString.cs @@ -0,0 +1,25 @@ +#nullable enable +namespace Barotrauma +{ + public class CapitalizeLString : LocalizedString + { + private readonly LocalizedString nestedStr; + + public CapitalizeLString(LocalizedString nStr) { nestedStr = nStr; } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + string str = nestedStr.Value; + if (!string.IsNullOrEmpty(str)) + { + cachedValue = char.ToUpper(str[0]) + str[1..]; + } + else + { + cachedValue = ""; + } + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs new file mode 100644 index 000000000..8048609fc --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ConcatLString.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace Barotrauma +{ + public class ConcatLString : LocalizedString + { + private readonly LocalizedString left; + private readonly LocalizedString right; + + public ConcatLString(LocalizedString l, LocalizedString r) + { + left = l; right = r; + } + + public override bool Loaded => left.Loaded || right.Loaded; + public override void RetrieveValue() + { + cachedValue = left.Value + right.Value; + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs new file mode 100644 index 000000000..d4ca38f08 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FallbackLString.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace Barotrauma +{ + public class FallbackLString : LocalizedString + { + private readonly LocalizedString primary; + private readonly LocalizedString fallback; + + private bool primaryIsLoaded = false; + + public FallbackLString(LocalizedString primary, LocalizedString fallback) + { + if (primary is FallbackLString {primary: { } innerPrimary, fallback: { } innerFallback}) + { + this.primary = innerPrimary; + this.fallback = innerFallback.Fallback(fallback); + } + else + { + this.primary = primary; + this.fallback = fallback; + } + } + + protected override bool MustRetrieveValue() + { + return base.MustRetrieveValue() + || MustRetrieveValue(primary) + || MustRetrieveValue(fallback) + || primaryIsLoaded != primary.Loaded; + } + + public override bool Loaded => primary.Loaded || fallback.Loaded; + public override void RetrieveValue() + { + cachedValue = primary.Value; + primaryIsLoaded = primary.Loaded; + if (!primary.Loaded) + { + cachedValue = fallback.Value; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs new file mode 100644 index 000000000..7e652ecec --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/FormattedLString.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class FormattedLString : LocalizedString + { + private readonly LocalizedString str; + private readonly ImmutableArray subStrs; + public FormattedLString(LocalizedString str, params LocalizedString[] subStrs) + { + this.str = str; + this.subStrs = subStrs.ToImmutableArray(); + } + + public override bool Loaded => str.Loaded && subStrs.All(s => s.Loaded); + public override void RetrieveValue() + { + //TODO: possibly broken! + cachedValue = string.Format(str.Value, subStrs.Select(s => s.Value as object).ToArray()); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs new file mode 100644 index 000000000..cfd7c255b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/InputTypeLString.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; + +namespace Barotrauma +{ + public class InputTypeLString : LocalizedString + { + private readonly LocalizedString nestedStr; + public InputTypeLString(LocalizedString nStr) { nestedStr = nStr; } + + protected override bool MustRetrieveValue() + { + //TODO: check for config changes! + return base.MustRetrieveValue(); + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value; +#if CLIENT + //TODO: server shouldn't have this type at all + foreach (InputType? inputType in Enum.GetValues(typeof(InputType))) + { + if (!inputType.HasValue) { continue; } + cachedValue = cachedValue.Replace($"[{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase); + cachedValue = cachedValue.Replace($"[InputType.{inputType}]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType.Value).Value, StringComparison.OrdinalIgnoreCase); + } +#endif + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs new file mode 100644 index 000000000..9571bdb2f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/JoinLString.cs @@ -0,0 +1,24 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + public class JoinLString : LocalizedString + { + private readonly IEnumerable subStrs; + private readonly string separator; + + public JoinLString(string separator, IEnumerable subStrs) + { + this.separator = separator; this.subStrs = subStrs; + } + + public override bool Loaded => subStrs.All(s => s.Loaded); + public override void RetrieveValue() + { + cachedValue = string.Join(separator, subStrs); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs new file mode 100644 index 000000000..7f27612ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -0,0 +1,177 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + public abstract class LocalizedString : IComparable + { + protected enum LoadedSuccessfully + { + Unknown, + No, + Yes + } + + protected LanguageIdentifier language { get; private set; } = LanguageIdentifier.None; + private int languageVersion = 0; + + protected string cachedValue = ""; + public string Value + { + get + { + if (MustRetrieveValue()) { RetrieveValue(); } + return cachedValue; + } + } + + public int Length => Value.Length; + + public abstract bool Loaded { get; } + + protected void UpdateLanguage() + { + language = GameSettings.CurrentConfig.Language; + languageVersion = TextManager.LanguageVersion; + } + + protected virtual bool MustRetrieveValue() //this can't be called on other LocalizedStrings by derived classes + { + return language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; + } + + protected static bool MustRetrieveValue(LocalizedString str) //this can be called by derived classes + { + return str.MustRetrieveValue(); + } + + public abstract void RetrieveValue(); + + public static implicit operator LocalizedString(string value) => new RawLString(value); + public static implicit operator LocalizedString(char value) => new RawLString(value.ToString()); + + public static LocalizedString operator+(LocalizedString left, LocalizedString right) => new ConcatLString(left, right); + public static LocalizedString operator+(LocalizedString left, object right) => left + (right.ToString() ?? ""); + public static LocalizedString operator+(object left, LocalizedString right) => (left.ToString() ?? "") + right; + + public static bool operator==(LocalizedString? left, LocalizedString? right) + { + return left?.Value == right?.Value; + } + + public static bool operator!=(LocalizedString? left, LocalizedString? right) + { + return !(left == right); + } + + public override string ToString() + { + return Value; + } + + public bool Contains(string subStr, StringComparison comparison = StringComparison.Ordinal) + { + return !Value.IsNullOrEmpty() && Value.Contains(subStr, comparison); + } + + public bool Contains(char chr, StringComparison comparison = StringComparison.Ordinal) + { + return Value.Contains(chr, comparison); + } + + public virtual LocalizedString ToUpper() + { + return new UpperLString(this); + } + + public static LocalizedString Join(string separator, params LocalizedString[] subStrs) + { + return Join(separator, (IEnumerable)subStrs); + } + + public static LocalizedString Join(string separator, IEnumerable subStrs) + { + return new JoinLString(separator, subStrs); + } + + public LocalizedString Fallback(LocalizedString fallback) + { + return new FallbackLString(this, fallback); + } + + public IReadOnlyList Split(params char[] separators) + { + var splitter = new LStringSplitter(this, separators); + return splitter.Substrings; + } + + public LocalizedString Replace(Identifier find, LocalizedString replace, StringComparison stringComparison = StringComparison.Ordinal) + { + return new ReplaceLString(this, stringComparison, (find, replace)); + } + + public LocalizedString Replace(string find, LocalizedString replace, StringComparison stringComparison = StringComparison.Ordinal) + { + return new ReplaceLString(this, stringComparison, (find.ToIdentifier(), replace)); + } + + public LocalizedString Replace(LocalizedString find, LocalizedString replace, + StringComparison stringComparison = StringComparison.Ordinal) + { + return new ReplaceLString(this, stringComparison, (find, replace)); + } + + public LocalizedString TrimStart() + { + return new TrimLString(this, TrimLString.Mode.Start); + } + + public LocalizedString TrimEnd() + { + return new TrimLString(this, TrimLString.Mode.End); + } + + public LocalizedString ToLower() + { + return new LowerLString(this); + } + + public override bool Equals(object? obj) + { + if (obj is LocalizedString lStr) { return Equals(lStr, StringComparison.Ordinal); } + if (obj is string str) { return Equals(str, StringComparison.Ordinal); } + return base.Equals(obj); + } + + public bool Equals(LocalizedString other, StringComparison comparison = StringComparison.Ordinal) + { + return Equals(other.Value, comparison); + } + + public bool Equals(string other, StringComparison comparison = StringComparison.Ordinal) + { + return string.Equals(Value, other, comparison); + } + + public bool StartsWith(LocalizedString other, StringComparison comparison = StringComparison.Ordinal) + { + return StartsWith(other.Value, comparison); + } + + public bool StartsWith(string other, StringComparison comparison = StringComparison.Ordinal) + { + return Value.StartsWith(other, comparison); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public int CompareTo(object? obj) + { + return Value.CompareTo(obj?.ToString() ?? ""); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs new file mode 100644 index 000000000..7175919f9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LowerLString.cs @@ -0,0 +1,20 @@ +#nullable enable +namespace Barotrauma +{ + public class LowerLString : LocalizedString + { + private readonly LocalizedString nestedStr; + + public LowerLString(LocalizedString nestedStr) + { + this.nestedStr = nestedStr; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value.ToLowerInvariant(); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs new file mode 100644 index 000000000..75fd64394 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/RawLString.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Barotrauma +{ + public class RawLString : LocalizedString + { + public RawLString(string value) { cachedValue = value; } + + protected override bool MustRetrieveValue() => false; + + public override bool Loaded => true; + public override void RetrieveValue() { } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs new file mode 100644 index 000000000..ceced3a14 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ReplaceLString.cs @@ -0,0 +1,75 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + public class ReplaceLString : LocalizedString + { + private readonly LocalizedString nestedStr; + private readonly ImmutableDictionary replacements; + private readonly StringComparison stringComparison; + + public ReplaceLString(LocalizedString nStr, StringComparison sc, IEnumerable<(LocalizedString Key, LocalizedString Value, FormatCapitals FormatCapitals)> r) + { + nestedStr = nStr; + replacements = r.Select(kvf => (kvf.Key, (kvf.Value, kvf.FormatCapitals))).ToImmutableDictionary(); + stringComparison = sc; + } + + public ReplaceLString(LocalizedString nStr, StringComparison sc, params (LocalizedString Key, LocalizedString Value)[] r) + : this(nStr, sc, r.Select(kv => (kv.Key, kv.Value, FormatCapitals.No))) { } + + public ReplaceLString(LocalizedString nStr, StringComparison sc, IEnumerable<(Identifier Key, LocalizedString Value, FormatCapitals FormatCapitals)> r) + : this(nStr, sc, r.Select(p => ((LocalizedString)p.Key.Value, p.Value, p.FormatCapitals))) { } + + public ReplaceLString(LocalizedString nStr, StringComparison sc, params (Identifier Key, LocalizedString Value)[] r) + : this(nStr, sc, r.Select(kv => ((LocalizedString)kv.Key.Value, kv.Value, FormatCapitals.No))) { } + + private static string HandleVariableCapitalization(string text, string variableTag, string variableValue) + { + int index = text.IndexOf(variableTag, StringComparison.InvariantCulture) - 1; + if (index == -1) + { + return variableValue; + } + + for (int i = index; i >= 0; i--) + { + if (char.IsWhiteSpace(text[i])) { continue; } + + if (text[i] != '.') + { + variableValue = variableValue.ToLowerInvariant(); + } + else + { + variableValue = TextManager.Capitalize(variableValue).Value; + break; + } + } + + return variableValue; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value; + foreach (var varName in replacements.Keys) + { + string key = varName.Value; + string value = replacements[varName].Value.Value; + if (replacements[varName].FormatCapitals == FormatCapitals.Yes) + { + value = HandleVariableCapitalization(cachedValue, key, value); + } + cachedValue = cachedValue.Replace(key, value, stringComparison); + } + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs new file mode 100644 index 000000000..c95b4f3bb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/ServerMsgLString.cs @@ -0,0 +1,185 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Barotrauma +{ + public class ServerMsgLString : LocalizedString + { + private static readonly Regex reFormattedMessage = + new Regex(@"^(?[\[\].a-z0-9_]+?)=(?[a-z0-9_]+?)\((?.+?)\)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex reReplacedMessage = new Regex(@"^(?[\[\].a-z0-9_]+?)=(?.*)$", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly ImmutableDictionary> messageFormatters = + new Dictionary> + { + { + "duration".ToIdentifier(), + secondsValue => double.TryParse(secondsValue, out var seconds) + ? $"{TimeSpan.FromSeconds(seconds):g}" + : null + } + }.ToImmutableDictionary(); + + private static readonly ImmutableHashSet serverMessageCharacters = + new [] {'~', '[', ']', '='}.ToImmutableHashSet(); + + private readonly string serverMessage; + private readonly ImmutableArray messageSplit; + + public ServerMsgLString(string serverMsg) + { + serverMessage = serverMsg; + messageSplit = serverMessage.Split("/").ToImmutableArray(); + } + + private static bool IsServerMessageWithVariables(string message) => + serverMessageCharacters.All(message.Contains); + + private LoadedSuccessfully loadedSuccessfully = LoadedSuccessfully.Unknown; + public override bool Loaded + { + get + { + if (loadedSuccessfully == LoadedSuccessfully.Unknown) { RetrieveValue(); } + return loadedSuccessfully == LoadedSuccessfully.Yes; + } + } + + public override void RetrieveValue() + { + Dictionary replacedMessages = new Dictionary(); + bool translationsFound = false; + + string? TranslateMessage(string input) + { + string? message = input; + if (message.EndsWith("~", StringComparison.Ordinal)) + { + message = message.Substring(0, message.Length - 1); + } + + if (!IsServerMessageWithVariables(message) && !message.Contains('=')) // No variables, try to translate + { + foreach (var replacedMessage in replacedMessages) + { + message = message.Replace(replacedMessage.Key, replacedMessage.Value); + } + + if (message.Contains(" ")) + { + return message; + } // Spaces found, do not translate + + var msg = TextManager.Get(message); + if (msg.Loaded) // If a translation was found, otherwise use the original + { + message = msg.Value; + translationsFound = true; + } + } + else + { + string? messageVariable = null; + var matchFormatted = reFormattedMessage.Match(message); + if (matchFormatted.Success) + { + var formatter = matchFormatted.Groups["formatter"].ToString().ToIdentifier(); + if (messageFormatters.TryGetValue(formatter, out var formatterFn)) + { + var formattedValue = formatterFn(matchFormatted.Groups["value"].ToString()); + if (formattedValue != null) + { + messageVariable = matchFormatted.Groups["variable"].ToString(); + message = formattedValue; + } + } + } + + if (messageVariable == null) + { + var matchReplaced = reReplacedMessage.Match(message); + if (matchReplaced.Success) + { + messageVariable = matchReplaced.Groups["variable"].ToString(); + message = matchReplaced.Groups["message"].ToString(); + } + } + + foreach (var replacedMessage in replacedMessages) + { + message = message.Replace(replacedMessage.Key, replacedMessage.Value); + } + + string[] messageWithVariables = message.Split('~'); + + var msg = TextManager.Get(messageWithVariables[0]); + + if (msg.Loaded) // If a translation was found, otherwise use the original + { + message = msg.Value; + translationsFound = true; + } + else if (messageVariable == null) + { + return message; // No translation found, probably caused by player input -> skip variable handling + } + + // First index is always the message identifier -> start at 1 + for (int j = 1; j < messageWithVariables.Length; j++) + { + string[] variableAndValue = messageWithVariables[j].Split('='); + message = message.Replace(variableAndValue[0], + variableAndValue[1].Length > 1 && variableAndValue[1][0] == '§' + ? TextManager.Get(variableAndValue[1].Substring(1)).Value + : variableAndValue[1]); + } + + if (messageVariable != null) + { + replacedMessages[messageVariable] = message; + message = null; + } + } + + return message; + } + + try + { + string translatedServerMessage = ""; + + for (int i = 0; i < messageSplit.Length; i++) + { + string? message = TranslateMessage(messageSplit[i]); + if (message != null) + { + translatedServerMessage += message; + } + } + + cachedValue = translationsFound ? translatedServerMessage : serverMessage; + loadedSuccessfully = LoadedSuccessfully.Yes; + } + catch (IndexOutOfRangeException exception) + { + string errorMsg = $"Failed to translate server message \"{serverMessage}\"."; +#if DEBUG + DebugConsole.ThrowError(errorMsg, exception); +#endif + GameAnalyticsManager.AddErrorEventOnce($"TextManager.GetServerMessage:{serverMessage}", + GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + cachedValue = errorMsg; + loadedSuccessfully = LoadedSuccessfully.No; + } + + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs new file mode 100644 index 000000000..d30ae2a00 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/SplitLString.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +#nullable enable +namespace Barotrauma +{ + public class LStringSplitter + { + public IReadOnlyList Substrings => substrings; + + private class SubstringList : IReadOnlyList + { + public SubstringList(LStringSplitter splitter) { this.splitter = splitter; } + + private LStringSplitter splitter; + private readonly List underlyingList = new List(); + + public List UnderlyingList + { + get + { + splitter.UpdateSubstrings(); + return underlyingList; + } + } + + public IEnumerator GetEnumerator() => UnderlyingList.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => UnderlyingList.Count; + + public LocalizedString this[int index] => UnderlyingList[index]; + } + + private readonly SubstringList substrings; + private readonly char[] separators; + private readonly LocalizedString originalString; + private string[] substrValues; + + private string cachedOriginal; + + public bool Loaded => originalString.Loaded; + + public LStringSplitter(LocalizedString input, params char[] separators) + { + originalString = input; + substrings = new SubstringList(this); + substrValues = Array.Empty(); + this.separators = separators; + cachedOriginal = ""; + } + + private void UpdateSubstrings() + { + if (originalString.Value != cachedOriginal) + { + cachedOriginal = originalString.Value; + substrValues = cachedOriginal.Split(separators); + substrings.UnderlyingList.Clear(); + substrings.UnderlyingList.AddRange(Enumerable.Range(0, substrValues.Length).Select(i => new SplitLString(this, i) as LocalizedString)); + } + } + + public string GetValue(int index) + { + UpdateSubstrings(); + return substrValues[index]; + } + } + + public class SplitLString : LocalizedString + { + private bool loaded = false; + private readonly LStringSplitter splitter; + private readonly int index; + + public SplitLString(LStringSplitter splitter, int index) + { + this.splitter = splitter; this.index = index; + } + + public override bool Loaded => loaded && splitter.Loaded; + public override void RetrieveValue() + { + loaded = true; + cachedValue = splitter.GetValue(index); + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs new file mode 100644 index 000000000..0056fc9da --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -0,0 +1,71 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + public class TagLString : LocalizedString + { + private readonly ImmutableArray tags; + + public TagLString(params Identifier[] tags) + { + this.tags = tags.ToImmutableArray(); + } + + private LoadedSuccessfully loadedSuccessfully = LoadedSuccessfully.Unknown; + + public override bool Loaded + { + get + { + if (loadedSuccessfully == LoadedSuccessfully.Unknown) { RetrieveValue(); } + return loadedSuccessfully == LoadedSuccessfully.Yes; + } + } + + public override void RetrieveValue() + { + UpdateLanguage(); + + (string value, bool loaded) tryLoad(LanguageIdentifier lang) + { + IReadOnlyList candidates = Array.Empty(); + int tagIndex = 0; + + if (TextManager.TextPacks.TryGetValue(lang, out var packs)) + { + while (candidates.Count == 0 && tagIndex < tags.Length) + { + foreach (var pack in packs) + { + if (pack.Texts.TryGetValue(tags[tagIndex], out var texts)) + { + candidates = candidates.ListConcat(texts); + } + } + tagIndex++; + } + } + + bool loaded = candidates.Count > 0; + return (loaded ? candidates.GetRandomUnsynced() : "", loaded); + } + + var (value, loaded) = tryLoad(language); + loadedSuccessfully = loaded ? LoadedSuccessfully.Yes : LoadedSuccessfully.No; + cachedValue = value; + if (!loaded && language != TextManager.DefaultLanguage) + { + (value, _) = tryLoad(TextManager.DefaultLanguage); + cachedValue = value; + //Notice how we don't set loadedSuccessfully again here. + //This is by design; falling back to English means that + //this text did NOT load successfully, so Loaded must + //return false. + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs new file mode 100644 index 000000000..92c987b9e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TrimLString.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; + +namespace Barotrauma +{ + public class TrimLString : LocalizedString + { + [Flags] + public enum Mode { Start = 0x1, End = 0x2, Both=0x3 } + private readonly LocalizedString nestedStr; + private readonly Mode mode; + + public TrimLString(LocalizedString nestedStr, Mode mode) + { + this.nestedStr = nestedStr; + this.mode = mode; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value; + if (mode.HasFlag(Mode.Start)) { cachedValue = cachedValue.TrimStart(); } + if (mode.HasFlag(Mode.End)) { cachedValue = cachedValue.TrimEnd(); } + UpdateLanguage(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs new file mode 100644 index 000000000..8c75fb7f0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/UpperLString.cs @@ -0,0 +1,25 @@ +#nullable enable +namespace Barotrauma +{ + public class UpperLString : LocalizedString + { + private readonly LocalizedString nestedStr; + + public UpperLString(LocalizedString nestedStr) + { + this.nestedStr = nestedStr; + } + + public override bool Loaded => nestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = nestedStr.Value.ToUpper(); + UpdateLanguage(); + } + + public override LocalizedString ToUpper() + { + return this; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs new file mode 100644 index 000000000..30e3678e7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -0,0 +1,199 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Barotrauma +{ + public class RichString + { + protected bool loaded = false; + protected LanguageIdentifier language = LanguageIdentifier.None; + + protected string cachedSanitizedValue = ""; + public string SanitizedValue + { + get + { + if (MustRetrieveValue()) { RetrieveValue(); } + return cachedSanitizedValue; + } + } + + public int Length => SanitizedValue.Length; + + private readonly Func? postProcess; + private readonly bool shouldParseRichTextData; + + private readonly LocalizedString originalStr; + public LocalizedString NestedStr { get; private set; } + public readonly LocalizedString SanitizedString; + +#if CLIENT + private readonly GUIFont? font; + private readonly GUIComponentStyle? componentStyle; + private bool forceUpperCase = false; + + private bool fontOrStyleForceUpperCase + => font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true }; +#endif + + public ImmutableArray? RichTextData { get; private set; } + +#if CLIENT + private RichString( + LocalizedString nestedStr, bool shouldParseRichTextData, Func? postProcess = null, + GUIFont? font = null, GUIComponentStyle? componentStyle = null) : this(nestedStr, shouldParseRichTextData, postProcess) + { + this.font = font; + this.componentStyle = componentStyle; + } +#endif + + private RichString(LocalizedString nestedStr, bool shouldParseRichTextData, Func? postProcess = null) + { + originalStr = nestedStr; + NestedStr = originalStr; + this.shouldParseRichTextData = shouldParseRichTextData; + this.postProcess = postProcess; + SanitizedString = new StripRichTagsLString(this); +#if CLIENT + this.font = null; + this.componentStyle = null; +#endif + } + + public static RichString Rich(LocalizedString str, Func? postProcess = null) + { + return new RichString(str, true, postProcess); + } + + public static RichString Plain(LocalizedString str) + { + return new RichString(str, false, postProcess: null); + } + + public static implicit operator LocalizedString(RichString richStr) => richStr.NestedStr; + + public static implicit operator RichString(LocalizedString lStr) + { +#if DEBUG + if (!lStr.IsNullOrEmpty() && lStr.Contains("‖")) + { + if (Debugger.IsAttached) { Debugger.Break(); } + } +#endif + return Plain(lStr); + } + public static implicit operator RichString(string str) => (LocalizedString)str; + + protected virtual bool MustRetrieveValue() + { + return NestedStr.Loaded != loaded + || language != GameSettings.CurrentConfig.Language +#if CLIENT + || (fontOrStyleForceUpperCase != forceUpperCase) +#endif + ; + } + + public void RetrieveValue() + { +#if CLIENT + NestedStr = fontOrStyleForceUpperCase ? originalStr.ToUpper() : originalStr; +#endif + + if (shouldParseRichTextData) + { + RichTextData = Barotrauma.RichTextData.GetRichTextData(NestedStr.Value, out cachedSanitizedValue); + } + else + { + cachedSanitizedValue = NestedStr.Value; + } + if (postProcess != null) { cachedSanitizedValue = postProcess(cachedSanitizedValue); } + language = GameSettings.CurrentConfig.Language; + loaded = NestedStr.Loaded; + } + +#if CLIENT + public RichString CaseTiedToFontAndStyle(GUIFont? font, GUIComponentStyle? componentStyle) + { + return new RichString(originalStr, shouldParseRichTextData, postProcess, font, componentStyle); + } +#endif + + public RichString ToUpper() + { + return new RichString(NestedStr.ToUpper(), shouldParseRichTextData, postProcess); + } + + public RichString ToLower() + { + return new RichString(NestedStr.ToLower(), shouldParseRichTextData, postProcess); + } + + public RichString Replace(string from, string to, StringComparison stringComparison = StringComparison.Ordinal) + { + return new RichString(NestedStr.Replace(from, to, stringComparison), shouldParseRichTextData, postProcess); + } + + public override string ToString() + { + return SanitizedValue; + } + + public bool Contains(string str, StringComparison stringComparison = StringComparison.Ordinal) => + SanitizedValue.Contains(str, stringComparison); + + public bool Contains(char chr, StringComparison stringComparison = StringComparison.Ordinal) => + SanitizedValue.Contains(chr, stringComparison); + + + public static bool operator ==(RichString? a, RichString? b) + => a?.SanitizedValue == b?.SanitizedValue +#if CLIENT + && a?.font == b?.font + && a?.componentStyle == b?.componentStyle +#endif + ; + + public static bool operator !=(RichString? a, RichString? b) => !(a == b); + + public static bool operator ==(RichString? a, LocalizedString? b) + => a?.SanitizedValue == b?.Value; + + public static bool operator !=(RichString? a, LocalizedString? b) => !(a == b); + + public static bool operator ==(LocalizedString? a, RichString? b) + => a?.Value == b?.SanitizedValue; + + public static bool operator !=(LocalizedString? a, RichString? b) => !(a == b); + + public static bool operator ==(RichString? a, string? b) + => a?.SanitizedValue == b; + + public static bool operator !=(RichString? a, string? b) => !(a == b); + + public static bool operator ==(string? a, RichString? b) + => a == b?.SanitizedValue; + + public static bool operator !=(string? a, RichString? b) => !(a == b); + } + + public class StripRichTagsLString : LocalizedString + { + public readonly RichString RichStr; + + public StripRichTagsLString(RichString richStr) + { + RichStr = richStr; + } + + public override bool Loaded => RichStr.NestedStr.Loaded; + public override void RetrieveValue() + { + cachedValue = RichStr.SanitizedValue; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs new file mode 100644 index 000000000..7f6743010 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -0,0 +1,404 @@ +#nullable enable + +using Barotrauma.IO; +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace Barotrauma +{ + public enum FormatCapitals + { + Yes, No + } + + public static class TextManager + { + public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); + public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); + public static IEnumerable AvailableLanguages => TextPacks.Keys; + + private readonly static Dictionary> cachedStrings = + new Dictionary>(); + private static ImmutableHashSet nonCacheableTags = + ImmutableHashSet.Empty; + + public static int LanguageVersion { get; private set; } = 0; + + private readonly static Regex isCJK = new Regex( + @"\p{IsHangulJamo}|" + + @"\p{IsHiragana}|" + + @"\p{IsKatakana}|" + + @"\p{IsCJKRadicalsSupplement}|" + + @"\p{IsCJKSymbolsandPunctuation}|" + + @"\p{IsEnclosedCJKLettersandMonths}|" + + @"\p{IsCJKCompatibility}|" + + @"\p{IsCJKUnifiedIdeographsExtensionA}|" + + @"\p{IsCJKUnifiedIdeographs}|" + + @"\p{IsHangulSyllables}|" + + @"\p{IsCJKCompatibilityForms}"); + + /// + /// Does the string contain symbols from Chinese, Japanese or Korean languages + /// + public static bool IsCJK(LocalizedString text) + { + return IsCJK(text.Value); + } + + public static bool IsCJK(string text) + { + if (string.IsNullOrEmpty(text)) { return false; } + return isCJK.IsMatch(text); + } + + public static bool ContainsTag(string tag) + { + return ContainsTag(tag.ToIdentifier()); + } + + public static bool ContainsTag(Identifier tag) + { + return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag)); + } + + public static IEnumerable GetAll(string tag) + => GetAll(tag.ToIdentifier()); + + public static IEnumerable GetAll(Identifier tag) + { + return TextPacks[GameSettings.CurrentConfig.Language] + .SelectMany(p => p.Texts.TryGetValue(tag, out var value) + ? (IEnumerable)value + : Array.Empty()); + } + + public static IEnumerable> GetAllTagTextPairs() + { + return TextPacks[GameSettings.CurrentConfig.Language] + .SelectMany(p => p.Texts) + .SelectMany(kvp => kvp.Value.Select(v => new KeyValuePair(kvp.Key, v))); + } + + public static IEnumerable GetTextFiles() + { + return GetTextFilesRecursive(Path.Combine("Content", "Texts")); + } + + private static IEnumerable GetTextFilesRecursive(string directory) + { + foreach (string file in Directory.GetFiles(directory)) + { + yield return file.CleanUpPath(); + } + foreach (string subDir in Directory.GetDirectories(directory)) + { + foreach (string file in GetTextFilesRecursive(subDir)) + { + yield return file; + } + } + } + + public static string GetTranslatedLanguageName(LanguageIdentifier languageIdentifier) + { + return TextPacks[languageIdentifier].First().TranslatedName; + } + + public static void ClearCache() + { + lock (cachedStrings) + { + cachedStrings.Clear(); + nonCacheableTags.Clear(); + } + } + + public static LocalizedString Get(params Identifier[] tags) + { + TagLString? str = null; + lock (cachedStrings) + { + if (tags.Length == 1 && !nonCacheableTags.Contains(tags[0])) + { + var tag = tags[0]; + if (cachedStrings.TryGetValue(tag, out var strRef)) + { + if (!strRef.TryGetTarget(out str)) + { + cachedStrings.Remove(tag); + } + } + + if (str is null && TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + { + int count = 0; + foreach (var pack in TextPacks[GameSettings.CurrentConfig.Language]) + { + if (pack.Texts.TryGetValue(tag, out var texts)) + { + count += texts.Length; + if (count > 1) { break; } + } + } + + if (count > 1) + { + nonCacheableTags = nonCacheableTags.Add(tag); + } + else + { + str = new TagLString(tags); + cachedStrings.Add(tag, new WeakReference(str)); + } + } + } + } + return str ?? new TagLString(tags); + } + + public static LocalizedString Get(params string[] tags) + => Get(tags.ToIdentifiers()); + + public static LocalizedString AddPunctuation(char punctuationSymbol, params LocalizedString[] texts) + { + return new AddedPunctuationLString(punctuationSymbol, texts); + } + + public static LocalizedString GetFormatted(Identifier tag, params object[] args) + { + return GetFormatted(new TagLString(tag), args); + } + + public static LocalizedString GetFormatted(LocalizedString str, params object[] args) + { + LocalizedString[] argStrs = new LocalizedString[args.Length]; + for (int i = 0; i < args.Length; i++) + { + if (args[i] is LocalizedString ls) { argStrs[i] = ls; } + else { argStrs[i] = new RawLString(args[i].ToString() ?? ""); } + } + return new FormattedLString(str, argStrs); + } + + public static string FormatServerMessage(string str) => $"{str}~"; + + public static string FormatServerMessage(string message, params (string Key, string Value)[] keysWithValues) + { + if (keysWithValues.Length == 0) + { + return FormatServerMessage(message); + } + var startIndex = message.LastIndexOf('/') + 1; + var endIndex = message.IndexOf('~', startIndex); + if (endIndex == -1) + { + endIndex = message.Length - 1; + } + var textId = message.Substring(startIndex, endIndex - startIndex + 1); + var prefixEntries = keysWithValues.Select((kv, index) => + { + if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1) + { + var kvStartIndex = kv.Value.LastIndexOf('/') + 1; + return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}"; + } + else + { + return null; + } + }).Where(e => e != null).ToArray(); + return string.Join("", + (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""), + message, + string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}")) + ); + } + + internal static string FormatServerMessageWithPronouns(CharacterInfo charInfo, string message, params (string Key, string Value)[] keysWithValues) + { + var pronounCategory = charInfo.Prefab.Pronouns; + (string Key, string Value)[] pronounKwv = new[] + { + ("[PronounLowercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]Lowercase")), + ("[PronounUppercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]")), + ("[PronounPossessiveLowercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]Lowercase")), + ("[PronounPossessiveUppercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]")), + ("[PronounReflexiveLowercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]Lowercase")), + ("[PronounReflexiveUppercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]")) + }; + return FormatServerMessage(message, keysWithValues.Concat(pronounKwv).ToArray()); + } + + // Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements. + public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.") + { + + return string.Join("/", + string.Join("/", parts.Select((part, index) => + { + var partStart = part.LastIndexOf('/') + 1; + return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}"; + })), + string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]"))); + } + + public static LocalizedString ParseInputTypes(LocalizedString str) + { + return new InputTypeLString(str); + } + + public static LocalizedString GetWithVariable(string tag, string varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No) + { + return GetWithVariable(tag.ToIdentifier(), varName.ToIdentifier(), value, formatCapitals); + } + + public static LocalizedString GetWithVariable(Identifier tag, Identifier varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No) + { + return GetWithVariables(tag, (varName, value)); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, string Value)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + (LocalizedString)new RawLString(kv.Value), + FormatCapitals.No))); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + kv.Value, + FormatCapitals.No))); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value, FormatCapitals FormatCapitals)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + kv.Value, + kv.FormatCapitals))); + } + + public static LocalizedString GetWithVariables(string tag, params (string Key, string Value, FormatCapitals FormatCapitals)[] replacements) + { + return GetWithVariables( + tag.ToIdentifier(), + replacements.Select(kv => + (kv.Key.ToIdentifier(), + (LocalizedString)new RawLString(kv.Value), + kv.FormatCapitals))); + } + + public static LocalizedString GetWithVariables(Identifier tag, params (Identifier Key, LocalizedString Value)[] replacements) + { + return GetWithVariables(tag, replacements.Select(kv => (kv.Key, kv.Value, FormatCapitals.No))); + } + + public static LocalizedString GetWithVariables(Identifier tag, IEnumerable<(Identifier, LocalizedString, FormatCapitals)> replacements) + { + return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements); + } + + public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement) + { + /* + + + + + + */ + + LocalizedString extraDescriptionLine = Get(descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty)); + foreach (XElement replaceElement in descriptionElement.Elements()) + { + if (replaceElement.NameAsIdentifier() != "replace") { continue; } + + Identifier tag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty); + string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty()); + LocalizedString replacementValue = string.Empty; + for (int i = 0; i < replacementValues.Length; i++) + { + replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]); + if (i < replacementValues.Length - 1) + { + replacementValue += ", "; + } + } + if (replaceElement.Attribute("color") != null) + { + string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); + replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖"; + } + extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); + } + if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed + description += extraDescriptionLine; + } + + public static LocalizedString GetServerMessage(string serverMessage) + { + return new ServerMsgLString(serverMessage); + } + + public static LocalizedString Capitalize(this LocalizedString str) + { + return new CapitalizeLString(str); + } + + public static void IncrementLanguageVersion() + { + LanguageVersion++; + ClearCache(); + } + +#if DEBUG + public static void CheckForDuplicates(LanguageIdentifier lang) + { + if (!TextPacks.ContainsKey(lang)) + { + DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); + return; + } + + int packIndex = 0; + foreach (TextPack textPack in TextPacks[lang]) + { + textPack.CheckForDuplicates(packIndex); + packIndex++; + } + } + + public static void WriteToCSV() + { + LanguageIdentifier lang = DefaultLanguage; + + if (!TextPacks.ContainsKey(lang)) + { + DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); + return; + } + + int packIndex = 0; + foreach (TextPack textPack in TextPacks[lang]) + { + textPack.WriteToCSV(packIndex); + packIndex++; + } + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs new file mode 100644 index 000000000..b4829e7ba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -0,0 +1,174 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + public readonly struct LanguageIdentifier + { + public static readonly LanguageIdentifier None = "None".ToLanguageIdentifier(); + + public readonly Identifier Value; + public int ValueHash => Value.GetHashCode(); + public LanguageIdentifier(Identifier value) { Value = value; } + + public override bool Equals(object obj) + { + if (obj is LanguageIdentifier other) { return this == other; } + return base.Equals(obj); + } + + public override int GetHashCode() => ValueHash; + + public static bool operator ==(LanguageIdentifier a, LanguageIdentifier b) => a.Value == b.Value; + public static bool operator !=(LanguageIdentifier a, LanguageIdentifier b) => !(a==b); + + public override string ToString() => Value.ToString(); + } + + public static class LanguageIdentifierExtensions + { + public static LanguageIdentifier ToLanguageIdentifier(this Identifier identifier) + { + return new LanguageIdentifier(identifier); + } + + public static LanguageIdentifier ToLanguageIdentifier(this string str) + { + return str.ToIdentifier().ToLanguageIdentifier(); + } + } + + public class TextPack + { + public readonly TextFile ContentFile; + + public readonly LanguageIdentifier Language; + + public readonly ImmutableDictionary> Texts; + public readonly string TranslatedName; + public readonly bool NoWhitespace; + + public TextPack(TextFile file, ContentXElement mainElement, LanguageIdentifier language) + { + ContentFile = file; + + var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value); + Language = language; + TranslatedName = mainElement.GetAttributeString("translatedname", languageName.Value); + NoWhitespace = mainElement.GetAttributeBool("nowhitespace", false); + + Dictionary> texts = new Dictionary>(); + foreach (var element in mainElement.Elements()) + { + Identifier elemName = element.NameAsIdentifier(); + if (!texts.ContainsKey(elemName)) { texts.Add(elemName, new List()); } + texts[elemName].Add(element.ElementInnerText() + .Replace(@"\n", "\n") + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(""", "\"") + .Replace("'", "'")); + } + Texts = texts.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); + } + +#if DEBUG + public void CheckForDuplicates(int index) + { + Dictionary tagCounts = new Dictionary(); + Dictionary contentCounts = new Dictionary(); + + XDocument doc = XMLExtensions.TryLoadXml(ContentFile.Path); + if (doc == null) { return; } + + foreach (var subElement in doc.Root.Elements()) + { + Identifier infoName = subElement.NameAsIdentifier(); + if (!tagCounts.ContainsKey(infoName)) + { + tagCounts.Add(infoName, 1); + } + else + { + tagCounts[infoName] += 1; + } + + string infoContent = subElement.Value; + if (string.IsNullOrEmpty(infoContent)) continue; + if (!contentCounts.ContainsKey(infoContent)) + { + contentCounts.Add(infoContent, 1); + } + else + { + contentCounts[infoContent] += 1; + } + } + + StringBuilder sb = new StringBuilder(); + sb.Append("Language: " + Language); + sb.AppendLine(); + sb.Append("Duplicate tags:"); + sb.AppendLine(); + sb.AppendLine(); + + for (int i = 0; i < tagCounts.Keys.Count; i++) + { + if (tagCounts[Texts.Keys.ElementAt(i)] > 1) + { + sb.Append(Texts.Keys.ElementAt(i) + " | Count: " + tagCounts[Texts.Keys.ElementAt(i)]); + sb.AppendLine(); + } + } + + sb.AppendLine(); + sb.AppendLine(); + sb.Append("Duplicate content:"); + sb.AppendLine(); + sb.AppendLine(); + + for (int i = 0; i < contentCounts.Keys.Count; i++) + { + if (contentCounts[contentCounts.Keys.ElementAt(i)] > 1) + { + sb.Append(contentCounts.Keys.ElementAt(i) + " | Count: " + contentCounts[contentCounts.Keys.ElementAt(i)]); + sb.AppendLine(); + } + } + + Barotrauma.IO.File.WriteAllText($"duplicate_{Language.ToString().ToLower()}_{index}.txt", sb.ToString()); + } + + public void WriteToCSV(int index) + { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < Texts.Count; i++) + { + Identifier key = Texts.Keys.ElementAt(i); + Texts.TryGetValue(key, out ImmutableArray infoList); + + for (int j = 0; j < infoList.Length; j++) + { + sb.Append(key); // ID + sb.Append('*'); + sb.Append(infoList[j]); // Original + sb.Append('*'); + // Translated + sb.Append('*'); + // Comments + sb.AppendLine(); + } + } + + Barotrauma.IO.File.WriteAllText($"csv_{Language.ToString().ToLower()}_{index}.csv", sb.ToString()); + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs deleted file mode 100644 index 6def082f0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ /dev/null @@ -1,951 +0,0 @@ -using System; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Barotrauma.Extensions; -using System.Xml.Linq; - -namespace Barotrauma -{ - public static class TextManager - { - //only used if none of the selected content packages contain any text files - const string VanillaTextFilePath = "Content/Texts/English/EnglishVanilla.xml"; - - private static readonly object mutex = new object(); - - //key = language - private static Dictionary> textPacks; - - private static readonly string[] serverMessageCharacters = new string[] { "~", "[", "]", "=" }; - - public static string Language; - - public static bool Initialized - { - get; - private set; - } - - private static HashSet availableLanguages; - public static IEnumerable AvailableLanguages - { - get - { - lock (mutex) - { - return new HashSet(availableLanguages); - } - } - } - - public static List GetTextFiles() - { - var list = new List(); - GetTextFilesRecursive(Path.Combine("Content", "Texts"), ref list); - return list; - } - - private static void GetTextFilesRecursive(string directory, ref List list) - { - foreach (string file in Directory.GetFiles(directory)) - { - list.Add(file); - } - foreach (string subDir in Directory.GetDirectories(directory)) - { - GetTextFilesRecursive(subDir, ref list); - } - } - - /// - /// Returns the name of the language in the respective language - /// - public static string GetTranslatedLanguageName(string language) - { - lock (mutex) - { - if (!textPacks.ContainsKey(language)) - { - return language; - } - - foreach (var textPack in textPacks[language]) - { - if (textPack.Language == language) - { - return textPack.TranslatedName; - } - } - return language; - } - } - - public static void LoadTextPacks(IEnumerable selectedContentPackages) - { - HashSet newLanguages = new HashSet(); - Dictionary> newTextPacks = new Dictionary>(); - - var textFiles = ContentPackage.GetFilesOfType(selectedContentPackages, ContentType.Text).ToList(); - - foreach (ContentFile file in textFiles) - { -#if !DEBUG - try - { -#endif - var textPack = new TextPack(file.Path); - newLanguages.Add(textPack.Language); - if (!newTextPacks.ContainsKey(textPack.Language)) - { - newTextPacks.Add(textPack.Language, new List()); - } - newTextPacks[textPack.Language].Add(textPack); -#if !DEBUG - } - catch (Exception e) - { - DebugConsole.ThrowError("Failed to load text file \"" + file.Path + "\"!", e); - } -#endif - } - - if (newTextPacks.Count == 0) - { - DebugConsole.ThrowError("No text files available in any of the selected content packages. Attempting to find a vanilla English text file..."); - if (!File.Exists(VanillaTextFilePath)) - { - throw new Exception("No text files found in any of the selected content packages or in the default text path!"); - } - var textPack = new TextPack(VanillaTextFilePath); - newLanguages.Add(textPack.Language); - newTextPacks.Add(textPack.Language, new List() { textPack }); - } - - if (newTextPacks.Count == 0) - { - throw new Exception("Failed to load text packs!"); - } - - lock (mutex) - { - textPacks = newTextPacks; - availableLanguages = newLanguages; - DebugConsole.NewMessage("Loaded languages: " + string.Join(", ", newLanguages)); - } - - Initialized = true; - } - - public static void LoadTextPack(string file) - { - lock (mutex) - { - var textPack = new TextPack(file); - availableLanguages.Add(textPack.Language); - if (!textPacks.ContainsKey(textPack.Language)) - { - textPacks.Add(textPack.Language, new List()); - } - textPacks[textPack.Language].Add(textPack); - } - } - - public static void RemoveTextPack(string file) - { - List keysToRemove = new List(); - foreach (var textPackKVP in textPacks) - { - var textPackLanguage = textPackKVP.Key; - var textPackList = textPackKVP.Value; - for (int i = 0; i < textPackList.Count; i++) - { - if (textPackList[i].FilePath == file) - { - textPackList.Remove(textPackList[i]); - if (textPackList.Count == 0) - { - keysToRemove.Add(textPackLanguage); - } - } - } - } - - foreach (var key in keysToRemove) - { - availableLanguages.Remove(key); - textPacks.Remove(key); - } - } - - public static bool ContainsTag(string textTag) - { - if (string.IsNullOrEmpty(textTag)) { return false; } - - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - foreach (TextPack textPack in textPacks[Language]) - { - if (textPack.Get(textTag) != null) { return true; } - } - } - - return false; - } - - private static readonly List availableTexts = new List(); - - public static string Get(string textTag, bool returnNull = false, string fallBackTag = null, bool useEnglishAsFallBack = true) - { - lock (mutex) - { - if (textPacks == null) - { - DebugConsole.ThrowError($"Failed to get the text \"{textTag}\" (no text packs loaded)."); - return textTag; - } - - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - -#if DEBUG - if (GameMain.Config != null && GameMain.Config.TextManagerDebugModeEnabled) - { - return textTag; - } -#endif - availableTexts.Clear(); - foreach (TextPack textPack in textPacks[Language]) - { - var texts = textPack.GetAll(textTag); - if (texts != null) - { - availableTexts.AddRange(texts); - } - } - - if (availableTexts.Any()) - { - return availableTexts.GetRandom().Replace(@"\n", "\n"); - } - - if (!string.IsNullOrEmpty(fallBackTag)) - { - foreach (TextPack textPack in textPacks[Language]) - { - string text = textPack.Get(fallBackTag); - if (text != null) { return text; } - } - } - - //if text was not found and we're using a language other than English, see if we can find an English version - //may happen, for example, if a user has selected another language and using mods that haven't been translated to that language - if (useEnglishAsFallBack && Language != "English" && textPacks.ContainsKey("English")) - { - foreach (TextPack textPack in textPacks["English"]) - { - string text = textPack.Get(textTag); - if (text != null) - { -#if DEBUG - DebugConsole.NewMessage("Text \"" + textTag + "\" not found for the language \"" + Language + "\". Using the English text \"" + text + "\" instead."); -#endif - return text; - } - } - } - - if (returnNull) - { - return null; - } - else - { - DebugConsole.ThrowError("Text \"" + textTag + "\" not found."); - return textTag; - } - } - } - - public static string GetWithVariables(string textTag, string[] variableTags, string[] variableValues, bool[] formatCapitals = null, bool returnNull = false, string fallBackTag = null) - { - string text = Get(textTag, returnNull, fallBackTag); - - if (text == null || text.Length == 0 || variableTags.Length != variableValues.Length) - { -#if DEBUG - if (variableTags.Length != variableValues.Length) - { - DebugConsole.ThrowError("variableTags.Length and variableValues.Length do not match for \"" + textTag + "\"."); - } - - if (formatCapitals != null && formatCapitals.Length != variableTags.Length) - { - DebugConsole.ThrowError("variableTags.Length and formatCapitals.Length do not match for \"" + textTag + "\"."); - } -#endif - if (returnNull) - { - return null; - } - else - { - return textTag; - } - } - - if (formatCapitals != null && (GameMain.Config == null || !GameMain.Config.Language.Contains("Chinese"))) - { - for (int i = 0; i < variableTags.Length; i++) - { - if (string.IsNullOrEmpty(variableValues[i])) { continue; } - if (formatCapitals[i]) - { - variableValues[i] = HandleVariableCapitalization(text, variableTags[i], variableValues[i]); - } - } - } - - for (int i = 0; i < variableTags.Length; i++) - { - if (variableValues[i] == null) - { -#if DEBUG - DebugConsole.ThrowError("Error in TextManager.GetWithVariables (variable " + i + " was null).\n" + Environment.StackTrace.CleanupStackTrace()); -#endif - continue; - } - text = text.Replace(variableTags[i], variableValues[i]); - } - - return text; - } - - public static string GetWithVariable(string textTag, string variableTag, string variableValue, bool formatCapitals = false, bool returnNull = false, string fallBackTag = null) - { - string text = Get(textTag, returnNull, fallBackTag); - - if (text == null || text.Length == 0) - { - if (returnNull) - { - return null; - } - else - { - return textTag; - } - } - - if (variableValue == null) - { - variableValue = "null"; -#if DEBUG - throw new ArgumentException($"Variable value \"{variableTag}\" was null."); -#endif - } - - if (formatCapitals && !GameMain.Config.Language.Contains("Chinese")) - { - variableValue = HandleVariableCapitalization(text, variableTag, variableValue); - } - - return text.Replace(variableTag, variableValue); - } - - private static string HandleVariableCapitalization(string text, string variableTag, string variableValue) - { - int index = text.IndexOf(variableTag) - 1; - if (index == -1) - { - return variableValue; - } - - for (int i = index; i >= 0; i--) - { - if (text[i] == ' ') - { - continue; - } - else - { - if (text[i] != '.') - { - variableValue = variableValue.ToLower(); - } - else - { - variableValue = Capitalize(variableValue); - break; - } - } - } - - return variableValue; - } - - //TODO: the server should not be doing this! - public static string ParseInputTypes(string text) - { -#if CLIENT - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - text = text.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBindText(inputType)); - text = text.Replace("[InputType." + inputType.ToString() + "]", GameMain.Config.KeyBindText(inputType)); - } -#endif - return text; - } - - public static string GetFormatted(string textTag, bool returnNull = false, params object[] args) - { - string text = Get(textTag, returnNull); - - if (text == null || text.Length == 0) - { - if (returnNull) - { - return null; - } - else - { - DebugConsole.ThrowError("Text \"" + textTag + "\" not found."); - return textTag; - } - } - - try - { - return string.Format(text, args); - } - catch (FormatException) - { - string errorMsg = "Failed to format text \"" + text + "\", args: " + string.Join(", ", args); - GameAnalyticsManager.AddErrorEventOnce("TextManager.GetFormatted:FormatException", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return text; - } - } - - /// - /// Constructs a string from XML in a way that allows replacing one or more variables with hard-coded or localized values. Usage example in the method's comments. - /// - public static void ConstructDescription(ref string Description, XElement descriptionElement) - { - /* - - - - - - */ - - if (descriptionElement.GetAttributeBool("linebreak", false)) - { - Description += "\n"; - return; - } - - string descriptionTag = descriptionElement.GetAttributeString("tag", string.Empty); - string extraDescriptionLine = Get(descriptionTag); - if (string.IsNullOrEmpty(extraDescriptionLine)) { return; } - foreach (XElement replaceElement in descriptionElement.Elements()) - { - if (replaceElement.Name.ToString().ToLowerInvariant() != "replace") { continue; } - - string tag = replaceElement.GetAttributeString("tag", string.Empty); - string[] replacementValues = replaceElement.GetAttributeStringArray("value", new string[0]); - string replacementValue = string.Empty; - for (int i = 0; i < replacementValues.Length; i++) - { -#if DEBUG - if (!int.TryParse(replacementValues[i], out int _) && !float.TryParse(replacementValues[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out float __) && !ContainsTag(replacementValues[i])) - { - DebugConsole.AddWarning($"Couldn't find the tag \"{replacementValues[i]}\" in text files for description \"{descriptionTag}\". Is the tag correct?"); - } -#endif - replacementValue += Get(replacementValues[i], returnNull: true) ?? replacementValues[i]; - if (i < replacementValues.Length - 1) - { - replacementValue += ", "; - } - } - if (replaceElement.Attribute("color") != null) - { - string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); - replacementValue = $"‖color:{colorStr}‖{replacementValue}‖color:end‖"; - } - extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); - } - if (!string.IsNullOrEmpty(Description)) { Description += "\n"; } - Description += extraDescriptionLine; - } - - public static string FormatServerMessage(string textId) - { - return $"{textId}~"; - } - - public static string FormatServerMessage(string message, IEnumerable keys, IEnumerable values) - { - if (keys == null || values == null || !keys.Any() || !values.Any()) - { - return FormatServerMessage(message); - } - var startIndex = message.LastIndexOf('/') + 1; - var endIndex = message.IndexOf('~', startIndex); - if (endIndex == -1) - { - endIndex = message.Length - 1; - } - var textId = message.Substring(startIndex, endIndex - startIndex + 1); - var keysWithValues = keys.Zip(values, (key, value) => new { Key = key, Value = value }); - var prefixEntries = keysWithValues.Select((kv, index) => - { - if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1) - { - var kvStartIndex = kv.Value.LastIndexOf('/') + 1; - return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}"; - } - else - { - return null; - } - }).Where(e => e != null).ToArray(); - return string.Join("", - (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""), - message, - string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}").ToArray()) - ); - } - - static readonly string[] genderPronounVariables = { - "[genderpronoun]", - "[genderpronounpossessive]", - "[genderpronounreflexive]", - "[Genderpronoun]", - "[Genderpronounpossessive]", - "[Genderpronounreflexive]" - }; - - static readonly string[] genderPronounMaleValues = { - "PronounMaleLowercase", - "PronounPossessiveMaleLowercase", - "PronounReflexiveMaleLowercase", - "PronounMale", - "PronounPossessiveMale", - "PronounReflexiveMale" - }; - - static readonly string[] genderPronounFemaleValues = { - "PronounFemaleLowercase", - "PronounPossessiveFemaleLowercase", - "PronounReflexiveFemaleLowercase", - "PronounMale", - "PronounPossessiveFemale", - "PronounReflexiveFemale" - }; - - public static string FormatServerMessageWithGenderPronouns(Gender gender, string message, IEnumerable keys, IEnumerable values) - { - return FormatServerMessage(message, keys.Concat(genderPronounVariables), values.Concat(gender == Gender.Male ? genderPronounMaleValues : genderPronounFemaleValues)); - } - - // Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements. - public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.") - { - - return string.Join("/", - string.Join("/", parts.Select((part, index) => - { - var partStart = part.LastIndexOf('/') + 1; - return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}"; - })), - string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]"))); - } - - static readonly Regex reFormattedMessage = new Regex(@"^(?[\[\].a-z0-9_]+?)=(?[a-z0-9_]+?)\((?.+?)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - static readonly Regex reReplacedMessage = new Regex(@"^(?[\[\].a-z0-9_]+?)=(?.*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - static readonly Dictionary> messageFormatters = new Dictionary> - { - { "duration", secondsValue => double.TryParse(secondsValue, out var seconds) ? $"{TimeSpan.FromSeconds(seconds):g}" : null } - }; - - // Format: ServerMessage.Identifier1/ServerMessage.Indentifier2~[variable1]=value~[variable2]=value - // Also: replacement=ServerMessage.Identifier1~[variable1]=value/ServerMessage.Identifier2~[variable2]=replacement - // And: replacement=formatter(value) - // Variable that requires translation -> ServerMessage.Indentifier1~[variable1]=§value - public static string GetServerMessage(string serverMessage) - { - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - } - - string[] messages = serverMessage.Split('/'); - var replacedMessages = new Dictionary(); - - bool translationsFound = false; - - try - { - for (int i = 0; i < messages.Length; i++) - { - if (messages[i].EndsWith("~", StringComparison.Ordinal)) - { - messages[i] = messages[i].Substring(0, messages[i].Length - 1); - } - if (!IsServerMessageWithVariables(messages[i]) && !messages[i].Contains('=')) // No variables, try to translate - { - foreach (var replacedMessage in replacedMessages) - { - messages[i] = messages[i].Replace(replacedMessage.Key, replacedMessage.Value); - } - - if (messages[i].Contains(" ")) continue; // Spaces found, do not translate - string msg = Get(messages[i], true); - if (msg != null) // If a translation was found, otherwise use the original - { - messages[i] = msg; - translationsFound = true; - } - } - else - { - string messageVariable = null; - var matchFormatted = reFormattedMessage.Match(messages[i]); - if (matchFormatted.Success) - { - var formatter = matchFormatted.Groups["formatter"].ToString(); - if (messageFormatters.TryGetValue(formatter, out var formatterFn)) - { - var formattedValue = formatterFn(matchFormatted.Groups["value"].ToString()); - if (formattedValue != null) - { - messageVariable = matchFormatted.Groups["variable"].ToString(); - messages[i] = formattedValue; - } - } - } - if (messageVariable == null) - { - var matchReplaced = reReplacedMessage.Match(messages[i]); - if (matchReplaced.Success) - { - messageVariable = matchReplaced.Groups["variable"].ToString(); - messages[i] = matchReplaced.Groups["message"].ToString(); - } - } - - foreach (var replacedMessage in replacedMessages) - { - messages[i] = messages[i].Replace(replacedMessage.Key, replacedMessage.Value); - } - - - string[] messageWithVariables = messages[i].Split('~'); - - string msg = Get(messageWithVariables[0], true); - - if (msg != null) // If a translation was found, otherwise use the original - { - messages[i] = msg; - translationsFound = true; - } - else if (messageVariable == null) - { - continue; // No translation found, probably caused by player input -> skip variable handling - } - - // First index is always the message identifier -> start at 1 - for (int j = 1; j < messageWithVariables.Length; j++) - { - string[] variableAndValue = messageWithVariables[j].Split('='); - messages[i] = messages[i].Replace(variableAndValue[0], variableAndValue[1].Length > 1 && variableAndValue[1][0] == '§' ? Get(variableAndValue[1].Substring(1)) : variableAndValue[1]); - } - - if (messageVariable != null) - { - replacedMessages[messageVariable] = messages[i]; - messages[i] = null; - } - } - } - - if (translationsFound) - { - string translatedServerMessage = string.Empty; - for (int i = 0; i < messages.Length; i++) - { - if (messages[i] != null) - { - translatedServerMessage += messages[i]; - } - } - return translatedServerMessage; - } - else - { - return serverMessage; - } - } - - catch (IndexOutOfRangeException exception) - { - string errorMsg = "Failed to translate server message \"" + serverMessage + "\"."; -#if DEBUG - DebugConsole.ThrowError(errorMsg, exception); -#endif - GameAnalyticsManager.AddErrorEventOnce("TextManager.GetServerMessage:" + serverMessage, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return errorMsg; - } - } - - /// - /// Fetches a single variable from a servermessage - /// - public static string GetServerMessageVariable(string message, string variable) - { - int variableIndex = message.IndexOf(variable); - if (variableIndex == -1) - { -#if DEBUG - DebugConsole.ThrowError($"Server message variable: '{variable}' not found in message: '{message}'"); -#endif - return string.Empty; - } - - int startIndex = message.IndexOf('=', variableIndex) + 1; - int endIndex = startIndex; - - for (int i = startIndex; i < message.Length; i++) - { - if (message[i] == '/' || message[i] == '~') - { - endIndex = i; - break; - } - } - - if (endIndex == startIndex) endIndex = message.Length; - - return message.Substring(startIndex, endIndex - startIndex); - } - - public static bool IsServerMessageWithVariables(string message) - { - for (int i = 0; i < serverMessageCharacters.Length; i++) - { - if (!message.Contains(serverMessageCharacters[i])) return false; - } - - return true; - } - - /// - /// Adds a punctuation symbol between two strings, taking into account special rules in some locales (e.g. non-breaking space before a colon in French) - /// - public static string AddPunctuation(char punctuationSymbol, params string[] texts) - { - string separator = ""; - switch (GameMain.Config.Language) - { - case "French": - bool addNonBreakingSpace = - punctuationSymbol == ':' || punctuationSymbol == ';' || - punctuationSymbol == '!' || punctuationSymbol == '?'; - separator = addNonBreakingSpace ? - new string(new char[] { (char)(0xA0), punctuationSymbol, ' ' }) : - new string(new char[] { punctuationSymbol, ' ' }); - break; - default: - separator = new string(new char[] { punctuationSymbol, ' ' }); - break; - } - return string.Join(separator, texts); - } - - public static List GetAll(string textTag) - { - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - - List allText; - - foreach (TextPack textPack in textPacks[Language]) - { - allText = textPack.GetAll(textTag); - if (allText != null) return allText; - } - - //if text was not found and we're using a language other than English, see if we can find an English version - //may happen, for example, if a user has selected another language and using mods that haven't been translated to that language - if (Language != "English" && textPacks.ContainsKey("English")) - { - foreach (TextPack textPack in textPacks["English"]) - { - allText = textPack.GetAll(textTag); - if (allText != null) return allText; - } - } - - return null; - } - } - - public static List> GetAllTagTextPairs() - { - lock (mutex) - { - if (!textPacks.ContainsKey(Language)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); - Language = "English"; - if (!textPacks.ContainsKey(Language)) - { - throw new Exception("No text packs available in English!"); - } - } - - List> allText = new List>(); - - foreach (TextPack textPack in textPacks[Language]) - { - allText.AddRange(textPack.GetAllTagTextPairs()); - } - - return allText; - } - } - - public static string ReplaceGenderPronouns(string text, Gender gender) - { - if (gender == Gender.Male) - { - return text.Replace("[genderpronoun]", Get("PronounMaleLowercase")) - .Replace("[genderpronounpossessive]", Get("PronounPossessiveMaleLowercase")) - .Replace("[genderpronounreflexive]", Get("PronounReflexiveMaleLowercase")) - .Replace("[Genderpronoun]", Get("PronounMale")) - .Replace("[Genderpronounpossessive]", Get("PronounPossessiveMale")) - .Replace("[Genderpronounreflexive]", Get("PronounReflexiveMale")); - } - else - { - return text.Replace("[genderpronoun]", Get("PronounFemaleLowercase")) - .Replace("[genderpronounpossessive]", Get("PronounPossessiveFemaleLowercase")) - .Replace("[genderpronounreflexive]", Get("PronounReflexiveFemaleLowerCase")) - .Replace("[Genderpronoun]", Get("PronounFemale")) - .Replace("[Genderpronounpossessive]", Get("PronounPossessiveFemale")) - .Replace("[Genderpronounreflexive]", Get("PronounReflexiveFemale")); - } - } - - static Regex isCJK = new Regex( - @"\p{IsHangulJamo}|" + - @"\p{IsHiragana}|" + - @"\p{IsKatakana}|" + - @"\p{IsCJKRadicalsSupplement}|" + - @"\p{IsCJKSymbolsandPunctuation}|" + - @"\p{IsEnclosedCJKLettersandMonths}|" + - @"\p{IsCJKCompatibility}|" + - @"\p{IsCJKUnifiedIdeographsExtensionA}|" + - @"\p{IsCJKUnifiedIdeographs}|" + - @"\p{IsHangulSyllables}|" + - @"\p{IsCJKCompatibilityForms}"); - - /// - /// Does the string contain symbols from Chinese, Japanese or Korean languages - /// - public static bool IsCJK(string text) - { - if (string.IsNullOrEmpty(text)) { return false; } - return isCJK.IsMatch(text); - } - -#if DEBUG - public static void CheckForDuplicates(string lang) - { - lock (mutex) - { - if (!textPacks.ContainsKey(lang)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); - return; - } - - int packIndex = 0; - foreach (TextPack textPack in textPacks[lang]) - { - textPack.CheckForDuplicates(packIndex); - packIndex++; - } - } - } - - public static void WriteToCSV() - { - lock (mutex) - { - string lang = "English"; - - if (!textPacks.ContainsKey(lang)) - { - DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!"); - return; - } - - int packIndex = 0; - foreach (TextPack textPack in textPacks[lang]) - { - textPack.WriteToCSV(packIndex); - packIndex++; - } - } - } -#endif - - public static string Capitalize(string str) - { - if (string.IsNullOrWhiteSpace(str)) - { - return str; - } - - return char.ToUpper(str[0]) + str.Substring(1); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs deleted file mode 100644 index 395e5ffa4..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Xml.Linq; -using Barotrauma.IO; - -namespace Barotrauma -{ - class TextPack - { - public readonly string Language; - - /// - /// The name of the language in the language this pack is written in - /// - public readonly string TranslatedName; - - private readonly Dictionary> texts; - - public readonly string FilePath; - - public TextPack(string filePath) - { - this.FilePath = filePath; - texts = new Dictionary>(); - - XDocument doc = null; - for (int i = 0; i < 3; i++) - { - doc = XMLExtensions.TryLoadXml(filePath); - if (doc != null) { break; } - if (filePath.Equals("content/texts/englishvanilla.xml", StringComparison.OrdinalIgnoreCase)) - { - //try fixing legacy EnglishVanilla path - string newPath = "Content/Texts/English/EnglishVanilla.xml"; - if (Barotrauma.IO.File.Exists(newPath)) - { - DebugConsole.NewMessage("Content package is using the obsolete text file path \"" + filePath + "\". Attempting to load from \"" + newPath + "\"..."); - this.FilePath = filePath = newPath; - } - } - Thread.Sleep(1000); - } - if (doc == null) - { - Language = "Unknown"; - return; - } - - Language = doc.Root.GetAttributeString("language", "Unknown"); - TranslatedName = doc.Root.GetAttributeString("translatedname", Language); - - foreach (XElement subElement in doc.Root.Elements()) - { - string infoName = subElement.Name.ToString().ToLowerInvariant(); - if (!texts.TryGetValue(infoName, out List infoList)) - { - infoList = new List(); - texts.Add(infoName, infoList); - } - - string text = subElement.ElementInnerText(); - text = text.Replace("&", "&"); - text = text.Replace("<", "<"); - text = text.Replace(">", ">"); - text = text.Replace(""", "\""); - infoList.Add(text); - } - } - - public string Get(string textTag) - { - if (string.IsNullOrEmpty(textTag)) - { - return null; - } - if (!texts.TryGetValue(textTag.ToLowerInvariant(), out List textList) || !textList.Any()) - { - return null; - } - - string text = textList[Rand.Int(textList.Count)].Replace(@"\n", "\n"); - return text; - } - - public List GetAll(string textTag) - { - if (textTag is null) { return null; } - - if (!texts.TryGetValue(textTag.ToLowerInvariant(), out List textList) || !textList.Any()) - { - return null; - } - - return textList; - } - - public List> GetAllTagTextPairs() - { - var pairs = new List>(); - foreach (KeyValuePair> kvp in texts) - { - foreach (string line in kvp.Value) - { - pairs.Add(new KeyValuePair(kvp.Key, line)); - } - } - - return pairs; - } - -#if DEBUG - public void CheckForDuplicates(int index) - { - Dictionary tagCounts = new Dictionary(); - Dictionary contentCounts = new Dictionary(); - - XDocument doc = XMLExtensions.TryLoadXml(FilePath); - if (doc == null) { return; } - - foreach (XElement subElement in doc.Root.Elements()) - { - string infoName = subElement.Name.ToString().ToLowerInvariant(); - if (!tagCounts.ContainsKey(infoName)) - { - tagCounts.Add(infoName, 1); - } - else - { - tagCounts[infoName] += 1; - } - - string infoContent = subElement.Value; - if (string.IsNullOrEmpty(infoContent)) continue; - if (!contentCounts.ContainsKey(infoContent)) - { - contentCounts.Add(infoContent, 1); - } - else - { - contentCounts[infoContent] += 1; - } - } - - StringBuilder sb = new StringBuilder(); - sb.Append("Language: " + Language); - sb.AppendLine(); - sb.Append("Duplicate tags:"); - sb.AppendLine(); - sb.AppendLine(); - - for (int i = 0; i < tagCounts.Keys.Count; i++) - { - if (tagCounts[texts.Keys.ElementAt(i)] > 1) - { - sb.Append(texts.Keys.ElementAt(i) + " | Count: " + tagCounts[texts.Keys.ElementAt(i)]); - sb.AppendLine(); - } - } - - sb.AppendLine(); - sb.AppendLine(); - sb.Append("Duplicate content:"); - sb.AppendLine(); - sb.AppendLine(); - - for (int i = 0; i < contentCounts.Keys.Count; i++) - { - if (contentCounts[contentCounts.Keys.ElementAt(i)] > 1) - { - sb.Append(contentCounts.Keys.ElementAt(i) + " | Count: " + contentCounts[contentCounts.Keys.ElementAt(i)]); - sb.AppendLine(); - } - } - - File.WriteAllText(@"duplicate_" + Language.ToLower() + "_" + index + ".txt", sb.ToString()); - } - - public void WriteToCSV(int index) - { - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < texts.Count; i++) - { - string key = texts.Keys.ElementAt(i); - texts.TryGetValue(key, out List infoList); - - for (int j = 0; j < infoList.Count; j++) - { - sb.Append(key); // ID - sb.Append('*'); - sb.Append(infoList[j]); // Original - sb.Append('*'); - // Translated - sb.Append('*'); - // Comments - sb.AppendLine(); - } - } - - File.WriteAllText(@"csv_" + Language.ToLower() + "_" + index + ".csv", sb.ToString()); - } -#endif - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs index 6685423d4..60a8364ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Timing.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Timing.cs @@ -14,25 +14,10 @@ namespace Barotrauma public const double Step = 1.0 / FixedUpdateRate; public const double AccumulatorMax = 0.25f; - private static int frameLimit; /// /// Maximum FPS (0 = unlimited). /// - public static int FrameLimit - { - get { return frameLimit; } - set - { - if (value <= 0) - { - frameLimit = 0; - } - else - { - frameLimit = Math.Max(value, FixedUpdateRate); - } - } - } + public static int FrameLimit => GameSettings.CurrentConfig.Graphics.FrameLimit; public static double Alpha { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs index 20a23537a..a8e58b990 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Traitors/TraitorMissionResult.cs @@ -6,7 +6,7 @@ namespace Barotrauma { public readonly string EndMessage; - public readonly string MissionIdentifier; + public readonly Identifier MissionIdentifier; public readonly bool Success; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 41039f752..f8d4a1d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public object? OriginalValue { get; private set; } - public readonly string Name; + public readonly Identifier Name; private readonly string Multiplier; @@ -22,7 +22,7 @@ namespace Barotrauma private readonly Upgrade upgrade; - private PropertyReference(string name, string multiplier, Upgrade upgrade) + private PropertyReference(Identifier name, string multiplier, Upgrade upgrade) { this.Name = name; this.Multiplier = multiplier; @@ -114,7 +114,7 @@ namespace Barotrauma } else { - float multiplier = UpgradePrefab.ParsePercentage(Multiplier, "", suppressWarnings: true); + float multiplier = UpgradePrefab.ParsePercentage(Multiplier, Identifier.Empty, suppressWarnings: true); return ApplyPercentage(value, multiplier, level); } } @@ -131,7 +131,7 @@ namespace Barotrauma foreach (var savedValue in savedElement.Elements()) { - if (string.Equals(savedValue.Name.ToString(), Name, StringComparison.OrdinalIgnoreCase)) + if (savedValue.NameAsIdentifier() == Name) { OriginalValue = savedValue.GetAttributeFloat("value", 0.0f); } @@ -152,7 +152,7 @@ namespace Barotrauma public static PropertyReference[] ParseAttributes(IEnumerable attributes, Upgrade upgrade) { - return attributes.Select(attribute => new PropertyReference(attribute.Name.ToString(), attribute.Value, upgrade)).ToArray(); + return attributes.Select(attribute => new PropertyReference(attribute.NameAsIdentifier(), attribute.Value, upgrade)).ToArray(); } private float ParseValue() @@ -186,13 +186,13 @@ namespace Barotrauma public UpgradePrefab Prefab { get; } - public string Identifier => Prefab.Identifier; + public Identifier Identifier => Prefab.Identifier; public int Level { get; set; } public bool Disposed { get; private set; } - private readonly XElement sourceElement; + private readonly ContentXElement sourceElement; public Upgrade(ISerializableEntity targetEntity, UpgradePrefab prefab, int level, XContainer? saveElement = null) { @@ -205,7 +205,7 @@ namespace Barotrauma List? saveElements = saveElement?.Elements().ToList(); - foreach (XElement subElement in prefab.SourceElement.Elements()) + foreach (var subElement in prefab.SourceElement.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -275,11 +275,12 @@ namespace Barotrauma { if (TargetComponents.SelectMany(pair => pair.Value) .Select(@ref => @ref.Name) - .Any(@string => string.Equals(@string, element.Name.ToString(), StringComparison.OrdinalIgnoreCase))) { continue; } + .Any(@identifier => @identifier == element.NameAsIdentifier())) { continue; } string value = element.GetAttributeString("value", string.Empty); - string name = element.Name.ToString(); - string componentName = element.Parent.Name.ToString(); + Identifier name = element.NameAsIdentifier(); + XElement parentElement = element.Parent ?? throw new NullReferenceException("Unable to reset properties: Parent element is null."); + string componentName = parentElement.Name.ToString(); DebugConsole.AddWarning($"Upgrade \"{Prefab.Name}\" in {TargetEntity.Name} does not affect the property \"{name}\" but the save file suggest it has done so before (has it been overriden?). \n" + $"The property has been reset to the original value of {value} and will be ignored from now on."); @@ -339,12 +340,12 @@ namespace Barotrauma string name = key is ItemComponent ? key.Name : "This"; - XElement subElement = new XElement(name); + var subElement = new XElement(name); foreach (PropertyReference propertyRef in value) { if (propertyRef.OriginalValue != null) { - subElement.Add(new XElement(propertyRef.Name, + subElement.Add(new XElement(propertyRef.Name.Value, new XAttribute("value", propertyRef.OriginalValue))); } else if (!Prefab.SuppressWarnings) @@ -390,10 +391,10 @@ namespace Barotrauma int closestMatch = int.MaxValue; foreach (var (propertyName, _) in entity.SerializableProperties) { - int match = ToolBox.LevenshteinDistance(propertyName, propertyReference.Name); + int match = ToolBox.LevenshteinDistance(propertyName.Value, propertyReference.Name.Value); if (match < closestMatch) { - matchingString = propertyName; + matchingString = propertyName.Value ?? ""; closestMatch = match; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 1562443c7..027279c9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -1,9 +1,11 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma @@ -18,15 +20,15 @@ namespace Barotrauma public readonly UpgradePrefab Prefab; - public UpgradePrice(UpgradePrefab prefab, XElement element) + public UpgradePrice(UpgradePrefab prefab, ContentXElement element) { Prefab = prefab; - IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty), - "IncreaseLow", element, suppressWarnings: prefab.SuppressWarnings); + IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty)!, + "IncreaseLow".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); - IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty), - "IncreaseHigh", element, suppressWarnings: prefab.SuppressWarnings); + IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty)!, + "IncreaseHigh".ToIdentifier(), element, suppressWarnings: prefab.SuppressWarnings); BasePrice = element.GetAttributeInt("baseprice", -1); @@ -52,43 +54,87 @@ namespace Barotrauma } } - internal class UpgradeCategory + abstract class UpgradeContentPrefab : Prefab { - public static readonly List Categories = new List(); + public static readonly PrefabCollection PrefabsAndCategories = new PrefabCollection( + onAdd: (prefab, isOverride) => + { + if (prefab is UpgradePrefab upgradePrefab) + { + UpgradePrefab.Prefabs.Add(upgradePrefab, isOverride); + } + else if (prefab is UpgradeCategory upgradeCategory) + { + UpgradeCategory.Categories.Add(upgradeCategory, isOverride); + } + }, + onRemove: (prefab) => + { + if (prefab is UpgradePrefab upgradePrefab) + { + UpgradePrefab.Prefabs.Remove(upgradePrefab); + } + else if (prefab is UpgradeCategory upgradeCategory) + { + UpgradeCategory.Categories.Remove(upgradeCategory); + } + }, + onSort: () => + { + UpgradePrefab.Prefabs.SortAll(); + UpgradeCategory.Categories.SortAll(); + }, + onAddOverrideFile: (file) => + { + UpgradePrefab.Prefabs.AddOverrideFile(file); + UpgradeCategory.Categories.AddOverrideFile(file); + }, + onRemoveOverrideFile: (file) => + { + UpgradePrefab.Prefabs.RemoveOverrideFile(file); + UpgradeCategory.Categories.RemoveOverrideFile(file); + }); - public readonly string[] ItemTags; - public readonly string Identifier; + public UpgradeContentPrefab(ContentXElement element, UpgradeModulesFile file) : base(file, element) { } + } + + internal class UpgradeCategory : UpgradeContentPrefab + { + public static readonly PrefabCollection Categories = new PrefabCollection(); + + private readonly ImmutableHashSet selfItemTags; + private readonly HashSet prefabsThatAllowUpgrades = new HashSet(); public readonly bool IsWallUpgrade; - public readonly string Name; + public readonly LocalizedString Name; - public UpgradeCategory(XElement element) + public readonly IEnumerable ItemTags; + + public UpgradeCategory(ContentXElement element, UpgradeModulesFile file) : base(element, file) { - ItemTags = element.GetAttributeStringArray("items", new string[] { }); - Identifier = element.GetAttributeString("identifier", string.Empty); - Name = element.GetAttributeString("name", string.Empty); + selfItemTags = element.GetAttributeIdentifierArray("items", Array.Empty())?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; + Name = element.GetAttributeString("name", string.Empty)!; IsWallUpgrade = element.GetAttributeBool("wallupgrade", false); - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + ItemTags = selfItemTags.CollectionConcat(prefabsThatAllowUpgrades); - if (!string.IsNullOrWhiteSpace(nameIdentifier)) - { - Name = TextManager.Get($"{nameIdentifier}", returnNull: true) ?? string.Empty; - } - else if (string.IsNullOrWhiteSpace(Name)) - { - Name = TextManager.Get($"UpgradeCategory.{Identifier}", true) ?? string.Empty; - } + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + if (!nameIdentifier.IsEmpty) { - string[] identifierArray = itemPrefab.AllowedUpgrades.Split(","); - if (identifierArray.Contains(Identifier)) - { - ItemTags = ItemTags.Concat(new[] { itemPrefab.Identifier }).ToArray(); - } + Name = TextManager.Get($"{nameIdentifier}"); } + else if (Name.IsNullOrWhiteSpace()) + { + Name = TextManager.Get($"UpgradeCategory.{Identifier}"); + } + } - Categories.Add(this); + public void DeterminePrefabsThatAllowUpgrades() + { + prefabsThatAllowUpgrades.Clear(); + prefabsThatAllowUpgrades.UnionWith(ItemPrefab.Prefabs + .Where(it => it.GetAllowedUpgrades().Contains(Identifier)) + .Select(it => it.Identifier)); } public bool CanBeApplied(Item item, UpgradePrefab? upgradePrefab) @@ -97,114 +143,146 @@ namespace Barotrauma if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } - return item.prefab.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); + return ((MapEntity)item).Prefab.GetAllowedUpgrades().Contains(Identifier) || + ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier == tag); } public bool CanBeApplied(XElement element, UpgradePrefab prefab) { - if (string.Equals("Structure", element.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { return IsWallUpgrade; } + if ("Structure" == element.NameAsIdentifier()) { return IsWallUpgrade; } - string identifier = element.GetAttributeString("identifier", string.Empty); - if (string.IsNullOrWhiteSpace(identifier)) { return false; } + Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); + if (identifier.IsEmpty) { return false; } ItemPrefab? item = ItemPrefab.Find(null, identifier); if (item == null) { return false; } - string[] disallowedUpgrades = element.GetAttributeStringArray("disallowedupgrades", new string[0]); + Identifier[] disallowedUpgrades = element.GetAttributeIdentifierArray("disallowedupgrades", Array.Empty()); - if (disallowedUpgrades.Any(s => s.Equals(Identifier, StringComparison.OrdinalIgnoreCase) || s.Equals(prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return false; } + if (disallowedUpgrades.Any(s => s == Identifier || s == prefab.Identifier)) { return false; } return item.GetAllowedUpgrades().Contains(Identifier) || - ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); + ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier == tag); } - public static UpgradeCategory? Find(string idenfitier) + public static UpgradeCategory? Find(Identifier identifier) { - return !string.IsNullOrWhiteSpace(idenfitier) ? Categories.Find(category => string.Equals(category.Identifier, idenfitier, StringComparison.OrdinalIgnoreCase)) : null; + return !identifier.IsEmpty ? Categories.Find(category => category.Identifier == identifier) : null; } + + public override void Dispose() { } } - internal partial class UpgradePrefab : IPrefab, IDisposable + internal partial class UpgradePrefab : UpgradeContentPrefab { - public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public static readonly PrefabCollection Prefabs = new PrefabCollection( + onAdd: (prefab, isOverride) => + { + if (!prefab.SuppressWarnings && !isOverride) + { + foreach (UpgradePrefab matchingPrefab in Prefabs?.Where(p => p != prefab && p.TargetItems.Any(s => prefab.TargetItems.Contains(s))) ?? throw new NullReferenceException("Honestly I have no clue why this could be null...")) + { + if (matchingPrefab.isOverride) { continue; } + + var upgradePrefab = matchingPrefab.targetProperties; + string key = string.Empty; + + if (upgradePrefab.Keys.Any(s => prefab.targetProperties.Keys.Any(s1 => s == (key = s1)))) + { + if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => prefab.targetProperties[key].Contains(s))) + { + DebugConsole.AddWarning($"Upgrade \"{prefab.Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + + "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + + "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); + } + } + } + } + }, + onRemove: null, + onSort: null, + onAddOverrideFile: null, + onRemoveOverrideFile: null + ); public int MaxLevel { get; } - public string OriginalName { get; } - - public string Name { get; } - - public string Description { get; } + public LocalizedString Name { get; } + + public LocalizedString Description { get; } public float IncreaseOnTooltip { get; } - public string Identifier { get; } + private readonly ImmutableHashSet upgradeCategoryIdentifiers; - public string FilePath { get; } - - public UpgradeCategory[] UpgradeCategories { get; } + public IEnumerable UpgradeCategories + { + get + { + foreach (var id in upgradeCategoryIdentifiers) + { + if (UpgradeCategory.Categories.TryGet(id, out var category)) { yield return category!; } + } + } + } public UpgradePrice Price { get; } - public ContentPackage? ContentPackage { get; private set; } + private bool isOverride => Prefabs.IsOverride(this); - private bool IsOverride { get; } + public ContentXElement SourceElement { get; } - public XElement SourceElement { get; } - - private bool Disposed { get; set; } + private bool disposed; public bool SuppressWarnings { get; } public bool HideInMenus { get; } - public IEnumerable TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags); + public IEnumerable TargetItems => UpgradeCategories.SelectMany(u => u.ItemTags); public bool IsWallUpgrade => UpgradeCategories.All(u => u.IsWallUpgrade); - private Dictionary TargetProperties { get; } + private Dictionary targetProperties { get; } - private UpgradePrefab(XElement element, string filePath, bool isOverride) + public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file) { - Name = element.GetAttributeString("name", string.Empty); - Description = element.GetAttributeString("description", string.Empty); + Name = element.GetAttributeString("name", string.Empty)!; + Description = element.GetAttributeString("description", string.Empty)!; MaxLevel = element.GetAttributeInt("maxlevel", 1); - Identifier = element.GetAttributeString("identifier", ""); SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); - FilePath = filePath; SourceElement = element; - IsOverride = isOverride; - OriginalName = Name; var targetProperties = new Dictionary(); - string nameIdentifier = element.GetAttributeString("nameidentifier", ""); - if (!string.IsNullOrWhiteSpace(nameIdentifier)) + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", ""); + if (!nameIdentifier.IsEmpty) { - Name = TextManager.Get($"UpgradeName.{nameIdentifier}", returnNull: true) ?? string.Empty; + Name = TextManager.Get($"UpgradeName.{nameIdentifier}"); } - else if (string.IsNullOrWhiteSpace(Name)) + else if (Name.IsNullOrWhiteSpace()) { - Name = TextManager.Get($"UpgradeName.{Identifier}", returnNull: true) ?? string.Empty; + Name = TextManager.Get($"UpgradeName.{Identifier}"); } - string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); - if (!string.IsNullOrWhiteSpace(descriptionIdentifier)) + Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); + if (!descriptionIdentifier.IsEmpty) { - Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}", returnNull: true) ?? string.Empty; + Description = TextManager.Get($"UpgradeDescription.{descriptionIdentifier}"); } - else if (string.IsNullOrWhiteSpace(Description)) + else if (Description.IsNullOrWhiteSpace()) { - Description = TextManager.Get($"UpgradeDescription.{Identifier}", returnNull: true) ?? string.Empty; + Description = TextManager.Get($"UpgradeDescription.{Identifier}"); } IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f); DebugConsole.Log(" " + Name); - foreach (XElement subElement in element.Elements()) +#if CLIENT + var decorativeSprites = new List(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -216,7 +294,7 @@ namespace Barotrauma #if CLIENT case "decorativesprite": { - DecorativeSprites.Add(new DecorativeSprite(subElement)); + decorativeSprites.Add(new DecorativeSprite(subElement)); break; } case "sprite": @@ -238,130 +316,24 @@ namespace Barotrauma } } - TargetProperties = targetProperties; +#if CLIENT + DecorativeSprites = decorativeSprites.ToImmutableArray(); +#endif - string[] categories = element.GetAttributeStringArray("categories", new string[] { }); - UpgradeCategories = (from category in UpgradeCategory.Categories from identifier in categories where string.Equals(category.Identifier, identifier) select category).ToArray(); + this.targetProperties = targetProperties; - if (!SuppressWarnings && !IsOverride) - { - foreach (UpgradePrefab matchingPrefab in Prefabs.Where(prefab => prefab.TargetItems.Any(s => TargetItems.Contains(s)))) - { - if (matchingPrefab.IsOverride) { continue; } - - var upgradePrefab = matchingPrefab.TargetProperties; - string key = string.Empty; - - if (upgradePrefab.Keys.Any(s => TargetProperties.Keys.Any(s1 => s == (key = s1)))) - { - if (upgradePrefab.ContainsKey(key) && upgradePrefab[key].Any(s => TargetProperties[key].Contains(s))) - { - DebugConsole.AddWarning($"Upgrade \"{Identifier}\" is affecting a property that is also being affected by \"{matchingPrefab.Identifier}\".\n" + - "This is unsupported and might yield unexpected results if both upgrades are applied at the same time to the same item.\n" + - "Add the attribute suppresswarnings=\"true\" to your XML element to disable this warning if you know what you're doing."); - } - } - } - } - - Prefabs.Add(this, isOverride); + upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty())? + .ToImmutableHashSet() ?? ImmutableHashSet.Empty; } public bool IsDisallowed(Item item) { - return item.disallowedUpgrades.Contains(Identifier) || UpgradeCategories.Any(c => item.disallowedUpgrades.Contains(c.Identifier)); + return item.DisallowedUpgradeSet.Contains(Identifier) || UpgradeCategories.Any(c => item.DisallowedUpgradeSet.Contains(c.Identifier)); } - public static UpgradePrefab? Find(string identifier) + public static UpgradePrefab? Find(Identifier identifier) { - return !string.IsNullOrWhiteSpace(identifier) ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; - } - - public static void LoadAll(IEnumerable files) - { - DebugConsole.Log("Loading upgrade module prefabs: "); - - foreach (ContentFile file in files) { LoadFromFile(file); } - } - - private static void LoadFromFile(ContentFile file) - { - XDocument doc = XMLExtensions.TryLoadXml(file.Path); - - var rootElement = doc?.Root; - if (rootElement == null) { return; } - - switch (rootElement.Name.ToString().ToLowerInvariant()) - { - case "upgrademodule": - { - new UpgradePrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; - break; - } - case "upgradecategory": - { - new UpgradeCategory(rootElement); - break; - } - case "upgrademodules": - { - foreach (var element in rootElement.Elements()) - { - if (element.IsOverride()) - { - var upgradeElement = element.GetChildElement("upgradeprefab"); - if (upgradeElement != null) - { - new UpgradePrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage }; - } - else - { - DebugConsole.ThrowError($"Cannot find an upgrade element from the children of the override element defined in {file.Path}"); - } - } - else - { - switch (element.Name.ToString().ToLowerInvariant()) - { - case "upgrademodule": - { - new UpgradePrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; - break; - } - case "upgradecategory": - { - new UpgradeCategory(element); - break; - } - } - } - } - - break; - } - case "override": - { - var upgrades = rootElement.GetChildElement("upgrademodules"); - if (upgrades != null) - { - foreach (var element in upgrades.Elements()) - { - new UpgradePrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - } - - foreach (var element in rootElement.GetChildElements("upgrademodule")) - { - new UpgradePrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; - } - - break; - } - default: - DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " + - "Valid elements are: \"UpgradeModule\", \"UpgradeModules\" and \"Override\"."); - break; - } + return identifier != Identifier.Empty ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; } /// @@ -379,10 +351,10 @@ namespace Barotrauma /// ParsePercentage(element.GetAttributeString("increase", string.Empty)); /// /// - public static int ParsePercentage(string value, string? attribute = null, XElement? sourceElement = null, bool suppressWarnings = false) + public static int ParsePercentage(string value, Identifier attribute = default, XElement? sourceElement = null, bool suppressWarnings = false) { string? line = sourceElement?.ToString().Split('\n')[0].Trim(); - bool doWarnings = !suppressWarnings && attribute != null && sourceElement != null && line != null; + bool doWarnings = !suppressWarnings && !attribute.IsEmpty && sourceElement != null && line != null; if (string.IsNullOrWhiteSpace(value)) { @@ -426,25 +398,24 @@ namespace Barotrauma private void Dispose(bool disposing) { - if (!Disposed) + if (!disposed) { if (disposing) { Prefabs.Remove(this); #if CLIENT - Sprite.Remove(); + Sprite?.Remove(); Sprite = null; DecorativeSprites.ForEach(sprite => sprite.Remove()); - DecorativeSprites.Clear(); - TargetProperties.Clear(); + targetProperties.Clear(); #endif } } - Disposed = true; + disposed = true; } - public void Dispose() + public override void Dispose() { Dispose(true); GC.SuppressFinalize(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs new file mode 100644 index 000000000..98aaef668 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CollectionConcat.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + public class CollectionConcat : ICollection + { + protected readonly IEnumerable enumerableA; + protected readonly IEnumerable enumerableB; + + public CollectionConcat(IEnumerable a, IEnumerable b) + { + enumerableA = a; enumerableB = b; + } + + public int Count => enumerableA.Count()+enumerableB.Count(); + + public bool IsReadOnly => true; + + public void Add(T item) => throw new InvalidOperationException(); + + public void Clear() => throw new InvalidOperationException(); + + public bool Remove(T item) => throw new InvalidOperationException(); + + public bool Contains(T item) => enumerableA.Contains(item) || enumerableB.Contains(item); + + public void CopyTo(T[] array, int arrayIndex) + { + void performCopy(IEnumerable enumerable) + { + if (enumerable is ICollection collection) + { + collection.CopyTo(array, arrayIndex); + arrayIndex += collection.Count; + } + else + { + foreach (var item in enumerable) + { + array[arrayIndex] = item; + arrayIndex++; + } + } + } + + performCopy(enumerableA); + performCopy(enumerableB); + } + + public IEnumerator GetEnumerator() + { + foreach (T item in enumerableA) { yield return item; } + foreach (T item in enumerableB) { yield return item; } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class ListConcat : CollectionConcat, IList, IReadOnlyList + { + public ListConcat(IEnumerable a, IEnumerable b) : base(a, b) { } + + public int IndexOf(T item) + { + int aCount = 0; + if (enumerableA is IList listA) + { + int index = listA.IndexOf(item); + if (index >= 0) { return index; } + aCount = listA.Count; + } + else + { + foreach (var a in enumerableA) + { + if (object.Equals(item, a)) { return aCount; } + aCount++; + } + } + + if (enumerableB is IList listB) + { + int index = listB.IndexOf(item); + if (index >= 0) { return index + aCount; } + } + else + { + foreach (var b in enumerableB) + { + if (object.Equals(item, b)) { return aCount; } + aCount++; + } + } + + return -1; + } + + public void Insert(int index, T item) + { + throw new InvalidOperationException(); + } + + public void RemoveAt(int index) + { + throw new InvalidOperationException(); + } + + public T this[int index] + { + get + { + int aCount = enumerableA.Count(); + return index < aCount ? enumerableA.ElementAt(index) : enumerableB.ElementAt(index - aCount); + } + set + { + throw new InvalidOperationException(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs new file mode 100644 index 000000000..454d287ea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/CursedDictionary.cs @@ -0,0 +1,231 @@ +#if DEBUG && MODBREAKER +#nullable enable +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + /// + /// Dictionary that's been deliberately designed to be a piece of + /// shit. Meant to be used to stress-test scenarios where we might + /// accidentally be relying on the implementation details of a + /// dictionary that shouldn't be relied on. + /// + public class CursedDictionary : IDictionary, IReadOnlyDictionary where TKey: notnull + { + private ICollection keys; + private ICollection values; + private readonly List> keyValuePairs = new List>(); + private readonly Dictionary keyToKvpIndex = new Dictionary(); + private readonly object mutex = new object(); + + private readonly Random rng; + + public CursedDictionary() + { + rng = new Random((int)(DateTime.Now.ToBinary() % int.MaxValue)); + keys = Array.Empty(); + values = Array.Empty(); + } + + private void Refresh() + { + keys = keyValuePairs.Select(kvp => kvp.Key).ToArray(); + values = keyValuePairs.Select(kvp => kvp.Value).ToArray(); + keyToKvpIndex.Clear(); + for (int i=0; i> GetEnumerator() + { + KeyValuePair[] clone; + lock (mutex) + { + keyValuePairs.Shuffle(rng); + Refresh(); + clone = keyValuePairs.ToArray(); + } + + foreach (var kvp in clone) + { + yield return kvp; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(KeyValuePair item) + { + lock (mutex) + { + if (keyToKvpIndex.ContainsKey(item.Key)) + { + throw new InvalidOperationException($"Duplicate key: {item.Key}"); + } + + keyValuePairs.Add(item); + keyValuePairs.Shuffle(rng); + Refresh(); + } + } + + public void Clear() + { + lock (mutex) + { + keyValuePairs.Clear(); + Refresh(); + } + } + + public bool Contains(KeyValuePair item) + { + lock (mutex) + { + return keyValuePairs.Contains(item); + } + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + lock (mutex) + { + keyValuePairs.Shuffle(rng); + keyValuePairs.CopyTo(array, arrayIndex); + Refresh(); + } + } + + public bool Remove(KeyValuePair item) + { + lock (mutex) + { + bool success = keyValuePairs.Remove(item); + keyValuePairs.Shuffle(rng); + Refresh(); + return success; + } + } + + public int Count + { + get + { + lock (mutex) + { + return keyValuePairs.Count; + } + } + } + + public bool IsReadOnly => false; + + public void Add(TKey key, TValue value) => Add(new KeyValuePair(key, value)); + + public bool ContainsKey(TKey key) + { + lock (mutex) + { + return keyToKvpIndex.ContainsKey(key); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (mutex) + { + value = default!; + bool success = keyToKvpIndex.TryGetValue(key, out int index); + if (success) + { + value = keyValuePairs[index].Value; + } + + keyValuePairs.Shuffle(rng); + Refresh(); + return success; + } + } + + public bool Remove(TKey key) => TryRemove(key, out _); + + public bool TryRemove(TKey key, out TValue value) + { + lock (mutex) + { + value = default!; + bool success = false; + if (keyToKvpIndex.TryGetValue(key, out int index)) + { + value = keyValuePairs[index].Value; + keyValuePairs.RemoveAt(index); + success = true; + } + + keyValuePairs.Shuffle(rng); + Refresh(); + return success; + } + } + + public TValue this[TKey key] + { + get + { + lock (mutex) + { + return keyValuePairs[keyToKvpIndex[key]].Value; + } + } + set + { + lock (mutex) + { + if (!keyToKvpIndex.ContainsKey(key)) + { + Add(key, value); + } + else + { + keyValuePairs[keyToKvpIndex[key]] = new KeyValuePair(key, value); + } + + keyValuePairs.Shuffle(rng); + Refresh(); + } + } + } + + + public ICollection Keys + { + get + { + lock (mutex) + { + return keys.ToArray(); + } + } + } + public ICollection Values + { + get + { + lock (mutex) + { + return values.ToArray(); + } + } + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + IEnumerable IReadOnlyDictionary.Values => Values; + } +} +#endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs index 30ce32a0c..b7c975946 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Homoglyphs.cs @@ -7,7 +7,7 @@ namespace Barotrauma class Homoglyphs { ///List of homoglyphs taken from https://github.com/codebox/homoglyph/ - private static List homoglyphs = new List(){ + private readonly static uint[][] homoglyphs = { new uint[]{0x20,0xa0,0x1680,0x2000,0x2001,0x2002,0x2003,0x2004,0x2005,0x2006,0x2007,0x2008,0x2009,0x200a,0x2028,0x2029,0x202f,0x205f}, new uint[]{0x21,0x1c3,0x2d51,0xff01}, new uint[]{0x24,0xff04}, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs index 1ec3d25a0..04c00952d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/HttpUtility.cs @@ -35,9 +35,9 @@ namespace Barotrauma { public sealed class HttpUtility { - public static Dictionary ParseQueryString(string query) + public static Dictionary ParseQueryString(string query) { - Dictionary collection = new Dictionary(); + Dictionary collection = new Dictionary(); var splitGet = query.Split('?'); if (splitGet.Length > 1) { @@ -47,7 +47,7 @@ namespace Barotrauma var splitKeyValue = kvp.Split('='); if (splitKeyValue.Length > 1) { - collection.Add(splitKeyValue[0], splitKeyValue[1]); + collection.Add(splitKeyValue[0].ToIdentifier(), splitKeyValue[1]); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index 81af9005f..a9cebf7c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -22,12 +22,12 @@ namespace Barotrauma if (parentElement is { HasElements: true }) { srcRanges = new List>(); - foreach (XElement subElement in parentElement.Elements()) + foreach (var subElement in parentElement.Elements()) { int id = subElement.GetAttributeInt("ID", -1); if (id > 0) { InsertId(id); } } - maxId = GetOffsetId(srcRanges.Last().End); + maxId = GetOffsetId(srcRanges.Any() ? srcRanges.Last().End : offset); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs new file mode 100644 index 000000000..7dead65e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/LinkedPairSet.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Barotrauma +{ + public class LinkedPairSet : IEnumerable<(T1, T2)> + { + private readonly Dictionary t1ToT2 = new Dictionary(); + private readonly Dictionary t2ToT1 = new Dictionary(); + + public bool Contains(T1 t1) + { + return t1ToT2.ContainsKey(t1); + } + + public bool Contains(T2 t2) + { + return t2ToT1.ContainsKey(t2); + } + + public T2 this[T1 t1] + { + get { return t1ToT2[t1]; } + set + { + T2 prevT2 = t1ToT2[t1]; + t2ToT1.Remove(prevT2); t2ToT1.Add(value, t1); + t1ToT2[t1] = value; + } + } + + public T1 this[T2 t2] + { + get { return t2ToT1[t2]; } + set + { + T1 prevT1 = t2ToT1[t2]; + t1ToT2.Remove(prevT1); t1ToT2.Add(value, t2); + t2ToT1[t2] = value; + } + } + + public void Add(T1 t1, T2 t2) + { + if (Contains(t1)) { throw new ArgumentException($"{GetType().Name} already contains {t1}"); } + if (Contains(t2)) { throw new ArgumentException($"{GetType().Name} already contains {t2}"); } + t1ToT2.Add(t1, t2); + t2ToT1.Add(t2, t1); + } + + public void Remove(T1 t1) + { + T2 t2 = t1ToT2[t1]; + t1ToT2.Remove(t1); + t2ToT1.Remove(t2); + } + + public void Remove(T2 t2) + { + T1 t1 = t2ToT1[t2]; + t1ToT2.Remove(t1); + t2ToT1.Remove(t2); + } + + public IEnumerator<(T1, T2)> GetEnumerator() + { + foreach (var t1 in t1ToT2.Keys) + { + yield return (t1, t1ToT2[t1]); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs new file mode 100644 index 000000000..21cdf1480 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ListDictionary.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Barotrauma +{ + public class ListDictionary : IReadOnlyDictionary + { + private readonly ImmutableDictionary keyToIndex; + private readonly IReadOnlyList list; + + public ListDictionary(IReadOnlyList list, int len, Func keyFunc) + { + this.list = list; + var keyToIndex = new Dictionary(); + for (int i = 0; i < len; i++) + { + keyToIndex.Add(keyFunc(i), i); + } + + this.keyToIndex = keyToIndex.ToImmutableDictionary(); + } + + public IEnumerator> GetEnumerator() + { + foreach (var kvp in keyToIndex) + { + yield return new KeyValuePair(kvp.Key, list[kvp.Value]); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => keyToIndex.Count; + public bool ContainsKey(TKey key) => keyToIndex.ContainsKey(key); + + public bool TryGetValue(TKey key, out TValue value) + { + if (keyToIndex.TryGetValue(key, out int index)) + { + value = list[index]; + return true; + } + value = default(TValue); + return false; + } + + public TValue this[TKey key] => list[keyToIndex[key]]; + + public IEnumerable Keys => keyToIndex.Keys; + public IEnumerable Values => keyToIndex.Values.Select(i => list[i]); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 77ddd1d2f..688c284c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -749,7 +749,7 @@ namespace Barotrauma Vector2 normal = Vector2.Normalize(endSegment - startSegment); normal = new Vector2(-normal.Y, normal.X); - midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.Server); + midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.ServerAndClient); if (bounds.HasValue) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs new file mode 100644 index 000000000..08631f611 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/None.cs @@ -0,0 +1,9 @@ +namespace Barotrauma +{ + public sealed class None : Option + { + private None() { } + + public static Option Create() => new None(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs new file mode 100644 index 000000000..1bdc94160 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -0,0 +1,14 @@ +namespace Barotrauma +{ + /// + /// Implementation of Option type. + /// + /// + /// Credit Jlobblet + /// + public abstract class Option + { + public static Option Some(T value) => Some.Create(value); + public static Option None() => None.Create(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs new file mode 100644 index 000000000..fad94a2a7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Some.cs @@ -0,0 +1,17 @@ +using System; + +namespace Barotrauma +{ + public sealed class Some : Option + { + public readonly T Value; + + private Some(T value) + { + if (value is null) { throw new ArgumentNullException(nameof(value), "Some cannot contain null"); } + Value = value; + } + + public static Option Create(T value) => new Some(value); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index 7f0193f6e..55be14eb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -1,26 +1,68 @@ using Microsoft.Xna.Framework; using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Barotrauma.IO; using Voronoi2; namespace Barotrauma { public static class Rand { + [Obsolete("TODO: remove")] + public static class Tracker + { + private readonly static List logMsgs = new List(); + public static IReadOnlyList LogMsgs => logMsgs; + + public static bool Active = false; + + public static void Reset() + { + logMsgs.Clear(); + Active = false; + } + + public static void RegisterCall(int stDepth=4) + { + if (!Active) { return; } + var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); + var frames = st.GetFrames(); + string msg = string.Join("; ", + frames.Take(stDepth).Select(f => + $"{Path.GetFileNameWithoutExtension(f.GetFileName())}:{f.GetFileLineNumber()}")); + logMsgs.Add(msg); + } + + public static void Log(string msg) + { + if (!Active) { return; } + logMsgs.Add(msg); + } + } + public enum RandSync { - Unsynced = -1, //not synced, used for unimportant details like minor particle properties - Server = 0, //synced with the server (used for gameplay elements that the players can interact with) - ClientOnly = 1 //set to match between clients (used for misc elements that the server doesn't track, but clients want to match anyway) + Unsynced, //not synced, used for unimportant details like minor particle properties + ServerAndClient, //synced with the server (used for gameplay elements that the players can interact with) +#if CLIENT + ClientOnly //set to match between clients (used for misc elements that the server doesn't track, but clients want to match anyway) +#endif } private static Random localRandom = new Random(); - private static readonly Random[] syncedRandom = new MTRandom[] { - new MTRandom(), new MTRandom() + private static readonly Dictionary syncedRandom = new Dictionary { + { RandSync.ServerAndClient, new MTRandom() }, +#if CLIENT + { RandSync.ClientOnly, new MTRandom() } +#endif }; public static Random GetRNG(RandSync randSync) { - return randSync == RandSync.Unsynced ? localRandom : syncedRandom[(int)randSync]; + return randSync == RandSync.Unsynced ? localRandom : syncedRandom[randSync]; } public static void SetLocalRandom(int seed) @@ -30,21 +72,32 @@ namespace Barotrauma public static void SetSyncedSeed(int seed) { - syncedRandom[(int)RandSync.Server] = new MTRandom(seed); - syncedRandom[(int)RandSync.ClientOnly] = new MTRandom(seed); + syncedRandom[RandSync.ServerAndClient] = new MTRandom(seed); +#if CLIENT + syncedRandom[RandSync.ClientOnly] = new MTRandom(seed); +#endif } public static int ThreadId = 0; private static void CheckRandThreadSafety(RandSync sync) { - if (ThreadId != 0 && sync == RandSync.Server) + if (sync == RandSync.ServerAndClient) { Tracker.RegisterCall(); } + + if (ThreadId != 0 && sync == RandSync.Unsynced) + { + if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId) + { + Debug.WriteLine($"Unsynced rand used in synced thread! {Environment.StackTrace}"); + } + } + if (ThreadId != 0 && sync == RandSync.ServerAndClient) { if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId) { #if DEBUG - throw new Exception("Unauthorized multithreaded access to RandSync.Server"); + throw new Exception("Unauthorized multithreaded access to RandSync.ServerAndClient"); #else - DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.Server\n" + Environment.StackTrace.CleanupStackTrace()); + DebugConsole.ThrowError("Unauthorized multithreaded access to RandSync.ServerAndClient\n" + Environment.StackTrace.CleanupStackTrace()); #endif } } @@ -53,13 +106,13 @@ namespace Barotrauma public static float Range(float minimum, float maximum, RandSync sync=RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (float)(sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).NextDouble() * (maximum - minimum) + minimum; + return (float)(sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum; } public static double Range(double minimum, double maximum, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).NextDouble() * (maximum - minimum) + minimum; + return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum; } /// @@ -68,13 +121,13 @@ namespace Barotrauma public static int Range(int minimum, int maximum, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).Next(maximum - minimum) + minimum; + return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).Next(maximum - minimum) + minimum; } public static int Int(int max, RandSync sync = RandSync.Unsynced) { CheckRandThreadSafety(sync); - return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[(int)sync])).Next(max); + return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).Next(max); } public static Vector2 Vector(float length, RandSync sync = RandSync.Unsynced) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs index 51d66c2b4..54c09d18e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReadOnlyListExtensions.cs @@ -7,10 +7,13 @@ namespace Barotrauma public static class ReadOnlyListExtensions { public static int IndexOf(this IReadOnlyList list, T elem) + => list.IndexOf(input => input.Equals(elem)); + + public static int IndexOf(this IReadOnlyList list, Func predicate) { for (int i = 0; i < list.Count; i++) { - if (list[i].Equals(elem)) { return i; } + if (predicate(list[i])) { return i; } } return -1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs new file mode 100644 index 000000000..2a1585e5b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Barotrauma +{ + public static class ReflectionUtils + { + public static IEnumerable GetDerivedNonAbstract() + { + return Assembly.GetEntryAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs new file mode 100644 index 000000000..71de5e5a3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace Barotrauma +{ + public abstract class Result + where T: notnull + where TError: notnull + { + public abstract bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + + public static Success Success(T value) + => new Success(value); + + public static Failure Failure(TError error) + => new Failure(error); + } + + public sealed class Success : Result + where T: notnull + where TError: notnull + { + public readonly T Value; + public override bool IsSuccess => true; + + public Success(T value) + { + Value = value; + } + } + + public sealed class Failure : Result + where T: notnull + where TError: notnull + { + public readonly TError Error; + public override bool IsSuccess => false; + + public Failure(TError error) + { + Error = error; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index 2493257e4..47445df31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma { @@ -20,18 +21,17 @@ namespace Barotrauma private const string metadataDefinition = "metadata"; private const string endDefinition = "end"; - public static List GetRichTextData(string text, out string sanitizedText) + public static ImmutableArray? GetRichTextData(string text, out string sanitizedText) { - List textColors = null; sanitizedText = text; - if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator)) + if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator, System.StringComparison.Ordinal)) { text = text.Replace("\r", ""); string[] segments = text.Split(definitionIndicator); sanitizedText = string.Empty; - textColors = new List(); + List textColors = new List(); RichTextData tempData = null; int prevIndex = 0; @@ -80,9 +80,9 @@ namespace Barotrauma } } } + return textColors.ToImmutableArray(); } - - return textColors; + return null; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 2fece0b4d..fefee9d61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +8,7 @@ namespace Barotrauma.IO { static class Validation { - private static readonly string[] unwritableDirs = new string[] { "Content", "Data/ContentPackages" }; + private static readonly string[] unwritableDirs = new string[] { "Content" }; private static readonly string[] unwritableExtensions = new string[] { ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite) @@ -58,7 +60,11 @@ namespace Barotrauma.IO public static class SafeXML { - public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path, bool throwExceptions = false) + public static void SaveSafe( + this System.Xml.Linq.XDocument doc, + string path, + System.Xml.Linq.SaveOptions saveOptions = System.Xml.Linq.SaveOptions.None, + bool throwExceptions = false) { if (!Validation.CanWrite(path, false)) { @@ -73,7 +79,7 @@ namespace Barotrauma.IO } return; } - doc.Save(path); + doc.Save(path, saveOptions); } public static void SaveSafe(this System.Xml.Linq.XElement element, string path, bool throwExceptions = false) @@ -107,7 +113,7 @@ namespace Barotrauma.IO public class XmlWriter : IDisposable { - public readonly System.Xml.XmlWriter Writer; + public readonly System.Xml.XmlWriter? Writer; public XmlWriter(string path, System.Xml.XmlWriterSettings settings) { @@ -156,65 +162,42 @@ namespace Barotrauma.IO } } + public static class XmlWriterExtensions + { + public static void Save(this System.Xml.Linq.XDocument doc, XmlWriter writer) + { + doc.Save(writer.Writer ?? throw new NullReferenceException("Unable to save XML document: XML writer is null.")); + } + } + public static class Path { public static readonly char DirectorySeparatorChar = System.IO.Path.DirectorySeparatorChar; public static readonly char AltDirectorySeparatorChar = System.IO.Path.AltDirectorySeparatorChar; - public static string GetExtension(string path) - { - return System.IO.Path.GetExtension(path); - } + public static string GetExtension(string path) => System.IO.Path.GetExtension(path); - public static string GetFileNameWithoutExtension(string path) - { - return System.IO.Path.GetFileNameWithoutExtension(path); - } + public static string GetFileNameWithoutExtension(string path) => System.IO.Path.GetFileNameWithoutExtension(path); - public static string GetPathRoot(string path) - { - return System.IO.Path.GetPathRoot(path); - } + public static string? GetPathRoot(string? path) => System.IO.Path.GetPathRoot(path); - public static string GetRelativePath(string relativeTo, string path) - { - return System.IO.Path.GetRelativePath(relativeTo, path); - } + public static string GetRelativePath(string relativeTo, string path) => System.IO.Path.GetRelativePath(relativeTo, path); - public static string GetDirectoryName(string path) - { - return System.IO.Path.GetDirectoryName(path); - } + public static string GetDirectoryName(ContentPath path) => GetDirectoryName(path.Value)!; + + public static string? GetDirectoryName(string path) => System.IO.Path.GetDirectoryName(path); - public static string GetFileName(string path) - { - return System.IO.Path.GetFileName(path); - } + public static string GetFileName(string path) => System.IO.Path.GetFileName(path); - public static string GetFullPath(string path) - { - return System.IO.Path.GetFullPath(path); - } + public static string GetFullPath(string path) => System.IO.Path.GetFullPath(path); - public static string Combine(params string[] s) - { - return System.IO.Path.Combine(s); - } + public static string Combine(params string[] s) => System.IO.Path.Combine(s); - public static string GetTempFileName() - { - return System.IO.Path.GetTempFileName(); - } + public static string GetTempFileName() => System.IO.Path.GetTempFileName(); - public static bool IsPathRooted(string path) - { - return System.IO.Path.IsPathRooted(path); - } - public static IEnumerable GetInvalidFileNameChars() - { - return System.IO.Path.GetInvalidFileNameChars(); - } + public static bool IsPathRooted(string path) => System.IO.Path.IsPathRooted(path); + public static IEnumerable GetInvalidFileNameChars() => System.IO.Path.GetInvalidFileNameChars(); } public static class Directory @@ -229,22 +212,22 @@ namespace Barotrauma.IO System.IO.Directory.SetCurrentDirectory(path); } - public static IEnumerable GetFiles(string path) + public static string[] GetFiles(string path) { return System.IO.Directory.GetFiles(path); } - public static IEnumerable GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) + public static string[] GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) { return System.IO.Directory.GetFiles(path, pattern, option); } - public static IEnumerable GetDirectories(string path) + public static string[] GetDirectories(string path, string searchPattern = "*", System.IO.SearchOption searchOption = System.IO.SearchOption.TopDirectoryOnly) { - return System.IO.Directory.GetDirectories(path); + return System.IO.Directory.GetDirectories(path, searchPattern, searchOption); } - public static IEnumerable GetFileSystemEntries(string path) + public static string[] GetFileSystemEntries(string path) { return System.IO.Directory.GetFileSystemEntries(path); } @@ -264,7 +247,7 @@ namespace Barotrauma.IO return System.IO.Directory.Exists(path); } - public static System.IO.DirectoryInfo CreateDirectory(string path) + public static System.IO.DirectoryInfo? CreateDirectory(string path) { if (!Validation.CanWrite(path, true)) { @@ -289,10 +272,9 @@ namespace Barotrauma.IO public static class File { - public static bool Exists(string path) - { - return System.IO.File.Exists(path); - } + public static bool Exists(ContentPath path) => Exists(path.Value); + + public static bool Exists(string path) => System.IO.File.Exists(path); public static void Copy(string src, string dest, bool overwrite=false) { @@ -319,6 +301,8 @@ namespace Barotrauma.IO System.IO.File.Move(src, dest); } + public static void Delete(ContentPath path) => Delete(path.Value); + public static void Delete(string path) { if (!Validation.CanWrite(path, false)) @@ -334,7 +318,7 @@ namespace Barotrauma.IO return System.IO.File.GetLastWriteTime(path); } - public static FileStream Open( + public static FileStream? Open( string path, System.IO.FileMode mode, System.IO.FileAccess access = System.IO.FileAccess.ReadWrite, @@ -362,17 +346,17 @@ namespace Barotrauma.IO return new FileStream(path, System.IO.File.Open(path, mode, access, shareVal)); } - public static FileStream OpenRead(string path) + public static FileStream? OpenRead(string path) { return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read); } - public static FileStream OpenWrite(string path) + public static FileStream? OpenWrite(string path) { return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write); } - public static FileStream Create(string path) + public static FileStream? Create(string path) { return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 6e427d5ce..4101d9513 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -150,7 +150,7 @@ namespace Barotrauma if (ownedSubsElement != null) { ownedSubmarines = new List(); - foreach (XElement subElement in ownedSubsElement.Elements()) + foreach (var subElement in ownedSubsElement.Elements()) { string subName = subElement.GetAttributeString("name", ""); string ownedSubPath = Path.Combine(TempPath, subName + ".sub"); @@ -271,7 +271,7 @@ namespace Barotrauma string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; if (fileName == "Save_Default") { - fileName = TextManager.Get("SaveFile.DefaultName", true); + fileName = TextManager.Get("SaveFile.DefaultName").Value; if (fileName.Length == 0) fileName = "Save"; } @@ -381,7 +381,7 @@ namespace Barotrauma char c = BitConverter.ToChar(bytes, 0); sb.Append(c); } - string sFileName = sb.ToString(); + string sFileName = sb.ToString().Replace('\\', '/'); fileName = sFileName; progress?.Invoke(sFileName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index bc5aa7d5b..e165d24e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -122,7 +122,7 @@ namespace Barotrauma enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath; } - List filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToList(); + string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray(); if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal))) { @@ -130,7 +130,7 @@ namespace Barotrauma } else { - List correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToList(); + string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray(); if (correctedPaths.Any()) { corrected = true; @@ -159,7 +159,7 @@ namespace Barotrauma return fileName; } - private static System.Text.RegularExpressions.Regex removeBBCodeRegex = + private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex = new System.Text.RegularExpressions.Regex(@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]"); public static string RemoveBBCodeTags(string str) @@ -168,15 +168,6 @@ namespace Barotrauma return removeBBCodeRegex.Replace(str, ""); } - public static string LimitString(string str, int maxCharacters) - { - if (str == null || maxCharacters < 0) return null; - - if (maxCharacters < 4 || str.Length <= maxCharacters) return str; - - return str.Substring(0, maxCharacters - 3) + "..."; - } - public static string RandomSeed(int length) { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -186,6 +177,8 @@ namespace Barotrauma .ToArray()); } + public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant()); + public static int StringToInt(string str) { str = str.Substring(0, Math.Min(str.Length, 32)); @@ -201,6 +194,7 @@ namespace Barotrauma return BitConverter.ToInt32(asciiBytes, 0); } + /// /// a method for changing inputtypes with old names to the new ones to ensure backwards compatibility with older subs /// @@ -264,16 +258,17 @@ namespace Barotrauma { string color = obj switch { - bool b => b ? "80,250,123" : "255,85,85", - string _ => "241,250,140", - int _ => "189,147,249", - float _ => "189,147,249", - double _ => "189,147,249", - null => "255,85,85", + bool b => b ? "80,250,123" : "255,85,85", + string _ => "241,250,140", + Identifier _ => "241,250,140", + int _ => "189,147,249", + float _ => "189,147,249", + double _ => "189,147,249", + null => "255,85,85", _ => "139,233,253" }; - return obj is string + return obj is string || obj is Identifier ? $"‖color:{color}‖\"{obj}\"‖color:end‖" : $"‖color:{color}‖{obj ?? "null"}‖color:end‖"; } @@ -352,7 +347,7 @@ namespace Barotrauma return d[n, m]; } - public static string SecondsToReadableTime(float seconds) + public static LocalizedString SecondsToReadableTime(float seconds) { int s = (int)(seconds % 60.0f); if (seconds < 60.0f) @@ -363,23 +358,23 @@ namespace Barotrauma int h = (int)(seconds / (60.0f * 60.0f)); int m = (int)((seconds / 60.0f) % 60); - string text = ""; + LocalizedString text = ""; if (h != 0) { text = TextManager.GetWithVariable("timeformathours", "[hours]", h.ToString()); } if (m != 0) { - string minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString()); - text = string.IsNullOrEmpty(text) ? minutesText : string.Join(" ", text, minutesText); + LocalizedString minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString()); + text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(" ", text, minutesText); } if (s != 0) { - string secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString()); - text = string.IsNullOrEmpty(text) ? secondsText : string.Join(" ", text, secondsText); + LocalizedString secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString()); + text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(" ", text, secondsText); } return text; } private static Dictionary> cachedLines = new Dictionary>(); - public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.Server) + public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { List lines; if (cachedLines.ContainsKey(filePath)) @@ -426,7 +421,20 @@ namespace Barotrauma return buffer; } - public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Rand.RandSync randSync) + { + return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync)); + } + + + public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) + { + List objectList = objects.ToList(); + List weights = objectList.Select(o => weightMethod(o)).ToList(); + return SelectWeightedRandom(objectList, weights, random); + } + + public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync) { return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync)); } @@ -455,11 +463,14 @@ namespace Barotrauma return default(T); } + public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5) + => StringToUInt32Hash(id.Value.ToLowerInvariant(), md5); + public static UInt32 StringToUInt32Hash(string str, MD5 md5) { //calculate key based on MD5 hash instead of string.GetHashCode //to ensure consistent results across platforms - byte[] inputBytes = Encoding.ASCII.GetBytes(str); + byte[] inputBytes = Encoding.UTF8.GetBytes(str); byte[] hash = md5.ComputeHash(inputBytes); UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead? @@ -609,16 +620,6 @@ namespace Barotrauma return commands.ToArray(); } - public static void OpenFileWithShell(string filename) - { - ProcessStartInfo startInfo = new ProcessStartInfo() - { - FileName = filename, - UseShellExecute = true - }; - Process.Start(startInfo); - } - /// /// Cleans up a path by replacing backslashes with forward slashes, and /// optionally corrects the casing of the path. Recommended when serializing diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub deleted file mode 100644 index 26e46ca4fdcfd02f51d6a2e23853582891b5edea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 232755 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]*>>z#TSBsMFdC-&EumN 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%0D2i` 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
rk#JDjr7MKE zePm2ky7lOv8tm$mmFe-dq{9m$yp?F_4cJZq&F0o>%|IIfQbqZrowtRa`}9uj5hRDU zlfD>m)R|1dk>?Go)n4rMIe8vpTE{{{;H-gwGx=_WF`kL2`zjcM?*Jc|=&NBsL-qyM z;3a~F;YDxz8&Z^~IDk3Oml3$aCkt?IKS`x^y!e)aNpS(3`n&jz&b;jwQ2}!`aBxs% z14#wR2@vhh)d@HW%s{HuzWT^?R-g`Hn>eInOskNst5a-3&|`8eJd42hE<000Lkry0XA7@H_P73jRU3_Ycr#R#GS z2__Phq+og~7U=wfOUni)7S!Tg&IT_?m{!It?CBY0cLVKN9RU*^ZdrK!ds86&N6~p} zIjTfa^n(Zh;X|D8R`?9>gf{}dzN?d$&3YQCsC&=Zd!$Ep>|}y-^0950#caFEH~3s} z{BD63Y>qC7yg=Q#%r9tA5bpK6M%k`L(2O=01)OV6e5&VVG~-A{@z=W!7_b)_!}9_q zWdXfeEjUaJ9uOZ=l!GFT;ygz1> zP-KSUQ-VLXF%Yc*nT~()z$1D#NY*@2BO_25ERSh8?5kMY$?2hcbP#B#ipA_EQZmD! zsB{k~7@CG^+YL4b*Z7D%daHCaftyjq3bcgS1P&4RZuhYM(uO0&NF!PzXi+UGAyA5+ zaF}_=`*~ibixh9ZfB7A7X-!4F-W&~Av{p~9_>p7!SPEF6?z6ne#Kb0EEX2m}w9`sE z3Q$nreVPK-Z%F;w*U>7cJCWfO%{daFx{{{O50CtO0bHZzTNC*5Wz(-8cnk^0@qWpx3DZDFEjEnL9@i?n?V9fM-FF!;A z0IBCal3x#aMWRUpE8}rfnFo-W60ejdN9Eoy_ApeS47vRzuqR86gqAcPfBGcGo#{ZZ z`WP-y8E8wwsI=a)NgGO2;}swx16|PTo{JD{985x>$wqRNX!jmwyVVRr#8jRnU8zIC zuxbHk0_Y>)1UyX?8s@VO6Ok_|Lt^067e<|}71Oh3KJb;dLoNYbtetO*@}FNry!G}% zDd7Fe%-DXU{P!i&UfVPY zhWmG$a)^z@Yrvs@{r-na562>I;MHO6wAQ@r;4+PrWbUooob1*c2g>7VTc9kMO zgCS@{QTR>c@l!^Ncv^qu{Zqt^h<sBAGG&0qea28|WPcQEX zJoa;#EfZ7KX5v5oWj`!&!(d3q5HU9aVF1S>y2IFf(`by|w97^tkxf+&hpFz^_Fb1iTl?C+*) zfh`=E+kaE_VnMh3|L+T1R{j671%d59ry@(911vGia$iy{|Eq7sORRggQ3_6>rv6r$ z?}VqbIyri)%8(G%9MBK^B-}S@wrqiSw8NiW{-VOy9ut8`0R~cm_Gbv~cY_`_^u#_s zg<)-(yAKQkBud)S0M5`9$=OR-v}6Bz-@QdBegtloAAmxbFD;q>bTelK7o2stjx+SQ z?Q&k7uV%?UeU{7M9Lc_%Y`#6EKm)xpQLuC_vQn}+8-gkz_?=5mtEFMFXdBd>dIy+G zcx^!Y22yGl#bh_z`Au$%Y8K9DVMx>_-yz`sSv&`}$|+k~XSUmPISdg74JOE*Evyz4 zDNNrbkS2R|WF25iQty{M(-&2d%J}8w=3xDQi;m?Wpo6u7Pt7hi4>1(Ppx9Q*WK(rL ziTX%IIA|h;r23sQuqfhOPJem{py>|zAz)htgr5AE(b%Z^by#Ezh6GO^uPt?rYx=PE zQ1r@E%-vb6KJ_^ zN$uo-V!yTzluUX^d;!kkQeWxui<8;ufO8t-qEg|Z?IH5~A+&F%G{4u*QqyK>9hExS zSLosN>pTG5!!w>qwUwXi4G%`-c!rf+d8T|4YN(NZ0KN0cgyul)ACf-HTC(NL9;@c| zsok+K2`b*E{=P|)@o+Vy~whap??8}K`f+h9z*{_lgGHZo~JE| z{r{VV^wHOiQ~nOQ2w&6M5h`##lLM}{-wNU#eg+Gg0a1#9FB!bXRs0%6D_P1(?~oj1gw=DFBe3`#V=d z!FxED2KQpu_ZXLC6d>O0w-X*P3ua z3%`%uHEVoVv9fzCI@ z5g;dcNOMLiEBJ3a?!Lq&RJi;3_&B1#>a@8$u=xKkT4rBUnIy^h9z(xzZFhZfc>Vm+ zSF}LY*lkB)eB7&_KGQ0{cJPRw4v<>WJDAj8B)uL`J;xZ2@t!KYRNs8nrBg&Iw<-v_ z*|qySVzqBtzrhVoP~-+%l@IU%KYc&AhfkXv!4hGp$i9I6_O#`HHuqlL08OQ5SEg;T z#gD%NA#wR^tlVF%U_tr%#Nyt%5!e=i-!VzaxUwyTHg0>Q9WQP8g7K2wG&r6E9p}BJ!9* zfb|A0Zx$t#Uc)@4fJBy>F~L2{6`!w0=3@N@;9BQ32+nXe936P$G)LxI49CGG;B0vI zcEtfm=_7E4g0;5#QX?6_=6X2v!-ufmi&ju&nI;JoLO`{lt#u{=hU{@&-eRo7yI1`# zSPE5c79W()IWAOCGdO2F-JTb#&-;z`0U{|v-M(xCRbX*EPu#~H1-{3MZ5k+8o3)RE ze8o48zq+8gt_#8VmFeREAC{i+@y%1M(eX&@P5dP9^N#aX1U|a+Wug&vk)TnWk9aAK z98A2HA&($B4`g&J7@tCWsIAWhfg9cirG4|+K5cTs`7)ve(i-O(G<1$Rll~X)@C0=upVMXKmdZ&XG^uV(S~)H&<@>V4U%jp+^A1K(5rT6q zU;NG1cgx8?Ivh+i#*b!uNi`i^VNjw6a!yV1w`~SO(L81UJy5WXV^i2BGw0? zv1)%kZ~XTHybRWa%@k=${q0Z%alCy-z}*@#gheUnMeI1EiZcF*o#dd--p{c2taKs{ z0)w-VuPK{kkMrtIwTI-+X$HrGT>b`jdaVGQw0GQjB+?!C;Q5MguI#jwtDG?h z_Hp&x@OCh^_mYsI3Tr3AW_?)#Z-nJMq0H~(r@8V#A&-L{MscV^)@Ai<)8hN8F|mji|q>Z;h*F}E8RV9p_K zbH|wud2l9rzUZquAQR$Aj&8Sp2Ch*SIh@k-y~Yg z#h0z2N-pN_YiY#%I4k+5c)v!O>Z@3_!K3g}r#RN<@{VK%y=Vrc9ZBh)Z~a-7!e769 z$0=NA@_7Lay+a40+k_Ms8k2j+KlRIacZV$x5*%&)mA<_OPULfb2bb2{uq_(UjI7@m z<{UX6kwAsj;dP<@f6KMxwctk@t+~-aB}2!rav@C^Ew#^@aHj-EiEyJn<7DN0-TZ6K z@A-ts=K#$um$%bmA@h9ZUmkz8+xVb)msS?4@3fM?Cry^e+9>0I@LA%@$kae8Q+?x^ zNJbt7nyQz@{qQ`Ujq$UlchcZiY2|90WdH1DMo@NSqXPA$-W2$}@^Hk|JnSjII_6=_ zR0H#-5UQnTe^v)vi*9_F{mnh<3wvj&TTO0;*H1lzCC}WZ5aUS5u4$$-wh6t$!r*7} zCtV2er*3+WNI%OLXR@0I$d&o$SWvLcp5KyUK%ERCpk*NQZrV55neVGnnxlSZ6N?;@ zlmHHZd**=J<(SeB{6i4q^?>$hDq#FvmjkPOU>tNCNRs}yZlmBVa1#fDnRDg{No;7_ zx_%9^0{wcVjk3xde`?fR#<@c!S^nUcsbfWyx5@6ah8we=6`mAh#?kaj;_SO$aQBJA z#B<&{b+4e4b7ArE9#WuBj-sSHqy0Td9TwD~+in6Fp2V9eAUt+v^lRFsq15bg^!YeKgcrF&b!x0goq@cig zJ^E8)n|>l%||Y)Dr)p|a&{uf5^tm0OV*FHCVR;9M8cR^{b{CFyBehxz;~~xS1=Y(2@R+-U@^eX+ z3X@2L1n_gm%7DVA$Nov6K$_MsQA+cpKLqn5KtN(qG8zGpU#u>V+Hwj5FthO@P520_WM(?=qp|O!hJQqJoZ2a%V`J@f6qMmuEjt(!QY3|%3?!dm zZ4x+hK}5D(P6y906?B}Ze%neO-30(mV2BtVlTVbS_PHxwpINNnV;!vt0LmejF40G+ z9&yj-7Va6SbA2S3Z|P&7ovU=z^o^AVdXpj#h?aR+Ai3h)j5`EnP5sSfFJ(CGH&Zll~McQ-_=3aMU-#A&y zLXBB4f&Pa{P<(aRY!&ow5E)QW@>*s&5{Tz_n5ts32SnIRKV_PckK|3{^OfHwpH3#_ zPa|wl{dQ#qz#9AU#)6y0N#hKp4o%uad&j8(O)!DyHl<`Iq-afK&TWv( zwt*ECGWTUBRSz7(PEs#S`-;h%nS;J%O{pCEZ(=gpea!BP9_U=EQ>({R?vrx`gcCs= zh4m>1a%MhJvJSE)jck&3C$;P)M z=+z#bQFT5wejA`JpB1lnKVi2H?`HivhAI56IwrY{y^=N4iuW0V*1(gx!cNV^|2dgl zN6Nf@ptYm+?ZgBbhFFy#0Dc4d6(`fgB3C%DPAIzc*tMxji+rcWav3f#U4lV9v>KPa z?#Jk+o#GYsU@)087xrK%&e-62+eO%(rU=x^d@&f$K?~H-1&=za1F=lc{?Z?Q4g)4R z!x)xGwPJ&k?$wa%lOd@Y?FT*dsJR2kiL0U~z$q&$SOhFLi5WHdUc`rptFnZPmMJMH zT``%+27nl*Nh5}}L%C3YL$VI)en|(lkQ&}l-dY;bl&V5l7G+&Vlp9wW;V)Sm0YUIA zd-;-8=7(StPf$zrOZSdw;0OTW+}q4eD$0~*8jecOH|@nqmH1v;36v}3m(CXw$}vd9 zHK4gwF&!)h1xO`#)kUh?dFh3kE)gP ztzRdFL75{sK>zXV{_ugH9(~Y<_gX`EI8g<(161-#>G%723P*ps7W4?y|6E=$4 z2UqW3M74wKU$N=0tAqEUHK_@g)Pm`wLf?+eU`&;PN{|q~FEJZ^&z+Va203F779ws7 zpE_?xavV~T74BO16cG9DT6(nHH$@ZNBoDT2MKiAnzN1S zhO<{;(mPj7Bv1=I!vFt?yvTkR+ns+yAooQLglCN{wSasN0{XEaFQdh` zXnLG_JbTvaHj2mqr3ik6C1)0ujEgdt)^eKp#~D>i)^T2)$b%A2mql{?e50liPmS{( z-@gjC4mm^O6v75P7eP|CTDT13m0lN)jY(<&zR4}=>^z`|$+Aq?a&>4mMS6+|#AX@R z!o{{b^vXDp>cj@mqOK%`-`0GT?q_)VHwjF5!1Y(CJh~YV|@wSG=KBL`Y~|-xg11S(w$nd&0zEZD15` zprQG30K;ly3T?hSwk@cuA)OPQp8;9GE2l)6jo((QOX2|>ma7)U&x3v6x{BQf?}Y|f z>q##sL3>&{TD0ui<<-AL5kNiK6P6tb?t#VUkP1b6=c&Q&-vcD&Pp&XDEj}R_)dL}t zs>p}J3Cu6OuZS`+K|La925=rkB_cF=j|7{K>MAqkoo(eK z(8#gBJWGCMv7D~D4XA;sy)jY+T02OZYi zSn3VfXe))7UAV|@7zxbIvHg$4>f4&_4&OQlOsaLsT^lqA?|M`2-WwhXoNnh@-&1JS zD-;@j^nFBoJSvSZ`n_F<*-eX#3@d*8dmu_GEF*EC4Ty1y*I1vu?RV zm!PF?ab0TPG1mnp-bv2g6JXLO$oIH{b<4 z>z$y-+%rv9z$CHzims-~TAUZY?WLWzIu>hiE2}nuDwZi!DTljocy#!a9Y*uZpy)fL zN*zOj^!>2C4#crlf9@~g4zd7I0=OAuBdW^y0Nay>&S>ZB&07}SU+Eh!k~*;2MVVA3 zb^;Wh5P9~%GsQq(Z-Tm+FfBmN8fe=Ih6o-xcgYQ?THx~GYL8kDCzgypvU&A!MgP9j zAr6@1*%7Tync{`tzufHpH7DovJ{(k-7Vc3WtLkrmR!wB|IXq;I%9w+5`{2-~6I)Ck zK5BU8XSZw-th=N=%%IZIyr-GJ?p(@is>`mG0FbA9`nY7kfW%_2`+8g{bFrU9WmwvG ziV+}4`4wp5sj>9T9#B3oA-!hj&MJWbP+7q}jX?(ZKy>+wmw<|fUh4{=II_r zWCZ|(RsYNu*9G&qL5)62zs`YT|I&Ep@QTWLr1A8NYr-Ij`X;2+2Z?@$N-ifVD&g*D zj5719Z215Mw}-J-CL1=*QXu>*_8vd{t$Y0M+TqmGRos`ud!8vIaa_0>Rmn=gDbkvZ zdY95`bPBuN%@beEcqtE{Gd!gY2&iUsNs$^2C}}#fKrphl83X6gmZ&Amf1NXN#ECT~ z#@b8nYcw~t-;~cUGbRM~uv3=aLIt%JBu}7#mhQHV;D9)xjwImlw}LYHJj4YCaPZx* z9t7G57HdE{qQJ=bXb@Y6NkYD5W6V&{r<65_oWvD_uEE6iECZHpOaj4{{}#X2YWrdc z8lqKNR6B(FXh^GBUGPaJD}s89NoQii7;`gzeO!jmvO@Baow*CJ4J|d<*JNl!b)Yw7 zcA!dX4ds)G90DKO6m&0zaEZHxAjLmP-f=2a9PVrc`tey=l@wB5gOH-VW2X|~Z&2fl zbqCkZFAm>Bk+{V=zHRV8#F{^l5)=D|9S*)5y4ASgU>Tzo19Cc^y1pa)8XuUK34(OL z7V|mmM)Q{M@1SO~2P*2GAeBqQHStnayXk?pN~rD(vg9K}W3c)`kYx_L3vdR?%vq>j zdF381$^$J4S>4}V^&cX?7O{^E%>{-@vi-_ZO$Z+Y#7B4>T?sXSf!^oCPXzA`69)ny zQ|`0H(RO|*{paB++l|b8mWR@=weOGpq@A@86LVaI`vp7JY|S4>V}9dMHcfO9v*!7~ zuy34M8B7<*2+5=gR$J?eUb;OB(GQz{H=G8o^4jOrGiM;9TOxn372>CwJQ3Z1c{M+Z znzN~~C)`{;f5F|jeAv-cSdOBx5OVsmwUN{8Uc|257Yv^Zehrt+4zwM*|Q;x;D`26rjIk8FvRn2C7QHquAxA^+_>6gHn(}!}r5y3JZ z6FWl)ZzF(Y}>sBLeM|%UO1prr6enZSC?cRd|7HKkEt@`qh0U_Ho^%U7RH98`YDkNset*n=i%h0M|4DI8$mG(F}00ayqFAMBp~4)5jZ z;OV3gYDMpT zR2xvlmYR7(wXzExl5Bu9fdC?F4*2B)1px6w2*|*Xt#|5kE9fn z-g;)KHa<8h!lBNRx~_pcUaSgGcZ1?S&BbdDlE^SXb@%_Zdoe6*nK+{VU-oA2Zx-MK z=Xa=lECKb11@+>|u?$|f4^YME6`7=J?!D*xMm^1-2(F5+7Z8DK0eey4U`geANyhhj zeGmH>kcxXZZ4rSD+HM^QXyfkQS(CBl=Z*ksbw3Qn@Q^J0+&!o> z{?5-As6rhjro0)K1`w^k)HpYv#e(#1y53>Gl!ilvgmtKAFWXHzMb?HN_*L9I&>nxQ zJp?$1eWZ4)fVFo-)70xXqnD0satdTO(-+()+E#Vtt@KyoCo=_mzvGO+EmPmcx*cfo ztQ0UmW;7FRKEC~Tz{obdF(IV03IWteKb}A|)}~wk4ZJl5L|tJJHBYvgkLxnD+ZA&@ zhyk}#6av21Z4wsN>`5cZm+ixx#pIWrLUW*g4hPd<5Y%eP8cE--ASAC|tCY0aKkpMH zSSFsI*i$nuJJ%odUpKy{h!jAZfk$j*UJ!6>N!T3T56GV{3==`uFVuIw+6{405jqA^ zJ9~2XM)7=L2QFv{qQa9`BEXk%u3rU&7tzS3mIH#!59P2?BV=-L?E#F;BR0@e2J|au zn%Xr4D5#Al7LdFWq_?Zwmi-JW^?PvwNOW`?3^A4W^tQBnjS7N&6Zrw6=*@#E_#D3?--h+QTpUOfZ^&NK zCj;~Fz`UtaUx942K>kWJl^#0@L-PFTutaf`Z7F4_3jsDlrvde$!s3_K?)KCt^)3Wk z$lMrX^~2$A)N9{79sW%ba5bcDy~4+iU42(zIPDr1?46uUK_pmiFJVLR%^>41RHGni zpFGDBc*tQut_}js(ng>`G9P6_md7O`nbHhJz=CPP$DuP~D+G6YNl~(uRd~=idRZP3 z>Ofj7@aE?(iJ3w~KpA*o?b*CaxGdGczxj zs{5(hvCmO(p!M>53*7{cCt}aFfL?XVXTvlfE7i(5VVn?oq?8}yOdc5H%QfYP=(YfF zOMo<->Bou{L+bLHSfVFtUgxwQ!)moq2+{Zp2!#tURt{+?j~~>oGa7W7BM27mE7cG1 z>=VcU5`w8Gd(LmMzZgq3TMl?Hmp<7@R{bbq* z3lNYxkPGJ7em`f4df!Oc5GfwC z$_lRcUrv6NQh2Lft#ZE@#NTI5hVWbU9znjDO!a`#7k@|NCfvu{>iKiKoI=|@f7w!| z8c;ThCzbLA$=TN@z&P#0l(_Js6q=4av{XnE1+|>zA$bB`pj&@$ zc{J34Xplpy*roujy_8R%uq2BE73A%fppX8_`5FOS*DVw0 zed8~4+WrrnLq<7+Lo2<@;QvoHTrti#8@)_*_tg%=Ye=#L-gc?@bcYYGI+YFiO;39Y9o}zZ!}3E<{mlcZ{IYf9_e%_dy|FB#4VF}pCl)1y`45-Oe9~O zuop(bw4>~!@Z(g<3lLt7=K zFY~!N0oPfxeqyYr#p;mq+>4uY_e92Go6Yaz0Falon)&NXfTDH@vmpJl!(kS6&r zp8Udq(gtwf(7|+ln>Vl!fZS#BjZY{dPGcsLvCf;uf&3?f5O}k1sE%t>xFn&}_Yvg0 z7y`m%Km^URA26YZ#PwPh8hljk&%8^5gmE@*U;Gvln#~KcMO_Hd`F*BdPYedy5(^3* zu!TO;WZdOqNjuZnVA3*!T6%E83ADx{(_$-KG=c!h;>(|Nrud#XX-B}cd8>0-f1IbT zx!+XwdRT|l^|JCtiTVK>ztkH%$NlbnO=}rWwy7Euc;T9K__Zr0gH)$K9>XFri$;K{ zV}#WjC|fC+t~jXU-64^ZwM4|$fHgVlXTYaBe>zW{l8BAmg4lj=kT05{&x;bDmc6*GoOA!6vaBS1Uw|a#))hH zyMp%*+(UlCK0v{4HgBa7VNDT~Tc8`9vkX|v;*s$<@NrMN6vIy7NF_X&xJ0xOykH>R z#VY>;I+m~kl?#}n*#lWC3}v$Q_FfP{@B_(>4dVb8dA*H!0-OO-R}7{}dBpm(dis{h z>;DK*VGmN~chUc>)e2e$@u{m<4A6T<46+k(P35qytV9X=Ps)0HXXMYFUZxNf1`QBH zLHHNO=r%iNI$k2s#me<{?`!4`m!kBI`XKsusK61?iYc6d5TDkT0+p0y>QFj|G!YgW|YN$YvNFp6AD&;~&m%ASSJ^DAyZu>Vs$m=YE=z_>npR zJV*T{5m^UY8@j#TomElP$ zrd}JvQZE2Q7w-V-FOKg#+Z(@tso;B)AaFQb^>$8*`T@ir4G-~%6eth1wC-8dF{KHH zH@d0rMShhc6k3v+o)8;^Q8WH_mOq1c)M}KcZo?ua7S}eFkx*(Nw>qgFqv&EPV0Jpr`?Xr~ zTicuKD8v~Fx}qi0aVv4~{CD_tOYM&T|5U6$x}*wI zD|H~9HfV1t8(idV1W$hW$wfH!N=XM#itahJHaOE0$5bD+<&TLJxyUg)X?y8Hz4LUb zzf1cBRdVZXj{|EC`^{XS_{HD`yck#zk5G3@MkepCTt4mi|64ltjHcV76dVl~O{0>a zUW!i?=@#JI8Y{VeTPA4kS2ch9$nba~k5bg1G6$y|y0=C{sv22CpO8SPZYnD~&Uq^b zD}D|Zc?w)hoc;tY!n;4r`4j*cD#r1Q{HszfhPI8DFz%#tHMzyGw4ChZeF9+kiu2}SEu!Fgm z;+5-&mwXCOpYVAa-V;69fGQIOqy!`R&47Y&x6@mPXOS@`RPkb5-Qbe4LtY}j)32w_ zdX$>xUT$*MmM@Fg2$S*O;jx4QdmleuZ$=-N8cqKBub63IT-Jo9gw{`99kf|;iiBf)KQqJ^GO7kO4wusep3|7ub+G&ZS9~ z1s)$42W3k>!*7h~q3dQ|6%M77_u^BbaS5_~8$Z4*+X6bqsbg)-X2PFP2Vkw3V_^IE zukXeZuiy3aS3UT6Ir5EXk8*P|Z|5I@PMZ!i-$ZK>Cs}+Mh)IdNWsWZc?6OZTP!YdP z7x;p@*-#&-*lBm`>#M*8%BT;!s+X<58?Qz|ZCrm3sMJBjr}FEXy-i!(iS>1YQ4SIf z>w`I7f88yKKacSq0Nt$T@SE9=CDa4fk>*>F6TcFaCB63_gko@8l3L|<=uK-FwO++f zq^E7jtV`Dkg9$pL4$@vF)5hLm*yEw1I)J#_Dd2*~_Jt=`C(2f$mB;?u$wws~rr4O0 zh5@;cR89|VR9(WZ_2(V=tNLyagHE!@=9-9ctHKI+R)?~9JXL69FDX9qt>lDWCt zfUUm?2T$@QQ%qV*0C2T|nZ+XLK;NtT+Sw-k%##Rqgz02?kHuvGhx1?Ysv{%J=v+rC zHFnl``2Y8mF{(cZDC_FFgsaf|s1i)yY@jEtp!`Zp=^nmr#x_hqi!)xgz=NWHEm5Ww zM+20?Wf7ui-e%vpy!lt;u_{}Df%wj!P;=r4s0}be)qu-iE^e3}#Pp{f_t_DNK+dyi zj5S()HtW1XK!cvWMNOc4+o$${-E_o#SNdBUSoG~hq%pr}IH%j5rkPLWCmgVgPleMi zAbe5qYe8}FU+}Va-C?S?^p(=s?G_4gmtS;mB5%1p*Pecgo){c>woUbR)X{xdf?WvJ zbfYtWl4+lyGjni}=eB~D;t)UrZfmq_cX&mG%! zVy&tHi?3aT@d@P@*iJ$AVZ5L6yA4q1nxTDXfwF9BGGzNZ=ll;qruzvSqu~CB*}fgLEG?_xPG&Pf&?qkJYp57$f-9^L(rt(DkW<` z^nE1E{yy)r34(W8!_q^5nt7#G5RbQ45F>XXh#3MU5xOjyK2t8GEEjn3AfOw9gbKgz zo#Yt$JUKEoEL0W|k&)eRP-_x8Mb0R^`zYL~8Lk8%`4i_IY{tt*={!qN7gL^p{rrCa zWGX^bPMUg+_--NxnfiE6UN`^X?$LW~Ff3uu1|UIky#(`>JWbP07M>tJZOweg2HEH?qOItE7bU=FQ#e*MonDhO}n{7#vz+4X_ws*RPYtzUo$;=*>CQgIv5 zmvP z6ETR{PnZRuO8xvOK4}?Q%}HPoR7?35z7>3e6#ZH2HPu>>kVr8AG+5_uwOTgwc#0E& z&$blyRcM3!$^efM8=e7|T#q#Pw~Hn+^V+x<HmnHze)1 z4Jx148V`dM4*g6-2p+OTcKb^!ZMVVT+3(wGTr03ExJs3bY4bsYA|L@f7@xuhviUpl zb;5n+zq+51e^-t%Foam|XK+ELLbh%hKs(oIMp+?6{`y94$zLJ44NMI_ zWkF!(6oZ_)Ip6e+tPb_rEA5_+rM2P_s8g{cTU)6aG(&G|%0dM`(w)+mSvYV0`AV#N z*T-!z5m(AM;K$C}cbmq0h)-taqT}cC$YnS9#U4wNJIn1_vbJDZOAZ^RV6X+;4jo?0 zzSgnAG&4>=8&4C1HAD&^WS`wJa=pS=_)WY}K>@q7nI?ls?IZW~JYv!<*?!3)$^iiW zRo?7tr;bMm6J0dsRl`<_J5mL=dYY@y@@M*w}CB`5mW z67-3?PloMZ%L(7~LK*fSN9VERs2PRP2Vy}KVM$IRgFx&MIp_HF^Y@g?u5y(K2F=wcG;htAtf(&{wU$_=gi>|2+wlz`nnxfRz*0>5)kZhg)0a;2@a|#FpL6Z9#CejzVtqq}@ZN=(X0~mtpx1tfjaKzuyn>uxZ7*Bpk9dSUvUX{?16Q zq5-(7Uuw7HSmWl+fnK=V=HrL!UN!b}Z4eN{-OT+omf25a_kLtiij8tkEwBOR7Pr_n zO39h}Wpk{G`VSoKPoSmKuE|xIo|aJmy`z~a(eI^?aRhD@mo4^h~kU1~v zK-8ja`G&XV-sG%~=psYYr2%`~rpIr^dHJ9%AnFtXF*l@8Ye8Vp*?S2j1z=DxA@dq+ zG6S1S2E+KCFm*)qF9Z&01^5TXkm)EP=B~GYj2Bs#m{}hhwis9q@ivHMrZ6ckSiMOd z3E#I@HeVU(dJ5{vAf*#1H`zmM5$*@gzfXL=Pa9aA)RzA4`4gDA{1Qasx1HFm<{4=O zACU|DY{`@tV7d+j<^))I66<)`h4T{Z_Uo>7iJbit^2#Od31;@+=ngFH1u|pk3`Gr9 z=1Ut)QOJ>K2OZV>sN|4+y?Mq;W3VHdck%5TdVUxGxv|-^ok$i8kfMjK&bKVkhv#YXSl}d^C z9-r$I^Qi!$z%bGr;Jom+2uY>&6+2%&yH-^fzcVk+Ex&&$b?;ie8v+IKozCX~f1HCE zax^)SU=FNR1=&~%)Gm%AGO_4aKE*k?yF+k8a!ls6j`IN$1i3+`Z|Rg&lg<*$RGtH9 zV`FHV`W1Binm7Lzus&rUt>`8X>AWIuMw?LrGI*7xoAPpVvJ*fGCmUF>&6#} zj^^h3EXNKzF?J98zT@IA>qwYoGAOC{9V(pgXVy2jcGLX1DUPfX>NWQ(Hw8$+fTnxJ>mAVPC&puX?}Ge z%ZX6`??HT`ky5)VT|s5~qMM}S)VuM!%Wfv!BtIOHij7K;1N-6#bZ)~cthbajepMC5 zKo%d-1G-)vUyW>b!dXM;j2E>LN%e~dW`c{}ne4V{U36b_Y~yq{8|aolN;}F`t`-0^ zQjS)mz27Y{R31hc)tb(2Vz0 zV2i5IURd(?X8R@RqgL&h0_cAx1tXx80moM}J5qNqj*ut(>_6w+mK{>$E@bw_KJ83#jN|fQ-EqAQ%$95c7@TQ~mh+#kGCw z(jKsK-W4ja%z|b`gXG1d2oxmP55;J2tsZgt+8kOUoncImdQqqDmO++FnXJxW13wBT zcVq;0;K2|L1DIwj;gM=+5v$*Z!z9o*KRtoxQs1WQqCd3W*~K{y&2%|HTnNQO)TI#m z;X7&qc4r%>R(B$>hboz(eUN1s`hcr^f$D4U_c@je!Zj|?DgyQ z6s^2N^k9^UqU!fqwpo7`)K+4!3`0I-g5mki3Dj}G-}_OtGVMVj0I8KyWgL%zb-+#r z6FFw3)W>BfcJNc=S{8M8*l_N8H)0cr4_TFYKmI+-GBE(a=laKP;4S! zsl#pKzy9y56jH9ypfB*Hcf}v#CC`5%5&TQ@u$pEg1*DU*zZ|@xwa5vy$Y%;7+J7%3 z_Y6hw!%x_l08K!$zp`Ml-!v_s>^txbBL~(u&r^KTe#4iChFx(D=|eB+; zj#OlR#u~?Zf_>@py}4A=J}PQ+To|$s0~ZL%h@>F$-ws*Jt2)Xp1R;OOO^Q-tl?pJ3 zaf4#)+na zl?NT+1-4BW=EgwA2=F!aO{{A~N#gGnrQ{hmmml@XN(m|;bnTepmr%TgIj{k@`*q3W zeLh&T$?3rL(G1b>m!gYjSxCO$s{~r!DQfxR62B5&py!ZqeL@ep{%%_AUBwO4i0D;% z824^LR(aDzG`Ud16q{34V6d10TX}It`^Q4~BLCq)33+||hzBlpG9Ye)H-%GSg#-%v zTD*i%{JeKB(JayENaq4Dk*e|(4*Fm8t9klWEK)oG>-QC;FQ-0K=Qy5wrp7;OAd{{0 zCm976O(&C{4zPU@z|Ra2#OULxJ=Ay40_l~@{nHqqHM6?vgr>QB`7NyqBY`-E@CCN_ zTQVGQmW)z^%X~V1Ek)a#U2wLi=FNT$*%D&k-cS8k72H3uQuhxiToSYZ^!_vZ#HJVU{HSHrUPBCZ+N8Kg+m}mO)+XM z+Pu5i?QH%&bWMoR%+xF|-LJVqkHg(LkX+&4ELCjEZyGxCnr<$B9c4c~GCf%^ffQ^v3t+-jS? zc{P<9JaC_(8LcqUVeJMq=J`{m$5*9D8I z+LGV-*3t)NoKb6}f zuQT_GY?ms`DF)i?frmU^uyX6oIQf#Q#uKBr6JV;u_f>s*)yk>i)qUx@=)JAX7z=57 ziNRyx!!5RyTcp9M<{2`rZA(_7)~a!hfh}CP0t~r9v^YfXaAz^j7FU4Z7A*S%W)pN9 zE;vB4-?y|@tSYOnKLImt@D)Cz9ik8Su#bWOVa=yEDBSGd%4`XPuURypkXTjI1XxAI zsm>=6`tAnFx%~8!tcC2>%RVJHtFg>Qp$rT@ttJ7X{aEDB5XQyVdHZJa%xzDfOP~A_ zBc|$_oIxXrllK!7jEAfiJ}d{G_edf$*^B~9vo}r^x$j9efE9d<7l?=-n96Y&fa;7| zlv|!}R0sdcp~IHT0SA+^43P1C19?zkfJOkCtYX5mr?w7{H8(X z1iYXNEGJn{uSoUtPmh@(`Q?FF=7@OXg%(_X@rT^ zy;iRWk%Fu{dDHcN8My)_x)j~+`{`EUEfAV zje}+171%EoZOTsetTOy-sU1P@gcyeq3qjADKM@|)<>HXwh9V1D226pv^(tOH#pMT6LcK-C=euY49W20%xJGERnNj#_6veu&ap7QsMQQ{Finr~Fz0_j5*+JoizpPG ze6v;wU7UC>ed#1bl=609<51YgH=ujpENO3a!H_fR*si1!2Om15N>T6zF^}AcT89I4 zHRYey2LK}sKTvtD0>&k8m2WQP9E?!aonZ>OYLo&6=vK<7+cz)^CEP`m$p@Uy@yr*!(GI^{aJ6 z!e%)5yFk3eB3@$Yp3P5vSF(=jYb`vtqrf@Ty(r00t32Swt!JbU1ca6;0iq#Hbv8S_ z3>X%1pZtJ)`JUFhW=#!;l~y~uX|%rYT~FN9SL!XNJsD)-bUIq|cSU%!gT_#9X06DN zIrXQ)`7=7p(P-%5SUnlQ5d!AQFkJ^$fy6#-lVtJCw7;TNmR&HhXw6{jv8>f>8F!Bn zS6K8$cRe#vilqxx{uI+jfptM@MdxbVPw+m0!W@`tms*kx7nSkzXygm_Qr~S2ebkZ7 z^bC`AiPjRFJ|WX8-EoBBR`+Zh12JA;nhqq@GZwDCX&7iUVo7K;;%_6$I5?22eTT4# zLNBG^)pm64kuM(Jel49m-~hEaFl1Avc++aoC`bWRph@~ZBytNb{przf)j=?k4xf_^4NybK=Oa`1`CV_y=~*Wv-;W{Q83ny$iML^_caYk_f7`AD{}dL->9cg6CcRMNQELY_DTVVO z^7X;pcl9)?ae9P5&NO5lAgU6WF+;C>1E3xR&A_1zx8MRZu>QF(vHp8WYWZG%@0}f7 zEX@63(n7Dj;}!8#84f-D_crLC<--o&KOgMZk&3{)D8Se%u0w+L(*1GE!*@5gbK2qz zbouuB))BIh@?Z=c;oCNw;hFCbN}437WOn*(64JvYql8o<6@NIiOjO6dCX9)<+h~k) z`^7&L*`BR@KqB9OBCuiWTEu*5w7PW%}v-rT(5HTL7ga=$#%XUCl&bFfyU*O)q%_+P?=g z>(Ut~=PMt70*5PwF^eE*yh$U(BC>zaJ&5nj*c(6t61I5 zd17L%&~HR6h4C}#Z9X&E7O)B`f{^#+V_oF~0qC-qukfOw8HlU_lTQ zOhbKtL}6dyPoF9fe}K_KWA|JB@zy=uS|(fLCP^#yjZ{o$@1 z2Y`TQgvX!n!mURZVz5izKxmW5YV?gTA8=L3Gw$5T>)zA8xvYL_W15g`snw?@mSJ2L z1Q7QV%YA%*9%09T{2`1&=iXAC>#6XL@q`#>zhr3vNYB+ng-xUTz_ngK@8|DL+hWzP z?Z775HTT;bV9lZ4^A5II;s!;tPs5KQytoM2tc5D~{~>&HWWYd>7;WL)eiuW7~@M<;C@xE;4^!t>kg{1DFu5%#Lk|^Lo#WMrPS@Y z0|HnlUbOv7`Q~tqw1GYxM`S(m4_^(BUtg5g(+L^+u^i>2o{_UpbSk*-GMSEs5=4YB z4sa+y$S<}q_8MTVH9PKSiq&Wur2-D}Br@0-EOsy7@N@!!e8;lQ#nkJ4oE&dFdBc zE0?KnP#Xs95WLWR%BU07?UyX`2KydpOdh)JTtB7o+vBR3cscIkWXo%D){NjsCOO_p z^1P6T2RXapi#&g05|=gclPc`38hJRHc%tFB0B)j6^JrN`zYE@6cUXA($b7Fd;0S%Z zKDRf7ZSaA|ne31+^qqHF_C?|?VLrhHkr6?y|W#v{JxeI){51=^8BVW7eZNq zDNwHkuF$MYC&V>~jDP*N?uwJG7#X>GM(EL3M^IE6u0@1wvpJ2i^>DoMJRlAWKhBWU zzPf`ux_Ebx-GEtHY`r z7@4H1_J!FW5c5Knm_wQu5n6!YJE|uaCIOFTk2%NMa5q_jahlEQ@enA!1UY%0<_Lh< zFLm2V_X{H9s^mEHZVsR>+HZq+CVWTwZe~4=CB!S`6eMEJCc%2IunJ264e!~_{H5pRJwDo<0#VWY~KvSogb-6s>Qxj za@RihtEi76!|!bVqP&<$3u-7GrrgqT+(re1qZ#eZ0==NUC;&Y$>xIbf7u;sz_jC;7 zBL*P&QJ;6oDG7O>Sr^qZ15rLKIF8@pt5>e~7#0ZeEnqWiVA>UOg+h;OWZMHerXs9) z8$0Fs%ZlOs+ddfWc)MIR6Dq&Y?k&ceM48g5xj*lGfk@^gpkboWkJ;;!5X~gS%zF_EaP=Mzg?WcY;ckwSpZ~%S|ve6>bDoXg%tac zeQ@lsS(d|2$BYw} zdG8p7oCKWy>Fy9`gG){1^+P+CMa_jMLTS!8V;5f$sPt5v`fW2V+#j<;o}1+)>m%JO z3i5L3M90*^1HP+)G|~JF)&q-CHV2N~`mDs#Slm7_HU@0~a`+z%w4Ygk_H!UGC}+iEjweB`@ftL}w)%}9NO{tCWzCKO;MkOS1%kj3-~4q?w@KNfuo zSg`?N`0oS8_XQ=k!A?9h8VKIffQf2gmI+cK=h*NgkZ%)0$Q9_s-_~K7;^F(S+JBM_ zISUoO)t_Ypg&mieP5Ge7MQbCn){YX!ZrDrp*WMD0a)2p|S&QEw0J*3&v3;iBi)Nyc zhM4QorSX)R1mtkC118b8@=XrV>g*(UhyoJ%K|mpCGW^Lrwa?DIPjlSxW~H7j-lFe; z;|Im^%I*}BWz2U_vPSd{jS5(X)WXWY{-G1CrE|A$P9gNWSm*rLGS{Q%_8W2mIbpW2 zpirRm2^p%w!LOltp)aILl;JyWY{Tl*}TA_tkY_9Cic%&;_zj5dr8VIIF-_yL=-XAi6C3lJ3MZ^rg4tGq%FUCO?Q`zs*;%T zXUkX6c@l@6`hp?f4F~^4iB>ig!zaroAeOAfSSS6qtn$*O)k&r;Fd17gKo6}f|Ih}S z=Vd`tz!`on|31H)$Ie=B0Y~SXa&;v*GOAUFAW7UG=>2*D2?-XXw`m^D01T?HqopB`W#o}a?XMo- z^4T3O^|b7`^ght8mqW&lzjGAXlU!Wl)lyoqFuh&Y@0CG{?tO0jK#P0!VDaY;Y&iw`}?}yt&L_rt-usvac^Jj z0>wgpis6-IssX|$P4ZSb3A-xzi+(Q3j5&w*6rup}ZE`+Z9ly)J2YsMAJYT_KH+4c(Z(w$W&*up4J96I( z09ej3{5_eGLsI{1V@aN((4_)X6XX# zg8p6o%;^+~Z^eaKHXO8+E~VRhQ#s=LdpV)6U*CSFs|7u!99Ta}3yEcF4^BwM_$E(n z<4|LV@{b&s{13%IN)gyp=gd--H7nA)t^SO3zKrkw{i);p(5smN)--05RnfYN%k9!e#@bLQx+hN6Zx%sjH*&KG;?#8+KmbZm+}~SXR!s~| zYo2(OSKcZV9-C~4b@VU?+?-PX!LsO6LrH_nuS|HYss*m7uwJERd~)qf8#ae6E`xww zF*%z`?nsQ<{t3DmT6~EMWYo;%6>{>PROGtq;y25C$2-Qyq+T#;a8pHOG*dmsVP8&k zjbIVq989sdTItnF1;MozE!$%Q7RAg^0!^>T5YwiW10DuM;%tC)_>sMy%GQe^m>u!K zFcXHA!IHXAeI5N|eOsh|R5RrZIARn%3#k%FY}$h8_EQ2T)1SQ3G_ObX4IBhno#d8L z6&nvgW^c~DZ|^1fLU!_*jndQL5LRMrSXFUJd4XB&!F|i*MuDzl;wK{I>!mnHfd#+` zlP2NkGc{mSCUa$J$0z&sL7EytKB?ua__ z`Jpt>$^dJZy1{mZikV=+a*4cW5>%cAusJu$J;TPJmt$yFmDqXY$j&YjqC* zTC$uLu={Uw9c6qaDh0zI;HMAT79Sv&t#qd3Ikg`9Q6JaA-GR>(@hsJo&!6L^c`i$f6eV&liUq!u#<-N_khYLy;3GN z{1`y%RaDMp9l> z&+CpSazd3vg<uR+tsgOS;Vu@F*OyKa8_ZH^*CtOf1kDBk z`~g6x8MEqHx^K3N!0Z8XyYi6iOvT4#5ZMP(q!&&~Z-%;VilJZ}{GF~GVSXYY?r1}` z{+?OFSMgxoNdwLl#=~rSStdUZAQ&V(@jI{+8xKkEPu&TfZb_$VO6Naalm*N@pYx@; z1jtOkI;a&cLLW&#CyZ@1#I9cc={*O8Y?tm9D#7>e^MvUaM>zKT8-CV0MZ8`*AS>AX zeczJs(};9DJH3{a1E4gR(zOp!eo}YlBv!+{NWJY5#DC(WxV%^8o8+PRL;!jtK?F7; z=984faY~ESqvEpV`Ke5E+^Z5u6kk7Hr+uM1zShwAfH0$PBzTK#nf-DHU|R&W-uwi9 zTNy(1`8tYyU2@yMv?sc^KJtxH`u~>$Fggi-U0^Qv;h}i1a^^S8j`EA1GpFICe(#p8 z2bLHFsyP9r%s$Y?4RVfEn}8(8#=}K_-+dD%GC$qed*{**tbYn#f%7r8#RlNQ z^f?V#28N#2#glXhbU_1E9jJm?Be}kcCI?2I$h-4v#!|mnu9YM#PJoZvN=6=+iHI|G zM$`NeSok`p!=Zl%wSwjDFIqWE19gmVDKMRM^?V9SL=`|Trz*|$S%#2}J5dK$dVUN6 zlP&OT*%BxC{XK}h-cW|&52g9sCDKI=_A%3JbAwPk(E@|U1)V7lMlrH`QL?UvMys!! z@EBhIx4xp75Lplm^uKeFzApXBFVHb}*i0xHhxlS24~(4AD9?EWhc+8X zr1S(qnzsJ9U*FFhgZ;lCdKUqUSZVN1C=vuGsN+TM5BLX$Q+~20uQ@a|slv-muG2{!_(_isc+a)Eh z1z=nJndH!jhU z?oZ_9?O{(~n+0-s+;rCX-k_%ifn;jqFRyfgE$DzPQrfwkm=-f{Z|YnRx`C`h7;Jn@n&1ruOG50lXj8eeE=$Z|@*R;Y~X zPx`P`K1nW9cDF!J3`2SE6^$L%hTX4oYBKMznIZ?hOAm(fDL>jR?Q~dWTPf4=TYYDp z$*TeZku{L;=$O?Hz$kXjz^t~;xkwoJU(EL~!% zh}on)N!2b7JCB&0=z|G_FGj$A;MU&bhM7uPu*?~Lkczkl5Ar7uC0;$7?m;rwsY(2| zpgKraUW5qPvLG=u@#(~Cjpsd+!M0Fk&I{p7aK+6n$kU($$kl0(fZ6SI`{OV{45*ct zN2U$1)_^8~dk0oFnPI>AD?nQQ>2xy zI??llTqYFY)tu3Am4XPL2Fc+noTOS3Zpu#f0&$F(nW zeQYoE-Q#Eg60>y(KS#ohNr^h?z;^3G(v3%mr{CVNAd5wRC<(Cz#YzzT+stP+CaQ*u zW;%&=Tuey#pm+|l&ppe^6P$&&`l`6CtF)8kLlQn<9LCuQ4A{0$fq!#glcl|U?ruSRW{iyv7yF6o$2mWdYj_etw_u+&ZJe%Yr4)9& zKzA7Jl0syKum1f~;TUrCMN<2`qI8wE7IN5Hol+k7?|_~$JR;CFl;(NS<35k7C9W1( zSY6Eq@kUx;3KLO5gddfl4W0yi2mUS0YB*8SHwcmDdAHa$sDQ{oXE{&db^dWCpq*P0 z5cR}u7Qc)%P_zy-+p@r6Tjki`49!qW+WV)2MgdkMY$JI<*`!+VEDdCgtRy4*fK83?54_4~6&6vy%Qy=}@x=Cr$Oz>nLz=u(b+l@Y)} z8iw`Xey}Miu7kyZzzy20LF~7`XrSE@Bn?7DfY;f(`Xaii#_PyiE>Kwzbo+tP{``V$ z$}v3OxG!EHl9xNcv;+`H z@3c(*g3Dh@6I-D^bSGJKK5ty$wZq4X@d|_!8G5t3QJx#iAUq^vatg6no{v!&P5n9j zHj9$_$D1x({;|Nqn?U#!@VeD}W&}=pbaw-|Fznxs5&LryjMX9!<)<(4dpy}0wcp0#c3Dww+*de= zJDf1%=%v?!7e;)&JcGX}A{W&xbaM55Gw?fF_77~a*kOC1h<+y48+G0TM#YXtpXkRV zj8Kx=X$*c00gHC8)W^R0h;%o>m6^d zX`{stxs~HFlk5B6M&&OT-0JtEi`M)rW0}(c-l~_2s~bLbHMqrm!!gM~<+8Q85A1$X zxQQ(B)610mmLJ4Zp1;QR3zlxItW^fNlqwt*(5ve!-LDW__o~F2VANQMu+D!yP$zy^ZoXiS~h^(wyI+SZ^hw6@cBIvxCtijOkb~U5tVn^_3MjHSl1XCS2^hB zDA*p}D(D*VK|O=8HXikzzUcr#6xz3nKfH^aVVK7U5Zop|VoAm5LDAj{IY+rxa49{{ z=vg+?)fnJ9>da&Cf$0T8xgxY-zNQW?o{?1r*{t8(n-O5cto}Ww3fSkV8M=EJ8BD6* zbsBVezQi9>Xe(LWj;9Dg^ILUpT|O72R2TK)K#p@g@brO;w1_j3+aZF$6W4GR$@4zB zN?;!SuBQ|9SJ6R_uEB#6eN99S>p5dQgEnku97fUCj_{%4B(sjXfcWaGbJb~7rB(g+ z+D$F8pK4n+Cz!-z?Moi$70kE0fW_GvV98$I{I9ALNmDVSNz5iHZ41gckgi+f%M^g9 zP|KijU?sgpZp?uU{uh9`iUKP$KE!i0`Kts7!xZGQBjVIU8;u04+y>}Q^apCMa4^Eq zElv9a*joic9*}IHWR~wXrvf~)#eVLwZc!B;d8n@s>Ed*7n-570+>b^d6v(w>{2OU7 z^<#}!S(nAJNK58vn$##vABx8BzzHjnu=#)jwAUl29+bH-ZT7$_3dYVXDL>^@j-~5g zYZf3{cWW0}d>C9V5uIp6^DQOu!M=5Kel=ki80(dZm3YI$s+h|Sydw8j=8VcppbUGf z^;5gB+KRJ)b6|2Q%CYK|v_E+>%aZ5B7ljDi-$R4t+P1%?{*^$t0f7YEB0xddz^e8& zb=qsd>`f_Dqoq;XyJdp7GnmQ^?kxe8oOB&t#~BsW*hfm&y_c`GON;E%AeuRfT7378 z`W;hgD{vy=zn${Elv#ImOb=*BqS_bg-yc6^_W(ol$CcNi0JjL2^afBc+sA$TzYn*2 z;m`x;>T^Tfdgsf&j*8l9A#HHH}-4=jHK;PFKd6P~MfvMGPZa+kh~q z`S>D1fQJu$@Bu=?+l!<(2D}P=G4whXO~8nKgBDnDAS_dsb00Q} zF0{wL*V0sF*z|R4cj`+sQEOb&xz7l^tDvXv5hh;)L~7{dgv#%Bp61#R%U5+#ndy6y zn2gfx^WfscG!5U1hUWGXonQui!@k@xqIz=_!i>#L;6k%322_TZ&xYCOlgJ(5Pp@2b zVo~NyJ!_j0tb-lXIGFkr8L&ECTOh$o-vHjEC?Yr8vQgF@V+GdR{bd<;AKGw1U620G zv0glit>xs27n!WRFqc5rN^1N`eKGGXpe*NNgMgZ* z-g5*C1Ie-{V=lpgo`>5*F_=;)AR!+seEy*aoZeP_fL}E5Lwtdf!fTc7WY#b z=o&eN2#^5yk1bzk`y#50L0xw-YWe-p`C9OBxC=vZ28#|ye_42hCA9IXm355{5HJWsMjsc zhlvjow2j@t$L-all~<+A55i|AjUFe%K~-$P%Zq=2{c|Tf>!S|zBt4o&o6UIK=`J6u zG?kG4)P?qHkc!A!U?+S>y0X6GMbg;;CizoY8g&snnR>z%wJ%q*r49Zi?@K+=)Q|;8 zxN9fL=tG=#==XkYjJMp{+qE#sIILq4fyrs^k(^y5F0&a-z1e4k@?UsbHDk)X1U3p} zyScX>NAU5JXwP`n<-6S>``q|-<_Z9|RoNuO8$;{){Gs%S{Pm`123GP+9vCb5D1xTE zH-K?f&|~DVv2Bdrcx$Yx6ytwbqpBpHQwVlFqY^3|x%_H*!CXZ+W>P2J3KApl|^%fX5ZLsJ&f zjDf>L)xZH=_PsJ75fCUM=66|fCv+WG`5Op8@M?Qmo30_jgxiA2yp(lBR_4kujL*+p z(oc;A2`Y3?DyYU+R4DkTT%zOz;rfg7r4|G^Lq2r4zMnj1OE9Bs$I&l8KGk|hc3;oXq$es2*AeDXx#e%jw%vXQRXQMq60SPNbC$zjdc1~L2Q`MAXZ>k=7c z1D-K>-fDl{?=K)hwc$CRz)PBt=>wg~)n&P7!v*rK{snU+g~r-=r}fBNJx~coG|m{I z^U8XEyOr3NP9Rb$C*IV^VP7I|oS}Qw{EJqcQ%Ld=VYilV_0Q2arLrK9s!UV8?m$0? z6%~S4Y7@2zz`JYu8hRVVlF$vo1yfbnxrkq*?pIVz!!j?&YHe+kJL(gm zzuQ3BNa28|pMBuiC?MPHFYgV^py5{=N1xBk&IWp`Iy)7sFV^P?LG@u2VUkMy^t2~n ze2fgL1lR`4j^-wm3@UH>1?sRBh3btyBI6xa`3GP|lQNu-*^EEHa8)u6kz_Iw{{iP% zH$EV7{Yn;h!Ta;MSWpVR%h{ZwrKpBR4e=K}V`i>+*qnlV?{B4Y`*|mxq)v_+6ugV;raEg8fhx%(uT=A-Ob8X_yj;#OP;CH?>|_PMsN<6_!SH zbZ})ZM%q8YcCtYj)dEi{nGOsW8T6CBQW?hFs8u)Axt5db7_n!HE!Ggm*IQyQY{SR$ zt;Muye!wx&B2axY=PXCw{-7+?4sto`o-!R2Yd>)#>%Ap>I_plg5RWDm68yaUWf;j_ zkJoFnngO@*W#?V^wM3xOrjWBT2XXS7w3-He&x=cVb^iVc#&0$}Q0rktQ;*f%CMD?5 zP+)U&?-i&urthB#v*{72uA(~Ae_Op-caSG=~A$ zVJ}>U*_%QMIEf12A_0bfqHBIbA{VqRrW<>reiG@X0dLl!%OlVly<@yh8PUsX`(4Se zFyqIiZf}q2z_>WNY}xzf&iRbuxypwsu_!_~&QDt9`$<+oA;nbL>NbnMpAU0$XS@!P z^bMMD53ze^ud zR4_zvZFz)al*2EDfsJ4Dex0@=gnqQE`*&)ZN3`wBgm<{u>F)uHIZeH6_Q9r;MHVQ~ zgK_`hqj-GbZLuu)UDM*2)o6*+*#mzV<`Km*Q0ThtF9rrG7=e7hhflUHWK`sRWqW%y z)3Q=KOid-j%-Z^H7fn(WGv@KDR< zpZjjaUdkRZ+_bchcLa<(C){j_z?nR2G{s49?aG5t1NYp$xo}~Cz38^HBL93re^XgmBQ26ld#HT zk-5`!>$g)iQJ19E`la+tK@CueYt!e;v5UdE6v z#FkdJ<(cdUm_2|Ga~x!h&d2ChimNiW*qVo_K!@mjfj2^(u?J)Iu*p+kte@^oOB2ZX z;A`jKBTr%}S08HHGg0{<$K0CUccuN0s|Ax}KpD^)Lf}ip+!x=B17wM>>GlcJnF7jz zRnfe&_jddU(;b@Pd0h4jg(~&9*N214W~B^}e%kr3-2(1tHYnYuBr4E(XQR%IG_`2q zH(xJ(C61r$He!84%-=WjFyt2RVA6b>d*Cz67v;o9Q%7cHzgn_|^`JDXF_K~u_@kx` zj;jDiT)L3=%e+EKjV-yXm@ls&pCu#}iuH<`T!0o%1CR1LkXx>mmSYl`F*HnV9{m2% z{)VSbg1=r(s^r@y@Mp|z3wSe}$-5j)b2%fEic)mIswZevckV3S?^=STt9D9_E#TNd;aRY2p@1(-EIg(LfCIHPc_0`f%E&#I+54H4dI*s{Lnf(F#t*C z+acYEyYKqYJ}rbsYTRYW+D5t|!(qf-u6Z&`Hv?-j*y7@J!l#3jz3EVCJ$SaCf#j)V z!o@RPTTn&{D>WTMC)uRz4$H%s^hWjuuC4-_41{JOUEJ`j9-P<(;Nh~l@*NiBoDFeF9dBg?IEX$>UTIrp_#VY? zyBa6WRDk-$jN(e(bS=>ZOXSUfsWI<);A69^R)V?fcTGJ|5USx|(g{tk;I3S{>21(@ z@8lk&^9U?wvvvi-9Me@4h1p<$udMdeVYJweDSbF8J?b1NC0SXg7e>|V_wl+|wcR5m zXPwSkqvUe~RM_DOybC;gAU@P-4iet{(~48xfPq=!8BH_!2f!=}hI#j99f02a`5rZ; z+zg_QO&ZR82Oh#)A>GJpstnnbir}}4@ zixkSIN(``IOl!e;P_}POy745N=+9&Xn$JWTflo+ZUfmad;R~cO#fU6=1^6&Ln}+)W z1R!vM9R|dLkd)#*d5i)Sfaw!zz@M4tZ=I*|@Kx#n{B}aQF>;ti9p=S) z{X)FM<-P!0-_hsE#&-`%sPEjA-q!L^cfE}wy%-PEemL(V(Mw_-1wDu#14mOlo3rbv z>mspnSICu;lkw#iXz1|`D^xGmdv5J`T(vRCD7y8Kk#YFD%ThB8I|F(Y&&D1PV&(^v zS(b-Hx=S?$hOpj-ek6prJrKx|T0mGEj$RDZD})^-y+=QXU9r}i&hh^9e(NQ2-FD-% z-?JsHV(}mnpMIy?n&3r(u%g)X{oOB~wGI~Zx>!z3KGy{Bs>QH&B;&Jl9T)qvcG)61 z)%X_(UKGrYZKzizeFssO0ew#mKs}5MuPDZ73%2`I6oWi^DZ1=VwZmn~;WXKaj3f8$ zSI*KuT8%yPy@OTqdcN420RdC0be7yWMR9J_P?MMHhe@_MF(&)7(E;$}zO3S_Xo2%m zZyCHUQgZwB<2zq;mYAmB&XT;wl-%__GnY09@{h!Txi3TwlH2FIODTvZA}p{k0yx-T zXTUC3*HS0cQ)C7sT8m~I5$_6MnZW}g8IQ8QScOmKXw9p@G+=4VRY^+5})`ErBC8!PaOB^MntI?KzBT zo<-*OZ}*1GQF1p0ios_foX%ro?p*|ru9Cww{XnT?O^}iSpLfI6_uuyir|$w_)|LZE z-F8~Z4;Y`GyJg9f>;ky3-Xad;^L~F`r4dL=SMq$FsGTZ$($sp5BXNv4*xz2_U+${{ z3W%|^5o)0F0kYrwWc}Y`>^{sicQIJVlWw?P z-d7>h7>HjhIsCF{yrItxR1%}N)S^6qS{Q#TVf?&VH3z!EY{L~+TMY8rPRtwYI!OIg z5A5PYUwr>5ZXoK)aj4Ht(Dr7d93RsCLO{Mn{hJD}s;1bV0ixlfl_eBkMnp@#4&hPX z*RJA3DCQ92Cf^|pM`*P*(5p7)U4SQHtM+#Xpt_*9k*&BdTl(lYd3G^7u!o@YUzNqV zadX9H47L{5pSNr7B3XCNE(BV?nHw3`Tm>4iTZWNWzrE`MX~n)p>!ZdNG%o6(MN_}v zmddIcE_s$I)`$lOh77o>-uI0k!taAPZcgf`Cxy^emkkZeAdFI;hl<_dW9^Es5Vfn?vTps|p|33P>E-?s*)vl@Qq|_tAn*GF&cthx z?Bz!gzw@lN=k&Sb9q++o5me>EF|jCl+8H-oPhV6)*g6@`It()TmSnF;S<=6eO6RD|jVQ@u$dy_9<8WA@gL8G(zzZglEH91b#11&(v5cbNM=e#=di$jH z=9Z7KTE*Xe16%GB8@dG1x0X$y#brQZlLt0g9aU$Zaf9j?1GHnM?GXS=K(xPf1q$^A z>IGt*k1taC>j0W-&zTtcdOsD)KC(;zevRMqVmdO)9wE_mM0KZzm;9oz9A)?;Mj+jf zv(Ad-zM#9VC;WE>ZY5&XPS-I_P?z{44$>0o`)fwtlhcA_Vn=RmYDLZs6lqQdbIuiL z3z*jz`lNN6=AC@;qt$p8FiW6$%-!a+I_ABiifxG$e+?9B%!1R|trSS!g>drDj8j?r zYRtIQ1qcUSHRBk7g*Nib(Dk-hR9v%8qzaZ_Lk7c+Y_6L(6ga${Bsk9VUZ0`8B8@e! zx2&Y!5(--G`IdODD&fyS%J=X5709-ueBu+|ZD$I>QQIXbY2Z#ikDz>&)Nb4SMMEsZ zP+IhZ7gm>)n%BOKYL1~6N2nJLEE#wB2Z(T=Tk3})O-=-f^#xAldTt*oYROr^8;=zlLC%CvN}O#y$LL=AN< z0!vvxbiXS`+k#dhk4a2*K3~VGuCwEQnAEzxm#n8x6smdm+eu+cjZ2Gry4fWrHBZSy z)7rp+Ce2x}x^EV>qzrJ%%=2yxmY;IX4b)S!Rp6?o1eZUANcyb4a%IM#KX;j%;V2eOWlfyZ zGI?HOczXOt1k@bSxxFNEGYW_L<2?X+Yc`6?K<49kpM_#ph54 z-BL~he^1tmTdAn)vf%!n3=KD6trz6KJ9Yw#7@lol&|a1LiF@}O;J|wfqXx!IrRXA1 zzH3bMQ{PERi+u9fffVKj>e2u|x388#B+A)xkw&|_uRx&vH zP`)e(W;Pa4%k)d}vJyb^TY#8u{7DVN{h);ooY>z^Rlbt}3mg%z60HWn)U||oh+ZEp zwM7WD{lSq)T&1C=8t{t%q`MXoC}0hb{IC@grEEl$Pl)ag-*b)pXrNy+!m<3Mf!$>_ zk^v&BxfJONW?$E4Ep?G23nWd`Sq7?e1HRqz$9>DMm<;;0196F3c_81c@9L-Qvcr`9 zXW~1Mr?H3N#z#A`*cs3mc%%cEJDKjc-mN3OdIha&{U~V|w{0@eO#1ji3A+pS7l@=o zBdbw(AI&w%71--!(EF0)KwP-5nx5ZhdnkO3g6ZVBCz9UWQVU=Zuhgm1=$!7VIKfS1 za4CCYdQy?;bw!8XsoKXph0{|K@|ISrMB#QS=>NT$DfSv@jKknr?L&}jt~8G7Bc={Y zU0-Y@v1h)EA7P)oe_OG5;!opga$V_Pz|{p1*}5qq}xDARE?7)EhC-;_jaq=(Fv99Oc%86*P1& z&jaum*HP`0xpTy1Bmx6O`Gg_p6*a?k#NjJxEk|SgG>j7UiNoncDZTM|_U4llc%yOD zdNg;5zLj#$cPiUFEM5o~I2mzID3#psTLyj?jAny}PJ`XvmsTzc;MFMmLgxY6>eTo? zC=^P;V#JhI9T9-_H1|WxlOlPiRa;U(n%Y9>NxjL5Yx{Xk-?q;m$AoB zBWSPRYh>#(IF+qs+qCB4iMY zKS01;n;>H^Lz8*1YhYeQ5V~Bvp6gjat5f~fPkiuxzkeCBQ>E-Q;VAuh*5GOG9~U{D zdg)oqyQOMgNa@?Yk*6LFAkO6}cK{BSGY?VF$|&Uan?$Q}kzfRmI#H<)(vBIs+zD7C z8HiNnI$yPj7E=RBCAED|3-T3q|4T>#pU``M0Cgo=Z%i5ylpv3mck?4l^6c5)pudOG zjQE?6w70BI!}=-%TUy`**|1*u1X1ys1-?Nvd-3Q?2bZk~FdX>AC1-jy@0GN8ah>5b zMFyZ;yC*(cnuzC%&Ppnyq?D4y@Xz-iBYfz`w_bZwz2f0|0>5rJjH@zPwXzfTylZ$? zyV9jP5)Bb4IT6{n0byLX7n>p#`7Z& zua*kHYs?oWwZ^6wD1p1Zu<~^_pzZd7o1#TtIk zRk-BVbzRhqf^h1*ZweAb2qKM@fXMqnJ)K6@fwZh6_C%1@xK9EuIr*sfK>rX}kEyd8 zd=lDvw(DgaURni}KQz`}f=)_~))msGiE9I2=B`(e#$}&S?(*}TjJuLKY||`*xMfh0 zyZoZo^)QX%+aAn|z(CBD@d&9SZlUYpexkawe0Bn-miAhN)w^SFr*ee+jq?g`H$xeM zJPK?tM+XDS7N7|GbFM!Bki^n4XJtGeEWK>wF&-WL0`L%n*bBoMb;#p!P0A_ObP@xm zMyGCj(}!o218~db+{Wdd=O=1`jW}X*6!Z`i6&8bz^VNl$ZY$y5rG1_U?ipnDg8-$f zp;vlp0yW&!#a{r%xUU1T?Dde5RU%t{EC(ZQ8^XFobYP$+w5&ZgRg{(&(6|An0cLVr zxSkTGu>nZNP-{n+-CJ!KSs^4&z)Fss=>`Oj8%u|qdj@pc)1ZOuw*u9*2OgD0u4mCx3EK!-akC!<5 ze`r5R(Re}jqsgc4T#V1G`GZ*aw4AhmD zS{zd!vB_jGSxdomT8z=qQIXztoikWS48!;jcz>k(&gNW`A4v@%_=^u9Fc_<=Ogd~Ln?yM#miq-E3MJXr+g7eWH0?ySjI5)wa=^Reg7&P~3-T4@8oHSOlESQ+EOD9E=eO>%s#+3ZRZ_ zexcsoLkf->aqgIPF6TAh?zk14t(RQf(Rs$HPogQ?@Et&z1$t4;p|%0t8?n zWA*j;epodgt#HVN@%ehEt-Imz8F%`EpbiIAg@`zyex9Q>Ft7-R^xzMos&k7p*CANZ z3RqLl4|#`}G^bV?&!t_L7hZCw_eM))xalpS=KL$*MGF-1``Z&h7src+iW_!^jL2rn zn&%;D0^s%i_bl*c1lTzl1K0hz*FLx-4@6rQebp$hq-%@pE3z_KeU$c@ zZS*V2g_ZKQSN^3{s$yasjBGy5x=+ddD8CxrzQyvMcu7`uvTF{t>X&Y-8(hUW^bLiA z5s`HT&H~>L?F>d7y^5CSN8-f8jj*w(9`mi62D6(l3vmZbcMCC!cR1d<+M_X>)8Ol4 zNnv=U-Eg3X865)DWBUgEzH6BoqBt;*H25lAvK#+yzpu6A@|>a2<4X7g9!p~&{e|le z2#7VBEbLTlFU@`&glvGMc*~c*4%cCx1Y-_)hCko&Iqu{GCzf}ST1Z1!#Gw?-m zWITURX;N-Us1(?+iw7}A)z0Pvt4k_?%ekFTgdO7V<-{hUF<+N+VKFI5k(It`^NA_D zib4lCWQEZ|$XwNUD5R!+XDp^Ct^{n4=XjB&+|;38(t zNu5PLi#Zq^_bt+~NYYTdNi^`DK%F7dM9%;{gGT(tGElQQwcH*&!}e01;~D^Fn^%Tem}yJ%5t{zss|WK;mJPO)|ow*~O4hLAO0KRm{gcpOC}oe8d=lFxz42Z=&1 zR#uN#;GY;~I^O-rZ5x-x-6EUePM<3H5bxB}&kBxp-(n{N@mA==R0G*R-Wr(B>vaPm zglCxRt0V_QLJ3n-WK_ij{Q3f9%@rS$`O)6(l<9{imAEEM@@Feh0_*;fScL%vWxQ+| z0d2n_Rs^HtG~q_v%sjKGt_qxfZNbQw!NGqGavNPEOJGk7{`&I! zjZ!UQQ7ix`8PH5TfYuoADc}Np3iZN%Upz!E9)tXgmeL&9Zh`}vk|LfvueUxvDkpaJ zGqiy$orqY%%_En!yc5b_`=(hHBjT2ybHLT#ons1RbJ<#Rdz))^iXyc*^guV~86V6A zzEPLq%AHs`8TScXWo38tg*s?7+(4rx*J_goeKwiEerZ;yLW6LY9O(zkghQ-+!J^dp zK--Gr?dQWM*$;%a8yK{k1+7tj%#70bWIeyClWL4|s$Z7~Ot(!mGy*7xBgJubFz1t& z2M8tn6bCXq8V9h21)1gL9j?6BAHWp`^k-3~FT*54_|>3`W68(r6L?dF2Mj+*(W9c8e7CvvjvHP z2OEeX(>-GM+wX9&+uMIwx~s{N5!;tyD&+z$k|ai5@R?q7_yU+z0!uZx5MBwed^zk5 z+Lf%qZ#(*xVo>eYCzxkgF9a5Tj_i6wJXhX^ls~4ZBW%t+@P?5<8T>`TQ z^)w`7cV6I3kd3?>qUh@H-0XHFnbp|hh8|Y$&+-eBi2|rjoRwKc;ZmU<;@jLZ4I;Il zr%juaqs)uato2~U*+QMS-#0b`eY9piYQa^he;Iz7!l1qP!&-PR6O=q60O2F%!IUe` z9g%C>lr}&dne2C#{|XCFex=OCM-gA4WBNb6GQBW3WBFl}Yo~cB&M-*1HV^(J7OfC+tv_OR`(DatQn zwhS{7>u>_KQlWL^CLruvg6e|dm!cO2*|9lY6BL-g(@;1pl_IiuYoe-VFRf1__6q~* zmj=YV-HZ8j@tIFR{WFpmA-*U=9v>!g!YQ-%Qvt#v0`vzA911k*fmAhay8=@Ty5#e; z`xR&(A}aFurahXzeEAt>ejPmG-&AVhn%SeF|A5K?Ie0+@O`D0;_$s(~xop@5t&l>3 zAZ1*P zTK4XA+Oa|zz0aq5H}l38c9+9t%L?hM>^n2B1|Q+++k!9_r@>+(KV>VUZUCyGn(Q|6Qx;H>f9S zKcXoBS`T(_iR9Tye0z?z9?C&lD7ow}<)o zrf#xkiLj-HbY91%iZ8|A30j3)u~gMT-W9sY?+WC;G#>rFBZ{UC6ye10-wyff|O7v!cs>2;nsu~7qMDDeKdFKK7bT)Oqdlm0{ zdXepCl^!`kF3ut?BlbRd{F-*HCvN(283Q4{-3MT9f|wOra3yb1v!9DPspoY3dWP9Y zpt1+Vbn0pd)rhUO&x1*>uL0c-|Dtlqdf zQOQVy_|Mu(R{S-Zg|`7M1~nI1w5R%>`gFTrwQ$ClXGo7xAH8#Heoy;wlH)e<4yOV5 zxDbnet=|@N>6pM5pQFaJEf@?1o=-baL6A*|9Dqi{Z_!b58hvzv3*uSGi zRV4PBXTVYNciuPm8uEnfej+2T?>E@xYGIm$tQ+q^QqW{}DPJ!L(EhJ|%!(fe;!4&*xM1mBi7U(@RKU$b62G*>(MknV37*HID}69rWmK=Vc*Ehfs#(@rsngASRw4AHfBL)kvo=V|$K- ztW;f1%ip_72a@DzB%D+*H!>4_#^EOrKkvhE$^_B$WR)9TBb2=^;k*kJAbs z&3)h76o!g!5k7Cjz^J6axE62J%gUjpKUae2!y$skT}PG0dfrf+e=t0P@x)5&H28$y zv~&GHntqwzO;S27GZtFOEKyS%1Hs_{i02a=wAR0?An5W&yMcX~q+vN>DA3mox%}YA zMQ3;Yn*rzs|4|WQ{Wc|ei;36%_B%Cg=4OJTE)?I(yNux{U+5w9o9j%+Tz1l@iLLAYEgIBS(f)66c9G9Y~x^#brrLs$SFY zoO!{Z!Hgk{D&lm%v;~>==rxp6L3Jq?w zbKP60>BupM0~#w)t~#(~R?tQRs)n+GyPJiJR+fm z>XI_3Z7@5O-~%l?7JR4-K)@867*%q)=o ztj=iQEvY|ILBI7uxR`>IveZ!C;MVN+nZG8=N2~SE;11EA4h5=Fq z)6nCb!|~u~21R2oZ*<(6^}BCAMv?#-pJdlR)a!SzwAEv+a;L={<`lOnm8NcRYcuZ@ z=(8C~dKek3-S-;%9WjM1epI+_62_@P446KXS^1Lh8n!2hO85BT4s}=DFvhdy?N~&-~!1 z%iVj&v@A`8&m~FGZ`I2IbZsseOmhcK%6}uP>CZ* zzA-s>L?wVl`e){69~;swxuG4zAauXsQ~U2W=vk|?eKQ6UgsE=7Gc{oMuJ1ClNH zF~Qx#5U=m|{N7ZM*82ihj6N@ruz=ei^2j&fOJU75-nIBr88iB z6JDdhHLnLfp%i%os8R#F)JZaBQ>Ntoybz(|RxAO~BN?1_2wlrM0T1*mCPVQ1rsOm(gh=KDv2{&7*?tS6gA~x@3@p#gC=xdyc zi~O*C+kR^hg_ykr#r{Jf%=~SbPyi0(V0}KPb2S151~u}BG*=RGW6m{0B=;(kF$nT6;fB5Qb*@iG8}ehAkhiF zoHx))hh50?<05@`W|~0uAW2$W@Rj;|+BC3@-wPO9oaQDeA7@sb-1bE>G*|(Z3@T&9 zHAHW6JBS-ky?cWt!25pUE*e34fO?R1!@GaQl8ej6-?Qr(N|L9UV)b|ab&21HDnR2g zvS`sC847u6#Pn!g+R2-tqr2*h9rRlP4uY91lV&zx0g7)1yu2Y41rp(2ZgON~#vX1v zm1OST(nYZ{^bDAR{Dl6&W;ru;o4ZT$uZ+%5RBOeo=PK;Erm`=&}P z$>Qk-Ln&8igNZo!mu~d*AVJ|U%{1#ntoHf>F#EOCQU9juA&WIbiyjR&I4nR8V)4S= zI2I&YJn!2)t9aqOavB6ryt`IMFANbEvhLjo$ zh!g0)Q`V?RCVIEzD6qEcdBh52P?&lrNdx}>^RmYoT7?#5Xw;8@H3r=+}XRj63 zwidJ`7<--sPJ&V_`>AJsT@n-pyDod4#d<6QPFyDSAsTgPRJjC7Cp3ss=)p(5r~dZa zypWFB=rgr3(6m4nLo)zh;9n=lSwB#;CB#bW%-Q>miyg__=$FI|`ts!^HNS5Z-7L^i zWbI;~UD=o3NZxeWPP%@kuR6U#4Kq5Zz~U@)!-L%iM7dg*i{5@lSzuxV!B}HT6)wx?Q47qdWz(Toa57pk zC`$_EtFw+17wg};GuWb*QM!O+*TP^)N5S}1xC1HD}t{wtaH1>!Dkk_ zf+~lr`aM{{rzA7$-f#-h^w!)cTUr;bk?0Dc<4o3#3kfU1b2ZndvVO>?0&o+|@>ZdR zZ)BW(oy9?K$IM(24Bfh?6n5>$-aiu>q&lEyQ0JEY1Rs za#!K?9tnPqebawI$tRah_q7nxU;K*7BTy=8cg<< z>sbY=4WI49>`-5;-cn!x$I)3VI0{5j^n+O7w!{e@EP);F5*)sMs%JOaohDWFuAIPr zZE5D5noD%Oi}F?E*T?Y+g*9BtVl+np7{X;1)2)w^FF?1KYN!A)2!DYoSiZ>^x^d)j z4gNk|P=*6ibMzEtt8b3I7UPlO%Ww{E-i}HsbIo^bg~b^a7WN(Y6%&at*j(31uXn@NE#(i@eV$omGK9B~0IWVi`#^(#n zU;3pVj(P<>IFVQ!oqj1Y!1YxTmb!F0J*v5o@ik5Ix#rj{x2GWWhV1eMW>E@eYX-5R z)0LkqWiEiJER+fg+5+rV#E0si!;|}$1PsD-l_)T+wVk-Ol&bC)XvwF~-mp?fUA}|f zqcjN~b`p5(z5o&@;*x4--zr%LQEgz85+p8d2&YV3wjn1m549{=K^?}gh7v|@VGHf$ zMH|?bnZAtyCjl=px-EY~4+6g(8vGWc9FS_mFaZfI@E*}MZWghnpVCaN^&qeBc~}jv z(fI+8#@mMv`*BE%Mh?cdE?RYN$T6l8vIjl`CORM>1WL-}u~eEc;R=SE&}6r+ZAl2} zrx;M&S+-9jliMZS|AJr8ccbzQIk4NWC*F#8%Nm*Uf+05?BtorV=B>2L1$xTXJlcp2 zDw;@N@pIJ_YynoTP0i_LyBbW0dREFnKS7T_P*9N6Ot1}K5wvjt@=Yop$gn^r-)HG-Gj1LGY*{Rn0_%k=DN9HaR-Il+F2PQ?`k@=R#u?IT6} zd+P0Rj6V|NmwSyJ?u^Bm4W~nea|Z;b%eIs-_bv0LRF;QVBet%X47~j~Jq|#>pA*F9 zOS)ppb39MF>%?MKlncbGvdi&vD@3U|ujhr;ikMnUv$W(*m3Y(1;2#uY_3JN`fCvMg;rFVN{yibmZjFo=x}vl1RfFd+3kk|WDoE}YaYNbz@H8Z<`tZwFyk!>l^U zjhoJd>8paTon#9k+r1U`6DEo9NW!m^>||dhMotQj zXE^tHuCg9+y-JJH&*WgOn2%fP{03nOk+8+Yv`%OZ_&RTM*NXVxXSSUE+p^_S_fNe* z@fMMz=dG0~=WFL)oVvUY3q&9`1d?l!Tyet%oPqzgJz|X}fAw#bJC%)K$OISqorfb7 z6>VT5Y?}eZERxH6%Q3Ra7yX2A<_fYTQT%{Fst70QgTCGOJhOt7xltgKiBGcP4mOuGZtb51fK$g2(z#_Sz@ zCa0ccje*JqC^mz@=$!f?UUZxNe2F6cMhH5;j!3<_z$k<~EzsM{cYyr)$ z_?eAT=?EV%M;HnTpuwRK?)ky?6#H>OV`~tQ3O$DGfZ^;9QVG!c9 zIJDb9*QnR|iN@TD3=mk04%pbG;i227#`fSsSURtcCqm4E~RXn{R;5y!PaD z+i=Lr=r%+}>|CJLoARANJmINe8ocx9JDMZ|QwC4cFb9$g%eXMd?=mLYd?EA=^Xzpp zX}%W#z4xdvd{*A@qk$ikD&i=ZoU@91y_A7p}*YJKAqF7N|@AMH#6kO6@*<32LtJ-kb$t&@_)=ZJwDV!Z#ri`K)Nwr zR$SB+-t33-N>5hu2@&^YK$XcYKpFjLG8jHn1~5RGGQI9I+G(7C^<@Lxag+ns`(chV z1n+7&55alPYI;tmH%BceL^h$00|qjN=bsIQ&aO+;XMGEb@kj(L*m%Z*kYDu+9+CEZ zGYK-^Kn3pH=f@%1al}_G0f*hEI`R=u^%)nfoPt9maeU_pG#=<|J)>ovg4k-ujx;MY zip|ow;-}|h6f`7UBzgbcex;h0$8*eb=nR4pJLndW?Q3W&mBRM$lpy~#23EU>!!h+5 zfO*NQN9XgaTZHdBjI%wf_muU*KwE(L8Lk2I9V*#xjU>~LcSu;iS0t~TbqF`}Y6h+z z=T_{arx;tpQfTg;GV?bBQD5}z;eh{dvfoCWg7NQTj`f;%Mp~hTwKeN!V1Wj7JCVmV z_Fk8(?;oq^=(_IWuVU9)jBi*G4c`0 zxB?m5IlOa;cDxhvewjNM!N-4ksic6%yGM zpk6UV#u8qFWIICrQ=MS&&&l8$=LMRmmwet`bLY`PRvN$%OT?1ep*noIkfBZ zHa7B@=@D9;_1Uw%Wy7g>BR^lbK>BI4QkRO00Hn5YKm~V??ik##t$F`0q3WxETC902 z&}Z{Mhs)3-K|iYwa7Igpy8z%GhJeT@sAkuwyAnIPUQ`@h+D^UWKE!T(>l}Rov_qAs z{1g-j^vU1*Pm3Cygb#u*L|kjC^`}h80G-!;^hg2LLsb7gH0SZ-*&gQaII7d@{svf& zznxz;#^$~W^L^JZAKzmsWuf!YyYvn;JXB+_r4aUPw%dGMzTRjl(8Q>#Eup;!s^5Ax zT5LsQj5Ee3wEba6`7}lSrep+?F+Ii7*?{b}Qu;Vr0wx+Jrqa_AP-ZHMZDi0KJ4_7z zSF89Rai9tQ>riKkO2TDWH9$di#def6c@7-Kr0>(^$*mf6r{@(#flV$H^maR1e>4y^ zie^EcPHvCO#o_=N>E`W6_!b#CETT-KmT1I5I5O3=|0tu^bfA+b9EnUwFy z{XBqhzH_oqa=x2BgF?3o;%L{I$z~kB{%%wtlvmR0-=aPtce*%gEz}zFhTB`j)nC6G zev|R!m8%Qjs1nKwNDcDNF7OA?Y66~bMLX+f0_V8(TgL+rcW2vDb~p$S`cOlimTH}= z1IY)KEhe8#S?0h673(O9V4-RO4$oQ&Dk3=!fnHwmiMl3FMFiAp+w8ZG9cc={tijW0 zom)dAUZIOWiQQMX`6a^|WS3gncyn@uK7N{wEC|doMuN)hi zPWN7MP5C*fNr~_8cL}9#}y6wN3@;v+Y2%A;O)W{bXuJO zG%R7(u*O~(k)KrM(Ge&oKj>H>}YyzUABw}#TWj++!7w@AwI=F28w)sa89 zp&6`DKd`*Tmn+^K-&>#-&k#5Mkt?ktUk|?5eLuA@jz80Ht-e}JY z96;<7u5iD(=W=b1Ir@kL3be3UbX1x1JgfVnu%#Yg4DX6@s6Ha|JMB3zOO!0-oyzUT`R8Wk5=OM^%=y3V@2sn91Z2hGorw>+zzQsl?_i z?n{uVXUFa~qX$-xA#ZD<9J`@^PP|Y7z{;i0+XX?=K(cbzSsLM`fgTw(77HDQiAzc?xrSbzR1MIZCwLUu8iP`_IvmRHtiAL^*9UYLwYYC&A!zS6AJ0EvsW9x&gf^-$)&~v zoEv8&G!8KtVDZF0y@RHN_33j#3T*;u@`iwA9d={%ZikdRrs}+?NYePpdvyzOFRib| zn@UXJYH|gN)d^SCGZv~<{t;8|Ts?1K0^(!Sh2wIPtAls9>37{xEvc2xiR6Bk9A)?738`#~avCuoR&kNCVqK zQEq-;vx)?p15LJ0l<)iKA^`|TaG{UPVS1@b zH<;9PsMTtuoWCz3ZPd!Y7ygC=W;g37#I8Gz9(JF-QK~faS0##OimIyFO>5tmDxM(M z?HZsGe|Xjj)g{l}b8ma}$flAYKUxgjq&5c~c&?)^1#yHgFTeb7UOova^aK3Lg}1Nh z04rqaJbTmi8tx;vh|~vf%ey&5t-S6m674#Phu7~FiBm5PxAxT#5pF2sHM>a=(g}@H zX#8mD&!-q9bm>m97`?3Ffu4T|N&@Gs+cW;&vd>1o{bGA-pt~F16%rXO%+4%mgqF>U zoOYUBX9QGpb+@z(F#sR6Z#$uaRSt4Wu4mMRo0mdjuy+D`7xO0$fGgUi04h|rZ*>(M zfaUN{dAF-Q&PLvr_$$?i?nlI#=73?C;3W&wWjC(M@y#26WzKb^^yr2G-cv{Po01*h zTmjt+bW<0-w3mCQA3)E|?{9>#)hY^T43F{rm6T|8>v}EYzVi1hHm-(w1MNpcqW}-G z_T{wh+)dHzsT`Ycef$9p{GN-+jdQQB=cMSP@DA1jq4UAnkN_x_IX8})0h^My_FA&i z#Eg`RX;7C8FP{gi@wwXM0sIfM#$uPW!*Y>G4`^RU@I{yI(F9=?nCybQyH7gE&L{M1qsGAu8v*25iyMs7enL)Y6?lQfU zfPF7#r^WIWm;1V`zX#FoVvqw^PWIE==@@zfy3s+i(;Nu=+j^K|uJzTP6rqZ?aDdf^Yv(VM}b_W;cN>}a8)(o67tW@6e-h-CLS=ziL1he}c1 zarY2`EYlzpwJd<#fx@)=oe6*S`GsV`7ZkK93ApWyq%rI>MPARsoID=R%Xkfo1GMLM zUf^q1R;5B_&Pf4jj`V<<2OjNXu!O%Ws(d(Sn@rYO^1S2nTuGSa8G?XAo05IC#erLh zR@6-pm*Xw??*?|4Oj5)`6E6DD*l6BYp2fHY;39OoVF!j)90IH&c{}H=PMQ(1L%(2= z?Ud?#{boq>btcYx>bkDA6-oXp_%GV*}3XlKLCmO=vI3{X~AO@NE@rwn6XhT@mDYuEyGb2^G z2J|!25j0n0Rs^b)0OM|P_lr{i@A@ecLQv|ojVRIAb_CcY81Eq(UiRyp7oqYb<`XdB z3;}|G*k2j+9hA>GE%-eV>la_WZTS0yH5n{-?G#>fTXD7lX}A7DGPBj+o%~Gk1FuB{;NB4iNz?%|$~z65%~s7q z(P$61Aj`^GNGWU>;t0u~2zhgmx8#3$+($d@S1eH^1;Z^@{FusT;P8b~cc;lZKx}Po zYbxgHd}u%_JsIP@UWLZ_nbwdaP5@jxStsctW*}~DILlzCR#9pe=IYck+R%Ow#Ywz#&DLOV29qO!6^`+W!*c@ZJRqgT?*=@X-^pmuT91&89Q}zIDl7h*vsLcG;gq=9Rj^2s zA_zEhYgoSznrY`vd3HT`-1RYl4vX540h*C82eV9SD!b{V7vq5Ii|AzpQYOy@0WweQ z?>>TY4HT4JZL7~bO{f;lAMEv0t(G0%qSwmf+sUilomXxNXWbvNHc#W~{Ug)VE>ZSm zTKmb;SI3Q~mea{kZkn`bp#FC<+;96XK`chs4i&5Pjd>YF81W6`9Ory=6y6IEiKRC{ zdGc)l1!@2qaorFrYMDhM|GVGl2ncm2cr(r9wbSr!wP+Am1gg*DCMtkKy@|nEFC)$& zTtukt3(g)m7hQ0?qj^p2(wic?k90Dg7;RYz7itJjuG+-P5?>z_Ecg)Uu9^F>9F7X8 znC2xY91Y^VH&A(q&n@{RN!k09Qil>UH~gk86alVH`g&!&FKjMIIW}J(f%fo;mk#N@Xjq}S#=vsn zoJVgYv7BuYoH%E-_GxR(ItO89p@Rm|2ff*E|Ay}?ztI`A7bv1j`kQlG55W-&*+;3a8tSkV2VE>Fxt}o1qV#0} zFqFwoX5P!o!O0i`Yz@xnBRuvT{mWUf$swzjt3AyL{PUS$*96|lCv5jP$}UM17lt+l zY`+c4T+kmLR`9xni8ep+Jv&aC1(!=fyq;g{=*^2|f3We1)b@>8yG_?d66c1auHjSE zfce&ORlH=}g`~agi=U;NH%L{S1OgqGrijwYBj43q$v8kwb{tyIhAck7 zfF-sbAp7Lzi;>HU=D+R4kztC!lP-n|?bT+pBjEd)n=8(lOr@X!qqKyH zb|`2TX5?3Bjq?sJfO`S->f1%lYS1gcM0A@uT4MN91&WMZ?M2~OfuD7HXO(fqWa)=w zS(fLm3o@-=M*m*Ri8-aqMa@Lxh6UQM3Jtv)>J| zBRp**4iu9vlv^+6?&x#^qUfOz=v z^NZApe3!7mR9I>s$Ro%UK-|1ar5=LA`6#yRQUP6mAPLnHiXUy?{f6#f}GB zX37-F5#3d`*lp<*bOv{%AlisSzlCn;SL@zN6>IvoG)2I{r5&B^NU$Wc@B>ms)G0VO zu^B(=zJ;dS_39frb}U98m)^EWMtjWIuhVjg5$`eojTrV#R=eOIh?|aFJU}J+|7d@etA%H@IT<5Xu z6tHP_;oh%coc2Z8WN?ayH`rTb-e+ofBGZtTBQt(Sww)1e`th&WBtCsACr|HJxAXEe z#ux9}h-S&|cui-~MtD7_2z4(DU0M_|LhP3aFYvaS=P$rm+V7)*IUIH$fF%jXySOR9 z8#wjD%tAaQ6t3FQ094Fs$EYC<^*gQPVOCsiUk5jVEe-M%gl~?Ap*8nKOtCmZ;w#|q z6##LdNO(Z&JRUdePrhP}gNhpu+|mnPDbV9sg=(N6YR-2rm4`bvfrUqg%oc%&lwL0o z`^03*(O^q$dXXc>GtJ-B8xcv1=j-p8*frffocB_X)N^k~d(k+)?OsjO^xngoW(mcD z!}bAXG)^8? zJzK-1Rm4@i6dWKij@$uGuhXpFO`o@!gL3cKC6`GD$))_;eO>DW`5oK;9*|AOzm>Gu z*Msb0lTpPY`J;ZORynbMW9#y-D#O; zD}kW3?&2gxN}q2^0h59`mNW#J+Qerg^4%TJlS!|m;4>bg6&*?MAvlhE)xcTqY1_j* zDk5#A+!dG1jk zfRQ-A<60i3LdA~NF8R99AntOEh!@5l{)|=k_TGl@ed~Mb%V6%s%&Tay{5PAPmY}42 z6LprWSbrpdX$O@9ILNe~Mz;lp{3n?w0wFIe@w4N+;T>f1!wr%p)zf}&8yrPnuarLE zt=Tep-k@JG4L7Lvk@E~&O$SBvV0Q{ zLmDAYW4K|n1>BheMx3}=MF{k@gBjRN9V|8VvOudF!$pwu{o`?5EKsGIAON5PgiCym8 zMb~{?V`Rl4FLbhy^!ZnkE&ZSPkG|0ZimprW@i#3JQx*jo2#~4eq`o7_Lx@nZz1z>C z_w~p~KLVl`i!C>OQ?@3GYya-Nrgf$qOSIlqDh$9`3D!_{Xkc!~V3SsOI4*VWtCxeJ zk-P=XnFGzL?)Bd+T6cL$;HU5SA2v$4YzJEQ`VJPq{m zWi6KSr0w!6OWD)(Gc_HY*o!G7V_pD2Q?s~eKK;%mv^X(cKJb8oX+_32$={#e05H4L z4E$0Hp}je2^^;Yyt|II2?z)!=I(LLLJl7tKj=!`n5>IBwY2pn#q0L`EML(&E3Pwz~ zI{r(fIF3hP%-dVZdu=C#%6MMBLD*}}#l)r90JofaOrZ4}a7#fIhU3!nr?+v5!c&2n zL=%F8=sCb9M}2cj*pUDv0qhKs6v_`S-Z zx~HwP1{iAD11UTUwV&p5+Tc>`MBz%pl0312+-3Owc_x=h1 z4Bi?{ELRxRU$Hd>^3WX$X)H^p>i!rjb_3OF&^CYxi&h;GiO)pt_expTAf0!GZc=BI zE%X}OM4#wpf02OcaM8xEF!1yD$MD*Q!IUhAD4s24^3f^(9-?x*jR425-OZ~UUr~O3 zUvl~u>>az5_1XuWv=Zf7u7ZG-;#^}jzVPAZtc*{+nwR6c==6Ku@f7Y}2y3^^&(GFa=83*)%#J2k zfMMyDxAPYE6J;a=fS`7~8xe7_q-rM_G-y<9NDYoE!^yxl>2W zgQJeTtWPflOJsz9R@kNWqrX-B!5g|A&p3Na4>prH-jl=D88?XNMV@zFfG&pb?**%1 z8x>Q5N`K|wG(37d#EuT^`pH>ShxX(e1}y) zGs4v-1jhC*H^}yo;9FtX!o5X;P$DsiWdMDB2nU48GoyF_$s47bQbg|w>MB@QH%aw) z8+8dOMn08Ynk>oM9drZ{JKu7f*6SC_{|4bP+ogD?u;Pa1ynPb&oFuDA^ei0*O5?Fg z6b$6mT=oJ?FQ+Z)djWLL8X*$l!zs=(+;Yas{(zbo@U>fPbq>v1X%!d@xlW2V2^_MDAS@IhQF%YyME8m zC*1rX=8>c#FOKu}?d@Ti6V4Ro$;9G@>Tyonad011P>yX_O^nA(^w2ebe{bL8Ed&C=s7t7x|Tuf zI;&5#`I8FLSyeze$*rt}=0 z2KI~nl@{e*hb*tY3;16n;I#F~tm%|C|IDRlc!WKwjCDBV%?c@sRX#WpHtY8J^|J1K zKp)Cs-w0FkWA5YWbDUn5bZd74=BTIqu<43?dLbCMrm+hSoY74}6}mfviHfb(z+S0< z8Cg9lS-$Px{f^m_ZL0&fn3NNUR3}Oh8vWfTgL-}1h+RQ(=RA81veUbgvW3DWd_X~> zu%d>CIzbunP`X)_Io71M?pij;S%Sd}!lRE9AxIa8StB59;6@FX;$+}KD{N9HmaS$Lv zK1V)(juq-Q-O8$%Nyj|{!?=~8dP))Z2S~aXhqa`7VdN+>uCNk<0&qARzzKWafNWvC zvqy77Pf3G3p)@i3rt|~x9RU{n*$IlXDsJn3fLw)czI}lpT3L$#7p#PUsYdb31bYL` z7X38vWkB*^2|BG=UFH>&eaCud0I&;YfxsNR*WPpuoHg_WmluNLqu0VKV!=5tauo-| zz}QNb#=)gb$>0wx39<-NR)N2nuaIK)lqp0Qzr^-IMg}%Y)2)Du%OT4c7bXR;475Kr zF5qR!@@{rKS@e8>EKp$pVhGOY=3}-#3Wu=L2X}6QGx;SY8w*3-Z*6rA^$2y?-tE`9 zXN$MB9>26x9AcEmV3?Po%sMuq;!qc4%N%XQ+km9#jG@@bwKpA%@69YP?t8_z54j)T zt3s4sm3CfxGiX`R;twK8c`FPKhRB`8m&q{D52M^41+nh79Z|{{q9C#j9?i>g_$Ju@gRo(w%F{9l`WyaoanZKHfv`eO>(;qgDo8b zHo5Wul^>gEKT&Ei^f)M2vq38hNx#q12KQbCnIZ2eUxkkhsX$J3`raN81?f0=EtP7C zGVM4tuk+HBU^7?B*CS1MiM?%jQQL#0L{EC=`)hHCMu9A$gl|pU!}JKn)@iMBSN*vY z$1ZbFn6d0l{E3;47hh6O77&`gb7XtJa^DhsIA$x0q|@8-Sr^5f^9PO9BILQkZoPky zNU3CNokf)Q#b@Ue;FhMUS?hW#_@>1yALA=Kz6NdBhxPa#Vkv=K zco*yWKc-o;1XpG3gS^l$X(rCcnGU%VCqH*al&O7oyrChkH`+tQvFiN`MiNe@%H4CG$0B)ZLi~eL#;OJ7v;^JM@3TcW(Kkw z$W0rY1qV-QE3-ymln)X?Z3*9rz-GYVa05pgiJ(x227Kg#@OV&n4%+mv(%uCpB8Y~i701rxpM%i{vka`?#FFnPKrz2E|d3xmLU!_gBXh|2azvP)CACv5U_IB4T$o z1z5QtE{WgoFPUsYhO^%a3Qhc+` zxH5|8buztxMG8T;(tuvitFB)a6!p6jpA^?9T*6~DL)t_1IgBcck7ouAQSf|rGj`jG z$!D_uj!F4(8-V4`J;;F4-_0!IWg0Z2u>s47PYykmz$KN;5W<`Dlzjy4@{bx8Q@=rA z7(Epz2w}r{(?9zl2?K#oDF3zM49_jYZTM3w%j5RR#4%3hIJz7omm7%e^+5MHmSM+D zQpde)cGM~ksofoEZqMv>uD-BP!8S4oPSbp14H|wvqqT5fM#t!MUGY&=y5cqG6 z0OnC7^b3UJu`g9P!(x(kA3O<;T=&s4VoF&iv@AQXq*5k zM9qF|H_d&&?ZyF$$YBSwGheTO%YE_us0ct$)PyQyOHwTN_IE>;By~NwuS#EhILm~R zt;776+n@LHQrqfm0_R#P134IFCn^_2e+@re{971bCt|~}Kqt!Z3sGp~ zKX#*Wnl3wMfua~l5tc`hQ#SST z+Zxme@aFE-G?NMbR%0#LCYN^C2>-4-(U9=-w(bbqdAZ-85}>VuWRiURD%Uz47uq(smgaAr(9Ls8QU7x zc8jsP?r(-%3N&K^gp&nrPjQ)lN==0K7~GpZXG<@b(hPV;+pK>3uQXdf^&5CDSA(q`#-}si>GT({ zL5^2&inLW=_;W5FTo1x-Q5X86wrD%5PkQ4w3=3{~sxV71>k~lW7y0S>!nRpBtMjb+R-|7dbTAEhq2YnXMWJ|p;>MyHV0Gf1!^HzEo+DGOVF|VeFta*ytLELdc+CR%C zs>(&A?)rhsyaDBPzT%6>#UBKg&QG%j95&3moJ84B^a=)XPgEBj*bZ-_;kF-Nzwiv- z#)XZui5k9J0s79o)y{7f8eSBL%ie${{0jv3`r+ruZ=zq){rs;{d-IJJje^iDVElw+ zCP#vsUpyP-QEtCX5tOn0jQTUcZf?$Lh_oN_xU=1hUEF!A- z91Dti+tI*w#1ALvW}G?`m;Ttus2V`5W`h}OmQiNwY4sZF#(y=n`3-S#s6U(3bB?kt zz`&3^?0}vlQ)c&9IsaHXizP?FD2zT33*45$ArK_^j^OV4^z(Ps?53)_dI*6l-#HsgZjdLuP#HBdE+BGcOyqzu6E8QrR70UIe#9VGE~68n;hkeL1+@ORTp?*Jb+ z?+tt(WS+6lvNScngd-p+(^WKVb4YGIVy5sy8wP^+^MlYrt;eM8E}nQRR$E<5azbTc zS)eg(&EsA)i#{S3U@o&aTqv-9VDMQpix5@Rk?A{y`L5H)`85|Xg7YkULn*r9Q4Myn zF))r+K~{nQ&eKb|=IlP+Eo*vE0fj(Y9ZG=9G%uLH!6qvO$5s78L_G2+Gv)ovuWnaa z6!2x404vc(F|{UW7yEpM4|Z`!olR)T@gUU4pLD*tr1Zykj6S!f+;44fO7R_Nz1Y`@X7QJLakDbB1iQy=Re6uVw^f@Ic+!s1>ZeHyc` zafNOSh6xhg6amfg+wB(JCIk=~TF9T_fm2;LwFzj@gp+U;c1hte87cg#57?lILid2d zZlG%?M%pdFF7yIj7bwV@XJZjQ?YuonZAK%=cu}ca%0904`*0*5nbP$=)Y8CU_nc{7 z8o!69h94E%4iX+wvmW>K2|;mq$}QF-XpB!IWaMx}BpEYW6c|u+A{h=rg*<|=9#!E! z$Wd5Z5BmF1J^-lkKVZvlk}h^A!3Jhl0%ljSyn;65$DDO6rX6m<3n&G2Nd_PzgBe=N zsG-B5Uq&;+N=Ao2mpg4s?X*Stq;`I4xR_IneV~SBs0})mblOU`8 zM;lt(hl`gn1t5p;PZvFR@-rBC@yYaDvEzOh8Dj_BnY(W4NFHU`MdPU!|8kZOZlg=x zWVy=MMPH?Q*b^-x2-O7twnV0d2~B{0dEy|)Q;wd$-QFmme14;!Uyz?FRzS3>62Lgz zxxIiq2BovNr(Anc%Y)-j^RHRk+Lb4t=0vAo+wMke`fI2Su*tXug4T-1;~EF?Z93#B zU&L*$X^w;`F9F;+4i9{?Z|Cr~S}L-h^^oNP(+lx*PjNx0IXWNE2%8gOiTP5M?Ykd0 zwU4kW(M1jMgA*VcF4u`u9!E&17eXSp5H!dadjYJ`Yj+x(w~!nk28QtSv0H9B`tOIo zMk~3w;A%~S1qEee|A=wb_2d)@`PJ@DU!c?wY-<6-uQJae`?9P))E)^&!p8yB*sd$D zw_kgV6*Cu4aLc62@GbOP{q*kg0xB^Ogi~d6N%uo)zoF}^A3L~TmlU^ewf-VL*o?6R z@XIb)C~Bh)B#XfplgVgfUAz184n-o)fVejf0*RECH^o(J5msPvr<)yO|?G{9Z&&=noGJA@4?$^v2zr(nz)z=g4^W@6DMrw!p%2S0aerBHa9qvz7KhM*Q6 zkdZ`?&2Xxk2=C7onrYCJB8x{e61}#2>Sj9*rkA&aaY&>+H85w%X-{0Dhpu&(JZU+C zz70y535a-}b=3!^SX^eh-HmIWuvCL;`mnih1as~ecs2e#Q;KPv^{~7wFAKrqc>^OG z&C%U!e!d=|#4tnXqwB1wQ8UOW7s_`U3P)qO&3?I?FL3W9{}AEV3I=Ypz^xN3LKVo&HO z>udNe^Sp^C4KT+m1yezrfcSqto)1@}iO0Q^bK?`b#e6`=)pt9CarrkQpDO%flKtit zV$i;xl#NBUYrkS*V;EjZ0?{0Rtb5^C&k%`hSCS#bF+uK-SO*gXz+t1MG%UYO4&)s< zSxoXcF2*JuwfZFUI%N$G0J$l-(&%;A2itv2jajzwGCkTo86L=B%G{`7A(b$tI8Zk3 zN^#)kYy@tFKBn`xsJ!8TQH68Ft43IxY|ot}1PLzty*@Cm`zt3Si51@)}c40?nwcypI<-Tw^omf1bmF+ZrZ6|J=ol$PSct<^u`loT}ek4{0J$-(Qvw> z6eNEcupvOg8~bg$fubZWWV^8b8kQONi^|bYZc$wa3n)fYpS86)nCJipx4@mnV*^as z7L5Bkt^hJYzupuC)yJCWrGVohQ_usxM%hIXCzcr2Tc=^<^}r9wyB9&ig^jX-oU4q| zp(xhJ@tp!!BmOdv99i1CIzthuJpkjKh(#j=v+df|A=gwYvrN}EwMorE$3kP}^)-Gl zCEU~H{WN)MBBYcf0IHimeOamGt(Zbl2rw5XcL=uUVGS9^wN913pbm@O6RyOR;Jo;P zK!|C#^9j|RL3G%o!CoW`IQ1hC-NqGYnJXDcU!8kW))&nN0%$Wh@IPy>^MD28&Jvta zZ%|*g3&U_#`b@Q%glgD(-;RA}w*<>Xgd+l9+1u)MbG73OmFn;avaL+`0BF*F-+jO< zCDQJ4^tk(0YNJGUy_gg4>(*VOPw{!nIA8`ue5~AH?MgXKGww6`G6o<~p)T{jA%XQD z9>PF5Sr`UTQA$rauEhSk$M;o~Gw@n51-NHsWYO?Nw}RZJYy;3O*gV^JNz=Kj8}inj!RMgYacOHy(5Xqco_WE=YfG$XBc5^rW>3&;pGcd&n7 zBY=tFX}%WyCH|`47u%@yRY35k37hliVPT+CZ~m#5bzt!V2CRUhd8Q4z-AqBg^Cml# zRKB4MiHcZn3pjqr6~7=*4x{?bl;axy2vv64*p!yAE)9@idukX|%P)%!oO2EysMoOa zteDb9fcET&TL%fSITz%u&$(f+O`+|dBt3jWn9ZKg%|(l8cT?1 z4|nf$sh(gye?U5J{P7ahp+>_=xf&Qvtm5Io|3sYD(Ku!?PnE;KZD@1v(IRlc4~ZuO z7Y+0K(PM7nu@6U>Z~Q}WkniwPK_#1W0QyqWNM-C(O0i4*QVsL z5uoIKNyK^TkFgAED_hE^)~nYSM>{j=#=gsAU! zg7(mYPflo3EV2p=clj99=U+a~U75wI(Q`?)4f`s+^U}q+q>$TEHZ@tLwI{dJ@8x2& z0G0TC81K||pzcFMl`d{d8p$cuIjKkZTV5U*^j;wW8GVaB-UM0LVUE$HUffI&`Iei? z^Rnow6x)-0&HY*PI=_^vFmA82ju6El6J^4{){6k!yaf_(GByg?7g(_S-K^B9>N|Yh z$+q?ymAbIbM3ttX*{P=Bt%`e<%o%cEc6@t3PGw-=Dl7DzyjR8Dpakru&y@BhD4u=` z_6YL{k0%UVl@lk?`H%{wOJrOXbmzEZ#%~8HJQzCk$#u?ge-jP(7g!gK_f=`N6n&iU zux`IHgv~dPN^yEQoaYIH-rZbV2Md!wLUxXsrgk>)H+RaL4UUMVmop0wi5Xz)qlF;@ zbJNe@l*L9e62Gf+?SUh}rr4%-d0KOs_TbVb%;lPwo``5mfNT-JGuR&dOr@!+CekA& zFQ|!m07%@nsDZ!8$8r#MB5n>@1D%lp13VBGUV-_`ARqTvVebeRH8gA#qt9tUAHF%0 zi??oy@SfXn43ui#^K13YBeT&(q@%#kfMg>Hh{XxwJ_=i&C=M_kMUV!vDP)S@O-tem+$tYd*?}RIe&mOK)h)#aGLG{>j<5KXC*X@xvPybUhdV~kKou)$ydG&pr zmz+T2RhtbIPXAJkRf0<{D3Bwl3x0KnP2CQ|_C&(#37bV=@@Irpl-X?} zmuLlFMNs!|4?s}1zAW=Gm5--cPH#+C`k}DE;wF#c;$HFH$CbDSiW}`_M*Vt~hR7OZ z?7!oi$AI@FC!B8gJ#ZwT-}iN!X#odN5F9g{D(A}If@4K~YRkfH26?drw4l;~;{7a` z7ZQ-iB(D(&CA4ufrc>IZAi9cJXv7w(fNS{4p8Cf(a-PbFsC|ZXYoY9Cgsu&f%SJpO}vau0envlb7*B~6=wEaWRr^Z|tb@5xgi^t$#FhF+B~ z`QZrMrSuD^Lnial^tCtBvvmi|M%^v}zJJ_zXShCX1`7VszYmW!R(LDsar(o}+z;7* zf3N-!o#u0p*cU#K4y-d%lxv>mVj3(3oYapW8FoU>sPA9a^3M*jDNR>=e@@fS>2*U( z*DsSrbWbZZhQa~?usuK`Y5fzmM?6&~)6*Yq0JMhsPgu1;Y{PoH%@Hs<=5AkMAtIZ+ z_EJjdQ#S?-PKd+K$s!})r@7%>Vw?>VYKCpBTB zPWy7>c>@0qB%9LPSNby8 z;735;LE-y(`*hlyvpX;Yu~;YL((!>WQZn#yc`X+H(%)bzeBt2Qna(o?OrzAD(5n;a z%~J9Kq8tlXKNDSn`mkJHh=-?Fu(;dk^A04Q)dVh!%>Ifqj`zWAMijP^iJi_HxEV~S zk#B<5@-MQ{sctyIQK+8`{@EWe8o=VC#()wogxmpkQ=1C~lGL<=m#W3@yv!W=i4qc5 zR~d!E6ouXN8V-~Pknl0v?W|7d5VH&$YwqJdK&>fK28lpS9+}xp;l(8!CX1Uno7iKSqRql==VUE^bX--gkE z>rtZK?dCzIWPU3ErSAm+H)i*%m2AHy(MX+FuEvVZr5h!8c`#64QW(kCA+X8h>u$<` z9P2wtWfit%mLDfS?p)~{ulUr`vc+UOc%!VQHvBjWwG>iLKbaHl&Y6DLrkTI8FYFDo z(yN-Ne@Le_YW|~rh-}WM56nGfVhBeFib*)-m!vBho-8jT&J%ywH92U%RycjU=9~Xs zjbq0E<{df?w6Ii{6|Um^Aqk#RIX;euPWcYbD4e7sh|l=~3<-pA4WJiqe)o=UAr9Ns z%f$IBf!36&PjrN2ANF&a^&kK^U2*CK8e{|+K7pgsQz<+h)t_~?zk9}wry`bf*05+YdRxhB4r5@;x3)9_O=9s50vu$ zs<~U}-ar#m6@T!vv?NvQA&YZA)5BWY2yU_khq&$DfwzV*03RIwE_50R_B@1v$`m<{ zh#JRX82`d$8fT%;MX~_+5=7pHOs+nEdmxF@uP*~ZjhPiW=O31}Gj9nwRHPUw9m%VE*z&04Yz@`IgU_9a0RX@!B1zTvRq_$cP>Dz@X2xOwCW<`#Swz8`&{KUSThN zI5lhZe$((4Yo3wqS;+t=CCI3v$-yN`D@Mv6cjFH>eRT*CM&!;G=bAe}!m&&^S824c zR0eOatQvVUuAw2F*e*WiE}gYM|dp8XQD+b&?Ac)<$rxLzN3Toz0<&G zhm>SILBh3Uo1IB@SwtSnLEbEwAkbCi*ZmQKp*Sk15XVS1 zA91apb6;r1?lnLu(Rmhw6$g9st>ET+MI~pqA?T5=DrjPaJzX2$z93uKuxQJ7e!|<5 ze_p%FA7H<6$}!aX`!tdCH@?Q%uR@b=zwlZxRg$*ScF5-CNk?#GQOZn4IM3~9>W?f4wmIWOJh+u+r<{${vI6yPKckWfgb(x zjEDbd)CKIVEChZnjc}d+4ZttW5<3A&&xEgtAA69IEI`b9exz*Z39vWeF_O3wZK6>3 zF(oIuAlk9gk-*JAznO0ydwXDO@YJ#&7_d5!4Tn<<{t}3g`DopD`w3wy#vyB2WEk+) z2a=bwcHt>6@R5XpMjPYS!thK9#+E_JjJkc~>BUKpbqXa_F<{Y?J7r9FP9}i|6{4rW z;dglCukowkH{_y%2m&7m_#^9cnTkN#)1F^&PC;iju-7s~S?~MUZr$n*!!7D`;{7zL z+Z+~%`XD|HFjnfC`v*)E6^DQTa-Br^Hc*F_K&g-0*ir54%@rlj#CQ>J7%D@*CpE$D z{d!&DjScBb36M;k9_4)e!d{w&tzaOu?x3D-J2dICEq9LZ%RIZ|dBrICueyWH1NU+8 zZu9f%#Z+yY{!8K=#p)ES__*(Hp7dje1;YH?cux2oL!QzpG4VSFF0BIU?N9{}z;K{+ z^kS(na&w5IOmHjE=mOAyzY|p$iHOnlPNjjHQ6{C;v@r)CQzU)v+$O`iVZ|O2_&RNs z*j$k|5Vyl$os&<`U3@1vd!-$=>xc<0pmvT3afo4b7lPF3GQic z=K>qq-_RWW*rj$`t%t`^{oCf&2=LE9LfA~n*pme9D4$S2UUh9;3 zai0=Dj@6Ut}IKy!+7ft4(O=a@w5xtlu(UOQ&qo)qkUS z9tnNT{ym*U)$$7fVS)J6C*uE{pK#HRxwvYRZaJea4C#$2vx@EAb7oJ`FMe_gX$YIa zi&s?7sC68|Z%WYeNIoQpV&y=$VDOfK`e};HYXTNWY_o0Le`k&GUKWx=6EO}E&{OTx zk+}+5-x)hx~c{Sd)Bzk=V(qJL%CGEG`LAcrAiuu)xqNX+!>`GZDr$cCU-A%euMw>av%nPYjwuX_gHpXwV)0G zmv!;?Npv)a>GUshc!@jzefyt4cvTGu^$NRV3x(Ce;9IMGgB~-!t{Yu?BaLKpADnSV zeSiEL*G@0M3J&${Ov0ce^qcgDv#m5SIZz8pu6YHYylaE`zDB^#sDdg!$gPgTSO>IN zY^#%eh~NVp!M|_xoF4D*iTjb;-OOy$#%9;7$?x%?K2BIlPcUZ*dRJ=K1Gv{*fb=b{ zKk_40AB*VBEEsnU{3p}~s3yNpPiG5~n*$63oxI2M`kRAAX!3mrjn-UUZS26vod4E0 zt;~&OBJQ9{hI+i4==FLdrY;SCj@|^`SWQx*foB@3dQrwZA|FJZN@R#G>wSWO7uTV~ z3V4n;g*XLnQiFLduZB+qL)UOm3a5z@2?o6xT@6@FCWNY?Q@q;o-bQKP3gBM?wMs{X zPwR{b%Ao=RY=I{sQebt0WMFmd5A{K75H)o356RkTl+G7V?gmT5Kc5SH(%itL4m>Z@ zQe;{x%J=v412$vbZO49E(>RNVX`%*_(s%Sr236+Y&hI`%8RQ3-7EISC7;UK##z0O? z_1=aeFU}&Sl8&H~AVhctKP-WfehmQ&K_tJZlH55b(-SPB>xIJ=txo33uc+BqIr#py z;YgdE$D%8(>o)qS--Ey5#Je0-dI$4_o+0kc0Ndwh>dqkG345G7y+OGAz9Fcr@PkeT zIso1rP|_X2%JB0J-sNJP64Xo1u@e1ZTL*}khhyncSOyCWtQ7b|d31x{^>Gp{3pLih z@6Lh)SVQ*fdlyUXLL2cX+feBvoJ6v(Hf)%?T#`x^VvpN^ep(|hM6D$PSKc|8eiE~^ zBGal=W#P#a`xLtnh!v%QSD)1)3}`EDnBYnvfStd z+xjcT@Hb0>u=^**Q(^V$hu&KStjzM~liAojLkFTUa5JOK>(dWUIUZrO;gny1iSwaI zw>`e0QfwfvQ*{T(>ERb#Ocih|VRb%#94Wbm##O<-)7Wnq4$OQ|^+<?x*|=hgMIJUDP>6+o2gI)Z?Quq(3V@ORLQ!LXWN zTN|~85>8b+0+`j8K0-x-&fJQteJ%c6$G0JiZ-kT%%aGNFAX2JGxa7EB#CEmB;*YAz z1mBPfxB+jnq}&pqxzk4;)o-d>xx~!4G(G=S{rz zp?xJZ`*Fc?X3DK^q#n6R7xT}i`QhT3OrV*en7B*98C*uVDVLUpMFB$ zDnQS`u*u(gJMy+ak)Zb&{&fagLc%|B1WpIx)L;h}VD0?{DheULd_z;)Qw9~l0s25c zPj3#Ou6IVYn)xeDmSs?jV7lVCj++s9mySrWM(+rYg_ZDu5z03hINbax&xd@2cEAIn zRW!*~V-n#tyB%aFATd--yIsu_A3I0Ioy4_d?(>cHCQUxM-FHnZP@ z*py8_#nQ;Tsj__{FdwvKjx4>ezPf;DC`X7*o^8P{i!z8nm~qcOs$JG zuTmgW4=C2B*#OHkaW@UcKpC*7Cw)43(>IeDR(#?(8?_VcJ%0csGS)g) zW9hsoIGd0eoXyw1G2%B{co77;zZKsF;;oSc!_zAUP6CVw3CuA6X9W9VKI&0UP3LWY z%Uqeu;s^aX*59M>&efcWu1%^Nvo2}@bD{;))S-1_YKsk$E<_7GU5kLV^k#qpmY=Wumb`U?@>qMVqrV=2dFI%!v_svf7VGw9%ztrYSgtwP{36fs~8+sLNwK%7R{ z7e)@ZfwYmMypUPc&IjKPV!s|8(X_xBdcC7~pq73gsRvi#9_pm=>7%!wuvz^84T4ZL ztKf8!FzLk)hum%+f@qSCk+GUlG zrq2@1LSy{Eui~8mcTPuBGyH(jcbYG`=xtW<8*)v4y$Y_Zu%oh5sq`+rD6&RirT>dJ zmOBOYx3brM1{P_$Rg-ixBu|6PD^P?!_Ewux$SZx_tev2ZT~` z#b~Iq(35l6Dv(0o94Ab2HEQ7G$8GyKeV>XcrxVUk3waN0osYSNG#JR4j&ZR1b^M}9 z`j}sYwqHL<=*JQ6KB_wz>0-;yKA@l!JftRx(awQu1RWHhs@Nmba)H|d@Ybeo{^~bwtMkhT%2!p3W;LA#5-zC?a?M+& zVVJm}63WZRvMe^lm4ek^Ua_8Alph+FzE=V6i6M6$g5+|z;o%&re4yJMg~OVKvZ_`A z;%MCJ7XSK_b!AZtb zs@yQA(S{e!W9J3@DPZt`y-}}y$yhVwbUX2_Z1J661@S6HhxD; zh?tvJONzx>|Is7>Xj@YNvX7vt05w3$zx9uE?c`%CE&l_1v@@diJJFdq@Ow%RBL}pP z26KLEv1Us0k%nROPTBf^(_JzPCod98xWBkmTi_fUs~on#s;#2Cx^|nN zEpd_mBqdSVr{x+2-lIOY)-)KR*$YZIII80{bl2yDmC^=QOoKH9uy3W23jBiMhSboy zAB=AqFvA-362DFJjc`{^xC(|`GmwEp#LUAz!gW_P(-)Zh2gy!b7|%X}EmjZ8L3%Vk z{IzL$22NZP6XbdSrrd#f4k46)CPd0WbE2vuUrI*MDkU>k*HN`R2w6TK9pp%^KB%<~ z*}8aDP4#7nO+Mc|T+&MT{$y{6UuRp~84$w#eIqnjLBL%24iIjht2L%Y!0$v(D@OYC zf5%#Vn>JROvQ@lpz?82vSyRt9+TkDv{m4eS)6V|-Ds&mjezLbNk*;k5a-XF_T62Yx=9Rt8(bcel(b^uYG2(q6py zJ4k0WyZ0(>>-ON;WSV9u(y$3B%lA(rSRrA;2xL{Tli-M^Ipu z zn?k?%8;Jbxr1}k(BA(#+U=>~asYc|7ulfY0@L>Q1*d!`HrXUBxL7uR%nXDM=j(Mv6 zdlPRvj1dH8$YbH92iu|-`obWv0 z`hAH>C+rR1=%9F<7$e9Bpuw2a8n04idLYs(h$p>0LiSTWfdZx{2*%W0AT92>|WfL zF@38O{x2QTI^0H<;oTxBZ94f z$TvR(EJU0sugZ5qdR;(rn$6~eRg}PSJU;svrR5C9)ZC z>jcI%fn!X1&5|r0k|~n4k5PFod_)d11_3!s4Hk~<04U;v4*P)H?X|zt=QSM>G$>&Z z#1mpmTs?JLDhW8Y_R%zH!iw6qv-d`0Q}qZmb(8?>$Km(l*aH0HuB7zMO-1G6k3yS* z6M&i+C?9p`n0eo&V~)kqKP&KVC^9=Ch2MT(R`USkbLhSK;eQ*nm>>?U66I%`kvvn# z1d<^N+(W9$cP0o0`7X+CdYNmj0GIL{PvC`)Qn6ck#EP zMw2dWNj;9BY6ACDCQ;UlD{4#&h&KVE^^b&p7g`Ta9nga;#r4o!W?b_A4EhiQs&pBJ ztO6Z(F{O|k4m<)g$iC=PY59}o%Mw){VdH+P5FuV2PgcyQye<9mGB`0*!3Mx#+sEgs zwRaHcCO;#Gl>cq=VAP6t`UM3a5HA%i13lUNv2>sq_`Uy17$9imK!ovwzL7{+J=vOB zmwWKc6*wyLbNoJE${bbX9mM0v=%La;2NWx8mu;CX^;>^G--oG)Sv+t0&96(hpu48_ z_E7iDk(2E&{LJXpIJx{}dlwFcM!nFGh(kjEHJ~{&CD>I}x)_%n4&a#GfOgPNdpKr! z{fRqYkZTlmH6h9X2GYJC8}$|71ESfUAm1W~KQy8mseVAq@k99S0YViPrNetG-Z7tH z8(TE3@XO0N>9{2e(lX->B+5gg2oo4Bhyfir(D=wX^!L}TTbL;Je-BQH$omMEQDfyG zmt~lQ4HLGZ8lJo2IUtvqPW=TIntNO3ChGynIt%(HWtox?3GF;@9C?t7&6eV2ferjT3lM8f>>@;jp}en7q^<<4EPWQtE72enIjp6CKSJCS?DZl35T7$YI5?M)Dl zCd%keFy*sagKZDrH1R`zH;dGOgmc0yC5MP=sN;G9W^L49PZE74aoRkKpGwB-JZWKb z>pGSuP{Q7;T|I+I0=^hjCFVeWC9}%jh>Gg#N>Mxa-7ND$GAqWCGsV_N9ya1?zjU}I zT$()313c@lUTxaxwi*B4d-(u+8^BJbvNmPq93U^6!}OEglxQ~pKpSrz)vdE}20WV_^fiC54yJiLr%{79_DawB-rg6oV-H2+n`x!4 zf6k{BjcrU_^X&MvK=n%%hauK1KgZnBB*CSOlit4^GW&=yW-mG?5q1anCo?>>nAud zyl2TB>)ZR5;*pmsG_w0o2zGGRK>8Gx*D)CrTBn2#%CX@*CGkJO5 zJ7SWm^U+h|ckL@uCdkq!@;Ly3Hihxsx@|w zq@9KpdM*fcL08J+YGG{?U+&kP7ZFYv$y61drT|@1Cca$+yMj->vy<39N+*P{#@5?r5Fnvp;ljqeJ(q8(}U2xH`K!_Lsp)|N3 zSP#r;_dXm5Iq?OKUL+%lGfZ7yUt}Co7+gl?<^}SxfJyHQjPMW$P+|OY^22RO`kQ_Dcn6cA2=bimbs= zE(S)5;;j;yQU&>Xz2GF&zm#jbUyse-{A;sQ6aT$8r*5V~#?$p&-W7Ltz=UZ-QPk?J zwHSXHB)U7jg)Iy@jjDT{H_5=$U_pu99#Xzia;AEAO8fv9+4O>L6{@euH7jtA^`&w; z%{E~ui$CYI3iHBe1%Jict5PbjkhKsfy1N2mwr72HQgS`s-kPHHs3cYkcbRtag}wL+ z__2SVPE*%FzwG@PngzzkeU)Ey^GVahL4XY8(2rOr zu2+N6+tgE!XnDMm)*ehuK@KkE-17-H**oJW><*|ga)!H`xJ^MIy-F26Hc+#7(H(x< zhtXQ3wM{Nb0On&r{Kyi2xkZxd~e{G(eEH zZ0wVCP2N8U+hKY;Bh%Gyvc%$t7)4(}uW7JeZ=-8t%8NfX+n@{G!8WjHC*i{xw|Vc` zh3z&u%gEdL0+Y^3(q8Nd~FHPU|+CztkUz ztRt}oWum@PHpyOIp7PA*f*66hD1yV(=O0Jst?ecdMbQtU5IK}0=L`Zma?T(SU+;Nt zTzJ{C3^R95*bnqpb>h~27(1}nVY6no^E&K84Ic2a(jCi7|7pnmT_#dB{TM*Q+3zH= zfq#18q%o-NE@vhgbr=|b%FlMU0;HbL6prV(5Yy`P{u{Re+420a19~F2S9k>>DK9sr zr&MB&L|VMYIHOK<9Arm4MG@gfA*uZ6v_#|E1=H*~mbUebxC=-JgKd*qbhwzxUu+*( z;njJzX)_-nV|78z$>AzEsj!xKmvrmc5*@7g8SX6F!h@3()pvE+czH*u=C$YZ1{ znA%mRH?zW{R~}mI^t=w&IlsihXD~F_ka@;cA$xIpQSIY5NO?&?DdJ*d%st<-PI)y$ zni?dOl2wr|f+Bpx_;|l8&NWU}Osr!1o#E#&xm4vlsfU^q ze)i(GtOoyPT6N?(xCWtszO<*8y(trYLK^TA;dxUEz;0ao^E0;{-NXtbjd!QlXDSK5 z1~v)+Mc{>WD{nJ!nmU(Y^AD;Z&Wo6R&F}QjjJfk30xhH9dQ%K;+*EIx58q4F-WB!p zx(9m+-xa7kP^ggSy9{KWG>zK$sZbqD;Sxb{yXw>}w@_I)zUxFW*WT+IWh_m%&_Fe5e(`4AzxJtZZFxE zm7ecSAy~)-F9`SEZVX~rFLq<#dLVuw&q->35Yz1xSvD7h)*Kj(IOv~cch-n*`qi~A zj_%Ve$G%qw_&THn6`C`zhV_FH0qvKpkz%{+zarhA1Gocr3qayTydh+)V8{x?p9V5w zOJts@x5z;e2A&Q0W73Ud*t4f zuO!j2H`@^Eu-%foy^CSrw;m^Ftc1()_28MkjiBB9M>KDS=>rd&!xB`VZ03Z~NBmO(?^d^CxxNXkH)f3 z7EZ!HXgUOtG9A0s)knny$k=F6{J*FrjD0(%6(yqs79}; z%7^`&nt2HeH97%jG0h6xfrwyu!A*Ll*FqvLT}*{VWITc-EkiX9dAwqN-A#Ju>GtgP z`)7+aKgc&TbGZ|47r>>?FB)ax{q2v<;}d+I32-(n1|KWdPnCNto}-WB25wBQARKl4 zvb5v-UE<8^ci~Jx6R)-ZI4ptL)CipS61wzy)9DD#(FL%T?3|+)IQl5Y_fXR2aS~{u z;RlDc{P#$db`zxE@zw&|cgjp*8#LiYd3qhlPmXu5P;fv+ORi8%AOR08#;m@=iN>hU zoX1t_^w+!(EmX~^*T!QZdyfZ2Ya8m(7oDH?H-yDzzrf~~yr|X!b>r&QIT<{*di9~y;51{i*5|9(X$$uy0Bm+ZZ6lT+mf zJ^*N{Od*J_FRkIk9)wZ4+B5=`66Qz#x{2GgDapQ;H#!KiN>vZ7?TmG0<^GB65(JwZ zQxNp;2}OeV`XE9SJ3VPtmcO2#IrK0AnBKQ1mRt@oX}<|v$WaMZz2469iHuMd>s_S+ zvE{SXHo?u!tzY6D;1mN1!-M>9iS3?A+?@fS?q)a? zdvltWW5CX&KAEqO&E^C3+MxUO$eE%f@-#E8xS!o2#E)h9E5NgamqM$aF6gq}rQi|^ zI3XQDl=6V6gl;5u5P7X;I8u_Vr$-e|8z-8kovps9j{SfDM9eNn(=mC7HXr<`1Ph4# zyx(@XYoF8RfvI{n?G4IRg_V#|HW7kyUQ7vat^t*iPd%@iG+&bYgk5jQlY8NnD{8Aq z?~$JCXcm2gyq`8D{@&*aEP0j0p>j$-ErzB9yt}Pq(}7T~)Pj(eo`jbb>4pShP9e}n z+C2n~k%x$HgNT>-TPX%?UO}pRf=qx;VX(PB3l^&%O&OnCwKgfo3;-WPD=4g` z1V6G*I>5R~1z8BYX1eU10kKfiCvTt{FXuM3U!?*~m`es4GbRO5+*0n*T7A}KOUgli zH@tFNE4tsX37K3pmCTH@I=^6g1-cq%0NoM}fU_+XlD379T4{hwJ;!5mb+MoKK`=Bo zWcAwW)IF0~D)Rf3uM)Hs*c>2P$!H+)aryzF_q-%(ruGSn$F3BuJ)FYwTo?$yMVi&( zys+KfOF^zd{DbK#GS+RR6~d={R*FzktQtKLGXuA$FHA( zoclC&luGw^#aRG7CvUxlTM9HfD)Uw@d#6vXjm2s1YxH)EgNa?7%jj52r4s%D7${D0 zhDyhts8jAYbIZ@nDuBLFPF?AW0?7$lOAEjjrxqr-V3@%H^Nxi)fq!h_*T(+&=os9}B_Zvqf&o&lnv8C~1h#RkF z!6*B4`QKdcP@J>bp*f%(pgVFg9rYk^OPzE}8w3d*6;*AR}WXi8t1oEVPX+{Ph zX)`{(U#r%QvRyipiCVJE@g7U?3BD-*V?f?W8Y%)=Gxn{^jn+dPcQ9j?KGVv#&ET~} z@%p$@{b)I+74{~RzU-!wn-k3ew-OK_v=t~5+y{Y~H>P>qa~<5qRv7XA$0a3q2mTzy zlw}Dr)g&8~0W5YXDbXtL)AA;>bHkQ`f`IwV2PFWt&NIqVh=FMmE&}sEXoN=UhSl;=Y zXQdt(zR;8V{%#JqEt7CMa{9MUgtrgGMF02D=lezQoz%N*`_44cfPb$QMEauU@&~|Y zM>M{*aYyZqucf!-yVP}BX#DpR^|Gu+`_JIkIjW~&Y5)(S|A=u4Wa%q}%T@B2i8Al+ z%xdpLQ$ak&Fc-U%6lL8$^hP=M{eJrxwz>lUS-zhv9>515rS@5jXZ2pDF#^ETq>n8! zr7?YV=PzP3*bgZ`*-I5iF|{8t6*+M4a4Yc()KG8M{Z*Ak{u?s?Fh|A<3j`5`+AXk` z5fd0PRSK9gB>R1npdVRfbGP*D#TL&qY}LY$PB9tb8V-C1U{(_@9H+Ot-2klNU?y+~ zVsK|Qd4RMJpTK^D6k(H|*Ddkso(h@^+D=_0U-fG`F)viiZ7Z;=bm0DMP4#p!6$hcev7hWta32VSl;MpAdwhNCAFn6$>wrvTq@Zg4SR3MDtgkR5 zXI{Q25v^OO*P&6<>w?~MD6ZQ6MVJNJUrPOya(X+|P-QoRKTCh^=ry7};d)R9`w|;K z1hwXuEUK*ry(k9`Ej(uo>0<-F=k^`@gA}ZKp$Qv^sg06a=o&y1^jzHzs-0*Mh@blWHZk8}lrt_Q%A{wl-Ev{ayil%0G6MnGqVRab;cEtU|IGt(fIEd&kh7g|tS_=9$X8$AWcA0Mt(`*8r z+y#jb%O%%*$x0n$GPLpa~WV;~9YNDnc6#gIg!lRGV*OM(CHJ9R*hp zWu=_!ukXE7^S+v{f?5gmL0EdKKzK&muMa*5p_ zpAijDq~?hNi?3-95Y-J~Y88hQjn}VYG58?Kt&-fpSziaM)N$zf&=6^FhB!-e%o)2u zbRVH^-G)CID|BIZF6`N#(JuBc`~kZA9TGI7b_(dE3S*fR#OpW_mSLNBpm}&~eF}g* zw~8bgEf1k5^)d432}vgFK)t!;RZO!j^fECSFRPUN9EVHs0;%P@U(-v_c9+HSExPcl zXO42<0-L#%jmCXINXMwvZbkq z<0~j;pPIh($LRW(>qQ7>dEq46k*l5cs+S6Z@j1nUq&~qRF3TdESvDCz zPP@869)J2hdG=ywY*K=3Og=Skb~+ffGnnC|SUjW!>v;YBd+i2;OF9s#RHi8AYaB7Dl&Cgp?D8u~t*!KjH#3B!>E40SAiK-Di0To|N4py!n|1j zQ%P;~gOkLA`GNUOjyLo>J0jqUUj6nH%B_>E^$DKVT)pZIQ1hjEsFNi@27e;r1;PT? z99=BE5JLH_ps)-kN4SB;Wuy#*jK=6v2-NRdAgUB)u0iD~pD6(0W{MgI_V+^TrVTbM zkgCVfc3saDIkS~~sfMaFd>q|QZXfLhAHj+GgU&y0Vgs>*t!#o&*Hi;1hd-ZPwz?%m z8^KpYWgM>&Oo8=I>#r+4El9o~`wFl+`x!XT#-9}+E16bT$PX&ilSN~!={Y%0L;6$t zAQHlW(LKo+U~px$suk!9CNpt)9_dP^2@;iMo}44KE-5mxW(BQ8`=}X>b-s%ZjYn@-+aZdQv$?+V${!{h|+3_@8Scypn25$H2c(48+ryAFp`fc;c+@%iRe z2l83O0pR8Kq$IZjJWqbQtNfZv)n*0jEjl-_!JrgV!RTJae6yptC>V--nVp(vQx#Av z7@Lv|1%!~%^0#b0KTaS4_5j%RgjU~5N}iObWnfoWC_gQWl~0tzfHD~P**6;gVi8nD zn%EB?A09dgWEb(|$hAunJT$P za7&d_)vao?rQWcJ=S=1;RNf_~8G^QZZ-LSeQ( z@bJ3*3>@~h@^kXcYmh;|r-nJrj1LI-TntcR_-`UqJJ87|+I15Ue$fHHCg%wc(;FOt zI<348M0xtt06~UecC3{?8l${w;WmLtIaYa$GQM?br~n$0TuLC6Aok z5JVY9fE)5c@99p81NQJm;T`kihx-cF2_$52R-8}1X7}}d>CV@02+ZhSeRIFrOMK@Z zTk+WlF#zwITzpT(pTVjjTEGw{HaIX8ryaNRul2r@j#;$O&8V=g-aX157VopqeQUoKCBBQk z0foXdq_%H@&PVbK8S~e620dpV1D`kO!Y<`KfN-Hd=w*(go9oZa`Hsx7jxc&AzKte` zjeqYz)VviA!O$PIoMjXdL>4R$jtdiFUKNtodU5^`G^J%wZ+Rise2pFz9fqSDUuID-D_}@eZAMEl{-pxq5iTT@(DSXTSaaz5iF1{~kS@7gY!s zw0zPd7Xo&)PJ`p!yPzlo#?sv@UV*=D7>PVMVZYFCV^2LA!jEz9!R#KuODr>fyi)Jl3M z|F(~pzoE)(JQnvVAHWmv^W%cx_WH91B=OlwTL&IAyoTy!5rrFY02ZRXpALKe=A52R zm_4F@Fb^C^%^r8!@FiW)NlkFW#LS|^YQOw4KUVfV1itQ@WqxKBj9uP=c3eB%^9V%c zSIr1ktfI%@VMSkbN;C?#dCGM3&@}Qhv9lZDfVvKlNU;sir6=~Wqt(^~r>zPpi z2%k?A2V$5vIz=1!<8gG|(TyJgQy_q|ebzM%JzXTddXR?8{0P9N?5UsLf0~;Tk|ZJ1 z`PW<{=ca)oR!TP_b`(F+-?o#&8LHaGn+F zk>GWg-zJ{Vt+VdTCLX;4_S8sUfO2HE7lz+C2lfVzh-F3ex_OH(F7q$^KC4DUP-TpV z+XoY1fa$`)6~!jf7~kV{kyw;0WAC-x6m_g%(1M2SV!X;AW+P4mOgNWU#lJNUSq?%m zWw3#o5%TW_-aD1I=EtZlZ> ziF*RH6lt2m^;!GJh6uy?6EL0A&%170z&&$1-W!gV%xj=k`abzc*wkS48tu8q+{=Da zFHjmq`|T={KhCn?c?=r>s)QCT8ZWr6$a4qZ)RfJvP(QU35~uI|mRqXXow&o0CIMNc zx)C#E6$6;IDy6GIpy`_QTK<7-KduE=Y!oXTdDbnM-+Y_8#`D6FAj%y^wRe%4R%84&{Q{nG{U+8u8p+soW#+(bED2@;WgKZ@ZZLm-9^GL+o`HYTR+ zn2kVMmLL=uT}Wn*qx~SyLJ-{+{%~f+-RK0h(F9nOv(w%`3@*I-mtIkkz+yXDiGn#H zC%k)VPHD0_V-2VX#PjkPn zxBytnYFBDVG~kRU4=Vu~$Yle^*S11;pfBz|F-)VlJ>^+|B@^)?MoI-COiF4a`Rs<4 zfqmgG3S?Ww#TK=9yDWj$8Dj-J@-=TBq5K+au& z=0YtTz#>TkeRFVKqnM1ChWK16#UI7S8;RTlf)hxiqsZtAFq{Zx>S?=bfJ=%Y3ImqT zC-agfqT2kOpe@6NPUxCGnx(VM`v%FtK7^NQ0`6tu=jziw9{wFuF}i2%Y+Yj}g?Y?J0m|rX-KGq`YbHJF--$k`~(f?_axuiqG72KJtTRh z(UDd&td9pE-a*G){{kF4bzSJl_u`ppa$eWx;>g^*2WDwnDCPJ?{A%S4`xPk`OEYQQ zd{L=>p?V^Kb-3EIcE(Uu4P28tItF+lMn6u5MD~tONPTF1Jg98vuhQz(?0#zCxZ_}# zb{(*KkI#5u>uvIF1S4~u!2LtKxzZdv74$T|x1Ssti$jCCxbI!d^0fVZ<_vB0&3l3A zPIso+J?v?Qcgy+l?Iq93HGK@RQ%xu=A8!W!zTR2Y@5V$#B^Fsp%x9tz z6Z$vJw6A@U@xl_oG!y^ck1`KU_J2-Guze#8V6AlyX=YG9o zX8){2ZPVn?NY~W6_$<&Q__wpi17mI0J>Im;CX{;Sz3Z7rBk*bsZJN6T zS-<8dM0!}r%yJ|jB$Bcp?)D?`m>m`rjkRGE#S4g>)DF z0}k(^#WmXLKJ=|{>B3WMHe&A9)C8Ec!U}Y{`ZLX${Qcx6Q-OCk<9X`}bbEP7^Lmi4 zTYMz}sU4*WZJKUDgZ!fG_{B>_`QWdiyujh7cYR+^yelk1fQ#7Q`&VEN&;+k=1y?Ks z&K1B1Q-L+Xy|(x{lSK}At0?W_3Y0P&8&l}-Wy|z!FL^K+&qN2U7CyfuI8xn_06>Pl zmA!f^{Pv@1x?m?8a_%2(yp8Q{KCds*ZU%CmQ4Y#o&BFx~kD7raY2hl+HPv+ZyX0RC ze?&wwki)^>;jHn_cY~t`;l^aC3lgFo;AHX@sF<@!-bCl~^{!w0!8NO&2r_JVTV@G%M?}qgZ zeIS2cyW{ZPKRJlyj9@`lCQ^v!fL?rER+j5Bc809-!Ke!myIj!(lj?AveafRQ0&ZER z0fP=M$))!!W-XCKv+o1jYXSRW*6}l~T!xsO;9J*TF18bJ>%Bfb{O%3JkeMRnT}njn zP(5HG&KScUpImn?(3c6~HYJNkCID9EjLJH4$yWhA(Kf+0jGdzO7Jjt#yMeWC)zIDT zi|NGUp(n)K*8*UDM*UgwTV&lSCCaxIEHD=i$~zbGt72+RnOl3?DeW#gsM6oU8@Gw%Sn zVqz`BRwdTpR|RN-|AIb1{_Yu-mPl2y2Pj8#4?Thz%(~6?3Dr?l6M%zvApDj~iDD=R zTruek9SjQ3XO%hrav!*`Kb^rrG6Bc16XK*3J=^{%L!{@{s3{_0dQcT5CA!Wx;taa@=18>p?rx1_nN*H<7d{6RD3cg*N<0bF?O^>d-K$J?^Ki%)-rjA zFCYj&h{)Ww?6|mR@{qqTYolIwSSla8KQe|kYkb_HJp7SRS00~y!fYXF;=Dwp;Zl#E ziw0DeShXwW?hlE!^_1t-quU{&-fWras6v|xK>;Bjakq5}qPKvqzWn@#_lKLcAtO5E z>~=*cjeHmSkahCIxZmn<334v(^Ti-Kj!26if&MD}Hk?re9bIJQ8mOKV-D2x*U zY4PId`7xmdwU-9O-Cq5LK!@Q8RVEEC`PF9P0@(=wfzO=$@-Fzk#ly_rXjsd#URFr~k0?jv`-1~IHNZ4f8d-vnAj(;stK;>nNx*ar! zKIa(~kmMG=>N#kGGgbjH)1%%rpd*urXvb4{rI8LAWv%TZ$6;WS2E4%e1CzK!`F3^+`I-zj*#U65votcu*z!;SGJ-!?! zc^Js}KOMv35BzMh$&~h+J*nGamA9&~Mm#r&ChO z`}I+O+f(TPApP?F;HeGxtXb8_GDBjI37nQ|N)x2v-Lxei|ASmsh5+#nI+s$M9}eCf zk9*Z`CtN^|JijOkxJoya}F7N$8iI*S=D@l&7;JX1k z$vq*1uFd;?Og>tsqcER0GR-u*Q{^prFjO(eH~-43xUan}#8)8JrAzFYMJPHbx}P~W^N80~Y^K0VNm=4Tm#AkOYAnZHB}uixhH#XP;iC;?2t z(q?#Lp1h_#Sb{}y#l`tmu!=^+q2WqAWI<@zVGWn}DdURlSCpyBcr;YR#mA>VJ8FUq<=o2qTq#?P0UbE?iTD&qaZ-Md8ykB1i56Yau*kmNpva?##&dhs{*;vnF}wK9<aWxnip9NGXH82Bin-l(lq>R*~)LEw;jEskl%FwC}h2I!(fULXAK<%}8QXi(k?Yiy>JJn0mS~^#snkPDLPJ%U4_m zDfGy!a*ABoAK-k&sRMw93JB4Y$2JY&78vFQ{*iD2`H*gSrML!%c@}?9kkG^w+D(8| zK__{hFScR4E>9yuHWV1voTolp{rJY{U{>KuGIr2?gmaYY4DhlDW77UPVA_O|YIa4V zbKhSy?T*y+zmCbFzVkEYC-1xG@Oa=y2(3I|jifkt?#;w2L_=vu-0!5h==B)h5IxIrW=O zTHGE*bsvj%dzoFNR>wwR$N&cMEen3)OzeyGr|J%gmB@g4-VuMqT=!o$a6YI0BJaRIcl z=trI&`9ueWfOOo0nFe`PFDJKj;j0Cq;?UZ65<<3pKs6RUO#_6(w4#q6={rS%P>%5i z=HnCGyFUT^8;*Dmw(K-i1_T6a{rDU_Y?t6X`0!!E_8=q|&jAy99t7OaaK7IoObLVH zCfBYmR7x}|^{%dOiwBOBM&Tvt7uu0I7^z^a4u~_yU_lknl>@;8lM%jZ8hqa~zs;I^ z3SLY!w=Pi@bK+r#AMf?G>5F6+S9w7G{@eP8mJ$QUmS}rPmPgDAVh<-iVMrd}ZJdMM zA%`mH$NwH~2EC$y0h@-B+tA6;@!YI>Y?+fs4d)`_xw0KskUjV?H2_M4o4Duow~qVw z8Z*-TdV}?!Z%NwPZr@}-5dG<4tDG}fp5{$E&HfPqK?bwZqXlpO>l!I#veZ3NO*DrC zwxOvxX{B2VdT+*B#7!T=9eLKNVXvQV}<)R{;m4hYsW^)AbAQkwD&{ zU%b=z^LC}wf;PY_IMXZRLEP5_pp$|kcd@xo*uDgK2sb=a0W-<&NMEL7gfgT+abHFU zyt$24P?X%)J+?N@-SjKqN`JC43i`en0W^H;6P|EIE`gEvJu%rovAru$|CRu5T`2iW zNTC~rCdWN=+6Yh;qJ*kA>j7lD(rPLQOhALxGD9VZ4zTD!20w;0%C4`yPyax?)AO~j zENF%nl-k=u#?KGGmH4x1xs@y>0Ef0h=%s-t#RKF%+wX_4NeKFNANP3K(h68i9OD{! zR9%#W0lUv{;p(C-*{8vsfZO&pxDSvPxixx@+opE_vZsLVn(GMvphqD*W9Ae1p4bR5 zr3=5)eX#BkRb?|XFNH^kgDgCucn^O+$mxBKJ{s{0z*$<-30Z_UpMV>=DHEPUlA=$$ zM?GK)KMKe(A=dststeNEp*1irM7~I`DgZ|;-U3@PO8K4_Zr4?mbxF6)T*OZmu#O<4 z-H%h?-VD95*VE7x>z1I3$xnp9rR+edTsq}!SKrg`+f|UKe!ll~Fl0j|X#YU`H@B{b z1yYgfZ?kKARudn!{r+fNe-v+6&XIKuAXOw(Om9gmTBe*qYa)L?Go~NRdpXL*D}*9* zG3iJlbOp;CO2FO%o8?xp55HV^8E9e+=wRB(upIiGhwiKovZfWj>FxdZc;|P-0Yt?c z&hJ6$16sr4Ai_mdtyrfcfr_u8mXMy$%>YvdJ*C}%z6(J`1CS%~>p5|#JITBvk@9<6 zx)y17{AKxsszE(!6&+bBX(q0i+YYDuT_l5#OE|ezEy2i`^R32lB=j5aQs-^)e^} zq^e%pvs6w4EeNd2mDinEcc6xNQeWDdHdI&m#zGI2E>9@?rT#AIcR^6!!Pb}&1)^MZ zXO>dw_3e)?-gO*)T$X1adh#2|(ZJ#1V7+3|3w;ag+r^XKUz1=7lQY;5#ZW{T>JN?V z>_h#EdpXILz406SW%zi+?`k3;a`)Hn)w9%gUR?+M2)CgSbUpK$opi^e)aZWwP+DCs)j*tE-^ zLF4YP0{P&D^gd2*6Mn&IJc^kY4!LQyuD0eFwZdCK=AL@eAP*|eT7bJO!&nl;ygoFc zM47JVq(y?`aIs7X7W)IA-dLbs2B=cG&IaZz7}o+{6;C;J^5pCX6)SFlxhZc!_L?Y{ z#D7a{KBM&ma5#^DJ!3=h$ZN`h4EZFWetW05-=sWb;Er9nmQQE%RaBrbw-u@FWI4#Q zlW=}8UV&_D;s=6P%wqEWxp@Fn?$@ZX^0kjsn<3;cSL3YemQRW>r-z@Sc z72eKIC~m@4_D_Z zv?3Tt&jffAmfpTpH^8p(6+xq#wh9HBIQACVi!2#CC?5&WjS_8Vuapcz7x<(g8%7zUNoWR#De&aNt~JiHBoVka+c%Kd z2nFR0lAcM`96(EbUuR!@r;{gD0PNQGDs~PirYV!l41}m+0gb+9uE7v`l7zShj7s{NFtY{B041XD%*44;^-7ool z9A^L;Rvm4k*dtkxn4HQ~omSd4*6YmMn(KkyG^)zx8OS6S>BOtu@n9a;t*;>dw& zTV7vc(3wVxDEmSt9Wi;k3ZaK2ztGSfy($<%eia+68Ix}4#7Yk29f)&gz#N{2fEU&( z7d^@(M=3|I^=inM>jyNG1zYULG+sLG`2EIj&87=ZiQWjSebH>{i=@S*6Zke3XSUQi zoe1#H$I-qUw-;Hy*K?un(*o>44Zd(;#!ETI-tN+0UJ&i7)TTcQz_r-WVoMuTC2t*o z`fy^z9xI?^<|B}dc-HS{H5{L`jt!(P}cE7kWV5^kFsF?oPvj z8FD+5EZh!h{#MS+Qwp8DPi_)x-3P^qQlG|e33U+WwfancHosWJzt_m*{i5$ehur6p)YcvbqpKYjbitdM;r`7|iDS)zGVhtU6%o){|nN z4xhCwV2BRq1VYmU^u!AqDof-lJA6f#eIh>hEK_i%Z>ATJM*?Vj41qcy_55GZEQSfw z&t2nGC>c;?nlw~qK$8TL+cL?ADRC$#Tn&$($C_;PJBQt!3u{_Fuh-{cj2BQM9}k|_ z+6H=}ZyLLGcT*zErjbBbvO_1&qyb<_H7vnB3J$mFbU z%YQ=!dgZn(#(?^xvQ2xHFMpR8yn46U(Y@YG2=#^JMjLTQ9{@1%NSwZduK6j44S#B& zc&CZ!UMn2{o?KRGA{}_|d{d%}$J+;w1rm! zIgvtRa=MD(%MXF@rrdWzMFna`sNdgKW6MqyRkf{rfLd;U&so{1_GyPAle+`66}qe% zM(1Weh55d2|8aB{+ipTp7=9oIxD5kAf?Kc|PH?xU@44-uYxS;5ZaD1y%h#}!_Nwfk zvSHw#`a?44JH=pcQ(X3SyY_p00>DPLl|S@%5($(EHsi=!^QZ#4W5dU?iAZc$`>_J2-q9Z`mL1-o`dT11U_iPe$7Qrs5J&}J<6-A*wOReZkwN9N?I6e|5k{0LADpOsPZwT zuN~9)M+V}mSk!SvtT@+5f~g4vjlCKf&(&Ij0iZP}Oyg`NBu}AM7smF-<&6&Q_%qZ8 zzsmdC-YQC@8y+7=_bMyK#KOc}_&%kP0mvTW*M@Qmu71$?e)l6(jKjiqZ$lx5j2^mP z{r$F&s_rXER~TM2KDFbU4}D9~#;XOkVYmbS`G6Zj#(I2!BnNcqkc-03@cvx(28!O0 z?;JbpM;boy_h}=hRC1`2d;qKdocdMd;nxeU#l1De&=H10aJcv`Fkmg z@7-8>dj@&PePVsXXEd8&CxQWdiMb@``EqeI8DKXdpZN<)PRRy^a_Ne_?gTxh z2)KBKUjXVw==NB@?`^b#$WH!9KH&Rpo{tjsN+ybsM>^Z5Y(w&V=-fBggI=L z4E$8yjR}v!X3CBuKsvo zas-@x;L8#dTIZ%cT&(c9Nt;12t!Di6kAp^6!lq7X@8;*PEBT%~toebp(1sYIua)r< z{gj+R1lc=GQbq6uPNP9tg14HU$yY1A-H%YQDYwPzVZ(LhP;HnGNfuZ!w5;RKugHTa z5vi9FH*sMwo215@O$LlCOMP~*6@DX*OMC|XGNrV8QV;GlFw@=Xpo`BXz(M$4L}i*k zyi6=du^#KY;M#8wR=s z?)B@!YGE(GElo@J@wzRmq$0lcCzYp<6-zg1w=Ad$|2}eD;+%L+$#6vU{V3V zjZMKo$8pLz%*vB2U}=QFKqVIbhQp-VMp1!gh0&#w8uZRv-GpisW2& zWE~f4t0ztTq0T%drt2>j`2#nK?H2gk3i(gmulG)bv~CaU=E`di&>AJO92y=)r=lLU6wWIeaqmz_ZREzOUaQTUJaWB@G%{V(Iy6;=&U7@Gpb=^*r&~zFBkf zhGBNU4}>(6GdZf-y0=^TfK_a;h`Mf1?n=dYXU`!Se%fX8|1_fYfJ_kxe+va|6 zA*vP#tgF)I8%-_D+-durqY{MhFs$zjlo0|-0n=FQD`-S{Q{?P$ROdC+#WVRzd)dW4 zaA075m=QN`2rbqTPP`-wjLl=-nUhq-+;8|J#|QvX!6>vGBAY&7kD_Lk{@iGIj!gj> zh_YRP(xS1L^*nT*e;bHhdc&<-%L4FlXM-8V8Pfci#TT8wKW3h#&G|5XC#u=F+(TEG zMZr4qs`#YWjkHUMN+{MzwSvB&oB95*1CjgK_*$A+_Hj_;H}rWDQ40uA^fy6Q%pQV# z&DKzfrwc1s#bwQ`6vU&>;O*}-@NuOI>BuQ2CZc9xeHiQeeFL@6ZScqiIa86O6MR2p z9p16}JI#-SG3L;L#61u+(YDEB@3(b`9k)D^ppi}BmW z=+LmyrFFQ(3FVR#Q6Yxvyc&*xipgwKA?>}xLMuS|LtDHZfigEgThw`)CwtXQVF2Q$ zekHC9t+RPHDg+8&vU>4!x(PEJ9yL<|Qy2goXpW3Pi!X_ckG(E6gQecknu1v-)+*>8 zitDBM{UwPp`?mKAcq>SNE}9LKC&?Q2X2ykFj-t-;FX%VJU<9Vo=|8u~#(RPAnkCO5 z(^pyCcJNQ>VuM&zCflUWY6;%JhqhcUzE2-d_@?=%may(vxm3=ojsr20=k#UWhpC(gBWtOM1W&IBoFO zhu<~2y7tnhuDYiOkhfb`d&i?2I(e{7U?WZ0mb+($sNE~lf3fjMdr|| zDoqO7#u0D@Ap>}!qm?q>UW?Zf3@=^ zpy1nlFu;U$T<(7b)Bnz#qSOUWO#0` zN?vMAmyoSV%AMHHiL9Wa1Aicy^lRY6(g51Y( zw}9vg{s%TE<9P)WtQG+uWpPsFyTZ$02H8Di(`7ditwfOUuPh1AYDTyY#w(;Bf^Mv^ z>NbGBk=HuRp~SV;`B z6&(`tXdT2fKzW~VYa_|&NRzI*dZ$nUMlXvkvFwGvndrkeEP~j;U)(fUzPoZuM+ieY zGsf2FKo}qNSZKTjz1L`s2gqUl6VOZyhxx1aNI;yibfJtg55kU!4n+fF6ob zBM1E!s;C%H+!UE9M%?{&Y80a>Zt=VELaLfq{vlj*Qj89pQ7tHdfAIFifh9;8O zqJL?9v?<-qg8J@Oce?(${W`;+iH?YD^mv~g?fgd=6RuBGG06IBoyf&sdb;&qLxdNA zM>bMqhx*+me|D}QKNlE**S8=lIbf{UiX>Keh%xP_+4Q78YN3(k1#v$Z`EB z^B8D?QF>=~n{tXD2b$l#MKU=+7YLb|<*}Gzj(g8iH;Y47>%F5F7z`pqC*JAQH?*pW zYWrt9_YWN8e8gYl*8@jXf1H8y_1Y64vU%qYJQQ^aVBuC7%&!L8=pgu&*FCk!r{TAc zK+y@#nH-UHBT^|sElv=)z1U0ZVweZWX{Zqlq7eHM3-@nHGhUyh^6W^<5#A?~f>f%0 zIv8>$Xo&aJ4~UzJ-t~Qaem{)kgd-W0(U+ewyj}~JDck%rZtdF)v6Agl?^LF3-}xIx|AN~T1FfH+7mH-*tt-i zhtQedsNO@WK`L0~G$=W>tk4K~a3rr>biW>_VFnqVh+W=bP^9xtmKIOtpZ3ZaKqN6~ zS;C(tOS%DH?g#;1@dGr zX%L&47kd*f^PL$L;~w9}7m4@{REEq#RzLd-B5S?G&)_TxCRy;)c@}nvC^3!;@5C7` zwDcE28DfL7{0c+>f%#rr zgUnwkrUH~!;4!*s7jbTd4F{Hv7v}}2aGB*udRVWFX~lwZMGnh{NGHR+>L3_dSNTPQ;sX49 zL|4X7Qc&R&8dy0HPk^=Vb-fq9m)Iuxf{=Ii&`R9EW1&f$1{}ILNUF(KaFCvL4n|N2 zVtu?m{~W*0&N%ufWti8Y6jfZ8@lG~>Hne?W+w4us^%x;B4i?23nKX-w3}eChv5Xcw zT<%%Cvg4WX=k7}aG60$-;B`js0kHwJcyPG^s8(57zp_&5N^7e2<JdzGTQd6x*Hm zMUVH)hOiq=K~J*$q|WUR!iq%HUeq?f8zcZo8*WHp4zP>V^}h$D2gv$0JS=&w-(>6-AY1$Bonet!a$tNMI{4UUb=3WhptUir9fsBW8|`|PX7DPMH{ z6h^N@K=78kL&N;lL?ZX?Mak`mU@t2v>+P}K>5WDMy@P;# zfXY^=Iz1_V$f0H8tE3| zub)wM@7I_3`b=41toW6{*y$*GN?Ejd0a&vNSy9Sfk-?+3GugI8O<2BjDO zkbi;H6S)Qs|M7$59q)PF#=l15ykKSk1fxX1-s}Zn;uw&8?smig*VylGw14;%jdj|X zQqTTYzr)EAWnAx0a1AjB1g$VJpM;g1URZ$IprsNxgD z0T^$1zKHQ6vIE@fRsPHI8EPD z{u6do{oD!eR?S#eGzQ-LC3b}Y_-8dyKHn2Him6i7SM&zp1Rs$-@^L}z_)EKMzuLOw z$R$Yq?_lF$!vijB+MKl4B^DGCqwmd=XMmR8#<^(w@?2B`HV~>-O*PjIFwp4fj)=kD z>XyIXMt5!(VviDY6+YLR?8RmiVB+$xI{nfq6Kih`&@VG1cNDKlxrU(cZSgv`1;`rO zeH{;#y_)%WlbeIzlkuJ2L(M4wZnWc|lX zQuD?O;3s`bcz-mD(RtfHVpXBy>Q{)0N6bTTXi{B(mj>vXRv$>3%1q|&$#vBMp4C3{ zmoP-Oq*XaMPY9Q_4-N(HaA)4{>$W(5!?8~71|Uly@V1S3i4cVf5*#K&j`Erpg9Y#u zWxS5WBXN0+kwF%S&7<#QCh|jI#6dk=*njljnAN~SfXJG$g<-mPH*=ZI)r%1s$c0jv z7-hE$B0q%}n0GgF=2KXD&8Yw&ilf)sh46}t zm^8v-RnltLzu}MIIWo|Icrv8dWJSq)+pC3C-Q`n_%1@S; zDe=Q3q04;1TR=(LJJPX8wIYj@#w_!W58N19S5mUOG8N ztthURN6?9LCTVQ_DTJG_U&Y?KeV)f!=0&?CG;TZH#tYtH!^dcQo&2|+q7*3=NHO>g z2UCF6yk5Hkd{t?btu!KPVdshU5`swwqpUV&2^*j%zXCq```CaO*3=d7vku3U7y~nd z@i5w2!W_ylKhUFG+N#a8Z5{g zjw(ii%IOEhi;<-p>gQNh%)RPhM$GyX$%Vq`rgoB?TE2s! z-PRSUuTfM20LG3J8DlmCtkLHF0*x<=(nJM50jGLMQI;XDYDtQg=38o0@vP63@bb04e}Pa3z9Wv;e$v%>9ap;YW53 zo0nzZ2Ft%9t=0SMkmU}fh5Jq-fCQ(Cd?3&h=*_(c@fc^z-FajLZ-7Q z3>em5%AB0V^$LE<`~1aT+X3*z5P4qB5h{5CGH;c$s3UiKG1P!!p8-R?_f#`cL&qECJdhq)PZg=0dD%WEk z<{TT}p`L?ZrO10Fwwd9O$Mh5GHT3R28LfjO0O0B%(x012Qo~W*(Qj1^41_CSmpsFu z8FUlt-m|93Y5>>(3R^AzK6d(CdBAPIw;c~CAU6F7qU_yhl%TSVZY6m_?r4c;n8@?a!U1n;7{pcQts zM2hHCT^bj5U9mChy6 zkpLnCTpsm_;{)`jB=O!$=$%Sj1=?DD^UuSY_w`GZJJ)r_7aI?Q(s95sJnt`>pdiu) zk^KvFYj@+RbfCHK4{3v%z80=hToakwh2Q+%DeCMQGDqRcNP~o|GMF6zX?T13(R9Dk(x2a0_Ayh9?}mhPU|X z8)c@P2`$~gbdXx)2q0A1;KzMG*Xs#*2NXzsrpVQdE!?NIo9K(6t@b4^z|wY=cA-}L zwpwfu5_8>Y!~bQHNQ>w#xTSnDO%DrAk@90a-K0Sz!ElUHvDN9$+AHkrDZ@Y(jl<`S z0qUEHB?|_mApjO&YoNhxCS5yBuN{oo1_9?Dj-mx)$X zXxd+Pw)nyNlgHsCK-mYyRT*SSRBuU`E2VdVz zY4SH{>&{;NSXJ$00xGt9vr$ngd^s!*tJ8efz>PI-sWrD(rJAdP)#(D9ZO=Bv6Hyc7 zgifh(KB$R#&pW}6i-Ip-APjc~Cf>Xec7fP`u z*FlySYAy4u2kAn^RURny+Qp1Cvyj1iEi>ucbdgrotHtKIG1pGJpDd~#W-zPAujr&d zNbh=FkSp2|`Pc?lEp%;of&Mo1y`SQXkWl-TT6Gc-d9H{ETl)zH~Od_`i?35k|8> zDxFrE9zK{w-rM)nM0n%0S&H^-wm?kvD=l|xmjb+~em%NJR~jqJ@Nms{CI;?t?0#B4 zfKXz4$5=EN{ZlT5?UPFIgR+;f`1AEjTVd~_NMpmVfwlfyvuIQWCW|lxFA;Bs=Mg;E zKsEr_Md19S8WrZY12fF+cj2q|y}r?8J%HE|-HlQsY4TxSGyT~03Hqm7LEia_t@ot@ z1iHW0Oo97-;B!#}(G0}Qs7n0+#|6fKT4UAkHwD>v?ExjQRo^cgM({iw#(-wv$MQW_ z!{&9wV$zI_`EJiAbT`hQ8x=pX8_KWQ)#Z)fji>0O;ETLb)K-AN6v~DQ{g8b{rWz;l8RZW=5kv&Ubxa8N#nE^wdUdf(VQ zZr3LnRe(!Ett+%l{0Td>JXg@uZ{@IriHhT&6W)D^*8t~&_M2B? zSba(^22ri+gl~eZ_ej>M3L~y0E(9ftX{^HfS@9bLThwwW$BhUG+mcYpQ7Zt=_y$Uq z2WYBE_Z{b}88YiZCagLV8ED|%i~0S>5hc__6s-^VIw?(q`R}H^8!O10wD4% z?zdE}6R^inX*u}UCAadj!eW-OI%f%z+-$#tE zpYP{I)9T#GCh1n{%#Gj9NVby1lL@+Sfir#Ul(3Z z-XWK}w5WOer;>uoS7&G?j;&+5MiXL69Z6VM4UB#xbvRPc&_bj=t*_-Qa1f>AfxUbR zS6xUgNc+AVL2I@YXK4Drbe3(I3vVgZkUh4!`hdxGF;O^iJay+Efb-ZCZ`j?yY9N|i zBW!q+Pzc?S_WV9MxWI{AEc5uIbYd%6(z5n^0rWME69AN?VX2~z!l4d&7~O)T!Jds^ zD8QKg(B;CEuLZM)H%*x!tA(YTfuXBOf|<0@lV&}osqP>zPac%1FqRVI16v&dmwo4- z77X|SxX7IR_r)YMkm&{vA^n_{Tj3))a@Tc4G;Ivu2Ed@E^EXd1&@W=Pg*YV(?dL1_ zjv~$C?;N-oNV1%reUNuK-2F|(_WP1Fr$}#3bY4IB+e`|u!s_#c;tlvIunU+H&&*U1 zoC{*?A=_m_T)bi@RxOU`ngMKuRABV0Yw4H81aJYcTI22V&5|se4kD>9qDQ?su5EIS zs^~0tzGtO?d4kjYblXeJZ3c)hN9J!Jk^DnE!=EP3qoAOs1jQh$%eRArj2>d&-xB#P z;shH2csbQ$28+xhTurCAJSln+=hF9a3BmSgTB7d~f=yBc#^cd;9{09s6twT^*^}2P zu(dq4U80Jlp?BkXq>Ob0yfz;eXj^LqtP;=mj7 zG_aM?crHrLkzRuZN4Yin!!Y!cxPJiX#?B(1K3ATwqqqKlthwc(Y%n z9zxj_o3e%|I+Y!trl}FOd5Hb^>cc?%J-qa(Zvto{HnpLF1%u)9|M~TqwR|0q4w>uLPIhU;M(=4E zr1aj6M<+P#UTbI!fFCA(A2e`i_Z?(zWj`K@6oTmP*XnZ9%Y#mDBUk26xF{#QE3swK zZG_*lcuDF5L>zQW(0K_^f*$niMc*j_YYkKs#YsTvMVIgc8Xa8jKJ1MW$k)$>!Wwj! zdl4maM7CZq*<;e&0blY;z>O9^8=h5EiBo;neJ$WpMjG_9^~g5(-O6imh+?cXYhe-O zvBJ>#)DKlFMKQD`JE&`LQz+q@CB$7n)iJ>}fi%kNLCxTeR1jrWI0tT@*jIZ(-b6va z7f2d+>ui0U$gHXrzXUlPG>4Fj1hBYfEejyn>;fxhrwHUBolYnN;Dbjpzge?@n=XnA z;D#adSr&+v*RMGcvPP)Z31%|rkX6rR1|jQE$J4`^-Q@#y8;?YwU;izFA5ja|k2UiK z#B1rABihHGq_(&a?v2kVM4R6xTGIP|*|=t4vIZASm8kfu0{t`KYMw%P{r$foo-B^C zwjNdOz;E1qekyO($*O%`-PycY``ic@2K?kUNpMKeYVGb$=<~{U zW1CfrxV>q@Lqp%dth=vKIo@9?$cK?p0zC3bsC^UE(du-|_ON=Ueas(P7R|AAux6}U z5GLP&!rubms}j+l?_)1sYtmtmgkSIrnlWpX1`oFFzemihtP_7V^e2k`T3(BW<}kQ1Fhg zHLyZ%txy>nq$<15qjBAieAokxn+XIi%Y@@Wc0 zWOwB1_Nhw-3g``Yv1{h{F>|t-KI<~)dXfi$C%!CG35oDEq0!im<0gHm`k4HJ_+fGm zz8-;1fcVxaU-^5UY;i#9(ILq6Mb|trk1<`hi2(V=cR#hxgGivrTY;2?O@MT|OExDe z5>S&is&Hb~To?GdzoO%9SBm?EM7x~^V|A8P14q1fyt?_81rh>^Jbb?fpXa0+^k{K$ zf#~w{xGRQp4E>lyE|0LCwybci89F-!LrU zWmB0DEYhR0(r>}^^=`+!LFjpL^+-mKa)N9eqQ@NBr1lCx1t`)GnhciCsaAQB%am0TgEbDvjohvU*v9srvlh_63 zQa+;Qk%De-n9R1#Wc?Twnh_lX+W9&*U)s@`Sygqk?5dlbQTH_;ph|V}cQ%zjS z1^_*Y5B2?n(ch>{yz{t^h=YPVT*02Dp~K z_WNqWO5FWB^f2qy;&&LtQ{kpVEk4OIzauXY7So+T`VsZ2``-NDnzdV_7mlI|G8A-A zRVm+A`w6)Boe=Ar$Icu%5;y-oZZ9?V3~pC&YsBRXH#Y=&md>F8VDQzs>uzJD9sm9h zj>YW3ZG9hHKY`!b%?tBBj?~3?Now3O>(|~bR4@(gquQY0NYK~*fS@e9+WzyF-aN#; zU8TvC(fx@tyNdjI!@t;uQ7WWhV^Km+e=Ijb)me10?0U|Rmx9h3^aWzByM3)$!7JfN z{mInVOvsz%$#2ucn0orvhkrXnt?xby7tuO>JX+(?EP2Gw>ztmtJvYP7DlEzQs0L*w`UvhjU6&k?c1dq7KQpj~3~OitQ)17sFNhTb>2T|%{Zo+G-=jwo zQiZ?jqEDbAFjZU);&i%^h)K5B-y?nrbXGD@Vr9G9k6sFTYyIlaHWTq2eoD5b5da@8 zza54zkgq>GRed9z9T3_5yen@KzG^hLjy`;e6@(%S&~+odF|it>qG$M00$oBocgfT$ zH7xxmT%ec+G{+y%u&7}hTuRqE?gyxJMW|k!z^`~w@8d@V9e{2cTxWqJ)jH1J79p_U zZ#!8~xXfAb1E!B7$jBdZ&98`IOn14Sym@zNuFcnx3T|{66I3`Dc?nsd4VI481%{Wc zTlAO-7t`45cRQt|YG%3kdZLsbvx!OoD<}zitqE6Q8}jc&)ysQnR@PPKGi>WPY|{sL z_q#}0h5Lm(P<5a@^*c96Ht`pObe%LNnXt}0L!MB@9&)bfl6a#m8YZf?nVaF z8&g^!s$Fs5PxL!q?q^EIfh0;5OXX*l$d<6T%`Qx+NP4yrQ2q9?yAX*ZaiHcCEo~AH z+QqLGU=JAUcZHa?RON9}BhcHgEmFAGL&77JLnByKiR=Pr{u9u2(#B%^@)0$bf}6PR zy=1SA6*_joCY(E!G1MrzVgoZ%_%hyGw6`Dm)f)r>JKV?aWswLQBY91qK)=+_`PKE* z3^V=NIFJRPW~?@cXBh0y;Dy21J)~HK3;~OXmu8(MOpPa<;fltZKtpv!vP+|0Ybw&ThAgH~8ZNgL;jFpVxjR_BCQ_#W_YJ_?}I9}b-SUcW-p z%a{n6n6&Z%hnc|hIJnN}y-e9_Sm-_OpP9;J6C`adm$&!<1Kh!)DapHF*l^PFfKsLi zV2EKuSZQ&BC@k277U<@Ucl^%o&vIjQzo)Nv*;w$sWqFsc-Y>y#J#J77wmDH~Q}P7d zgduDz;a8MPzS3nUiDFQ;I?FX1(Z1qVNEYj;o{7@Kh9L2X@O`iG*8lo_ zr@qt>Twq(0s@FC}h@jYZ>l_FBB1`GCQulf<$S!&;}Z>{YD!WDWEi@3dmX-(O%F3_+XrLm?rdZZdso? zSu^iXk>g0tVHhqr_U=vTjXdmaG5vZbGW+=2nbxMv$`dJ9I)$7^9u zo{6f>E|BUy-=mtAK2ktUCgbKKJm#19nKxI=caoUWcTk|0#nBS+$e1MrNdi_=6evvp zYWJt5>YZNS`UMzPi8|e5{0?T)$L^)6u}tm&lbIR0?0i}S1h0NFR)l+g70&6^`9Fq7 zyvEh9Vrsw*xfhOY1qr}Hu^Ysh$>F(nv!E+kOFY)LJk? z;klqeCfx5cB!Qv}7mC&DZ%?v}=f9`NLDlfeet2IXEfn{OR5Sg%82wkVAsE%qEUjY-okJlLE2{mO*%;eQ%hlUy-9y(}Lme6R)6p3iH=E{*KrzoFS}?JqPICE_@#E zvVM0K6`(6{UFXhrP(Qfyqn+oGms2P6jVBY|<2zodbBVA+ZxQPYUd ztei%*;0~mh)2a*o8B{=H{dCljlKEreC$bFdz|GpoYD8PcAkhUKkT8z`CJ5aVv7x+a z-kxn1EpzD8lqDqltZpssH7+IH8?Vzr0A;Tq(|~Ct??KxgoHo6}Oc%XbrbGG+BuHnN z6Os|=g~lFrIO;wSjA!?hm19sxCts(Ea2)EHLs;X7D7Z8_gwTqMas%l@N6I=;`oyFV zt6%|6#UrQ&y@Bf87_%W)iC)5m=urjA@WYJJqXdGzC*cwx8Jr1#xaJ$GI|?|z2@p@c z%SEXiQ?juH4}4qjQYX-R0io>U);Nfei-FV(WU28tn`K3Mz(^QJI5n~*#-e~bE#ZNH z2b3gVCTQ&)zy=rmu0D(mkTw74yeZl*!VGTk>!-8Q;HU+y$x0o~w1d{M#*e0wYRjd{ zP@X2&CmFo!G(Zbneu}cDo&W}TUS_S->!ZTu{DvTICQ}_Wl;&VlC_4*jfjp%|Ks6&H z5B_c6b(Sq-nT{B~L4b8CF#_8mZZp84%BoVdz{OH@NTS@+s_0w##Ai5S|K&T zRgLzPYZB!v{stJI-Vom;C+@8mulcKD(K&svzjymaCdyKntA;YV3K8(fIY|^>?oDhcG=H~E7qF=U}im)%h100j3+#A2!~Z}WV=DZDUSh1 z@q9ZldYs*J&AY1EjfLLiPMV+&Lq;_K%LLuLs1l28y5Mad0ZTI!~M;RkwTc{_}*S8*1$?u%t+TIQblhuI7B&r-iN-N9z zYXiy0H^`_;uc>y>Qk7ya-e1FMY1H@TygBXNA(<^Xz7H?CXQS0W__KeNZX#m)6`F8IHRiKW4?7df$;#dd+m zxzA4(Ew%K^lG$I6N{MEZ>P!enE#x)egEz?qgSjAz|)O&^c? znZc;j;AM6`_pwyp!z*;!smgRB%Vf(Pqh7YihPBKz$pm39u~u#gpycA55(nU)bGGg- zn=UfNoy9JD>~JAqx|N4-D&Tb#8MZT?O=x%QIxCA@scpbNE}crbJ}mAeGcjg3Y-mlb zhE6vb(AgDw#F{=a%}!@BMUTJ(A_F0Mjy04WtD=FIM$FoIyyd;j7Y(TtixFvzh9Z+Ea|4^gB7160PaP z@I*Vmt5wGw-y$>ph3ZxsfGVOZbu%%iArOrJT z6dy=4bvLYadJ56O2$6=^Ma8Y>2yHO#Zk5rdGwWhJQ`HTCjZ;L_7ARw)&!mNbR9pW5Pxg|M9N>IA!6l&j{_Crqm}7c#T* zqO$I-E8FdsSpwDx0nJ1HvO3)+V$PUCAtec zF|h@OFBNDxK^1$0zOm4CRb+NL*Y>g^k*SSxos^KUEQiSurd?~$LvUm`i&ega53m;L zbSsL6u?*{r&8fTXB$}#HELY`D(FBc-DEB9x>53DfR)tXenweC<62Y2CVegIBWvbj5 z;~r(T+afW`m{tq*jy6Ogo3Nc`sp8Zc>;$BRR~paeV5_aH=BBt>E=N=mlK~1zlKq0Y zEUxFXexs4u>;~P$3}O*%zcyOundNRb*{HrcA+qD%2r!6lD`lx{jc>6TK1Z4D(yZ6x zQ=JttXZcPC6PQw6^b(2L%B&Z}9?1_HteEdW07#gPH)e4ytwcr>@~Y?6q@9*Z(b{w^ z)2URK%rE>*MXITk!p;FP*u@2c_SWobP0#YF#&{*_oo;t$Y!dyJwr#f9P}GSl2BF_lxCklHX)Np3GDM1QgAfj$HPOs~->YAe#T zoZ)QAZc5&cOEA3B%7Vjqg#sI`?rQGx9*!HJewBHRPZZdGqcrW!8sj{GwbC=)TUc$j zJkfW0s_L%BB^oPMM}=B(Qrb~2UrbL|jmn}?EU~ztbVsgUoe#_zYXIh;+GbdQTvP;O zG+LQprIi`6GwAxe^fI;Dto^)}-4K?5HrG15WLx z8wpYNii4Vrt?^c-N~N{pY@5qADk_=p?owR`_!XXMt?@*v+m`%LUY2Jk+4;`%IA&AC_yt}7$&51%LUfCHM^}0ch0jS1s@qT~zMQaAiqi(o zAmKK14SHKl<8qpq$>1$6s&h(E>rSUF%TCgYU@Ykj-vl(+hTeDro+C=6wGwp0^#lNtrYn-FZ#QN))x3TPdYUax@<4ojM9u9dG!`wz0=jFOuDDvq-8&}zGk7>5??p*40=V_5pv}N9@Q>s3h=RI!& z&`GmU(VTn{)1-!%QHlHxC-ZfE(>FaUJ@r7BZ9B`}%J!z3KC>B;0#PlKR!ON&N5F%o zJ*nvVn_`MjdxNGZ_8W=aye(4l%q~@SqUX1pay#X3W+F2)txj&xua|O@o`~hxw6n(9jPsktgN z$7w#Z@=3BR`c}Jz`5G_E8qrO79+Al5P8Hu^xeg(Ei)^QpX|M~YB2NZ`K}!afzbj(g zHba!A_DE=^1Z_^TptsFNqed--kL%dVp`tAdap^j){HNSjW}HS!P|GV23Q-;P@Y;DWREFblp5yJF|FZBJ=9 zhRUt=(uOihs}$^bO_65PG2PRaC2j;b)@I5AZ$&d**#L5CI~nW>8>+LdK;Wd3w)r_J zX1m?2-yS!sl5KAXHYmVa6CZ#jW8_CUqmju^RwHnb^(#%vs3thS)~nRALlp2)5>~OB z-)>7o3Mc#ZW}&&AZO5e%y_sse&3puMuauqeN_INzmb0bC*faHde&zP2LZVk^nrhCP zY^U9>rJ98XMBJ=xeb%VtE8PjbFlU>>vXLkc@_l1AQ1Y8xbu`18z6s=Q&o--SqFez? zrZE~*?bMLS~S*(qvMkQcr?1rtCtd+$k<)R!UV2gW>BelRR~igN(f}^GSj~5BimmAI|iqEeakHt6%k_Z{f^>**P}=EWO(Pr+E*#8C}f(Y?Z7OTYjRbk#0l)G3axJk z{#F>a>jg`d>Qk*UTrGIHKWk6MLV2>X^F@7ET=WK_I-jm3c}-C>W2nP61WceiY|zzy z$JeP|+sm(OfGg?wQf0j%Y!&e2dS;|BPD`Mw{BBGXn6z3T({G{g{xp@+sCsLvZcG3lXS$nd zbGvA39jZm^iZWFvgM7WM0LaIs)b6G+De0mDGE%Kf!DVX|`4YsFLL~Gcq@m0QFtZo3dvmyF~o+ynw zq9%xGZiP*2g%Pz(?Ffj`^Q)CLT2hq~X-=m-jid1bm&&C$@Lpo3&GU(^w^FzrxP3Rf z>9m?jClXezAvxNhoXP3F=y~b}YY@2&$#Pn8H)tkwx|ijrE2pqiCzOnGW$@*gVOFl} zHX1vI6;dN{Q}3XJWQCF2Rmxz?1T#+X8NhJh^*Yf_6JBPtCfbccuB-P{xt?ij)%8;1 z#--V8nitukn1LWXEzK6D-K=|leW$x>yWCrj7L$q(U>X-)E3uE-05u96dMv7F>eU5=ae*~WIXu`=UfJ(UqblN&G@U6_pO zYs{-u2Nrk(GwZIn=xDOB?rNjvxFCWSoL(;rqrNvR&~*sIrfciPpdgv1FcI=neq|Sh zZQCk=Prtg>DhUHqG9J%2)|E_C8<*E+j_9vuW3@?0%{j;BnG~JPHRi7FIz_**m1ui8 zD9;5D#cW4{;GxvO8xS=HhSE$=wCQ3#Nq5SH72rhuS~kC`!#d>V`o=fiS-q|Z;Oth! ztRv{f(Oe-(%B*HGX;v(iD)suNf)C0Gr;?e>+|IH-n(4W+y09#_5B`3YY9;2&>15gw zGnLlRW3#X!ndN3(;WKM7EtOW;VR17Dv~RAgOwz=BG_F)wV?LbVZ48Xs5w^?rWOF;H zR13^J=g~EiCc!zTjs%O&_w8|RQwEGt!KDXIB3IEMh|Ds0Ay3wpovpmVW?8dJ8=#Mp zO1m{!cl2ycs~DB?a$9L+>W%aqoa+)OzE-ve8@5Q>BdMjinpzc09j(IDh%&LHGFv%8 zcYS^X=tQofH^gBc%j+)35w_fmFhRVsM{duel99a}BOz2dH1FHELNU-P|` zYxddEV!)^#E_QRmr7A*Q)qD->j8WBEMRUJ9&0U(J2qBcxJDu*Q)KEo*vH{+2T0QdX3z& zxM}y5qBhjOjtYrdqTl0I)w+<-HNomKnUzq}i*+Tp zk~&Rn(H!NA)6{S>WAL4rDm96k%r+AgBk58il~txgFqBfDZkztZD@nNJEZNM?!&y$Q zZ3?Me*TLB=$0;o=)zGFbx3|jYxxyUw$j)ULpomlML5QSaO2rA4cI9aifK7%$cW{dn z1$9M%uccN~0A0Ofva6Ic2858^+AZ_bR8#X)C9}k?MH3>)f zG~1;TY#}joh5krO&{R6(ktME13<0}t0vaP-^8D#WVlt*OvxgYKTiqr^l$1838*Iuk zb~Kw`w*)?KEemYh0DY-Tidd&auX9}oWfixzy1ZOl1-4~v z(`#&MkQBKDiz-{QQu84R;j3weZng%Ke5=qdH2Z#r5L+fDrm0Of$6+11G1>se)YtJw z+ZYz{1)ghT^Q>526HRa&4xJ3in7JL6YeFEEnH4wLQMXfX4%e+szAJ76tDyXqP;8 zx$b5?8^99v^}MgNxpfuL=SwYN8N)_4JqCP4i>J1Pqq|mqsaN_2Ti-3KMvZEAcReP{ zTN_tKSkg*YYG>IZZ+ASSlwV9cWg?@=!(~w_rgJ2{tYeJOu4Oc3p z*BDjHW?tZAi-BNpzN^t0-%WV10LltPxTn)}W><#TEKkl2n;^Ff+vs%ltWtnbg{x#F zEhX0&)Zvv9bNR8ctdi5hCSytzJ)1B}WrI1(JV^L;ksvJwCqV-2=4@>+z`f~=7ls*Y z%cr!O%a;to)b$RNbpf)FH55(;VA?hf?q`4Ai<63+TI*Ym z?`&G#LVw;8W)o>Ka8o^s-8RYvBGay{iK;r?_6u26H>;(sMQX6Al~t~6Y*NL|k}i_9 zL8`&BBtDjmE@g75I)tvPjcJOha?{mD9C0IrKUq&#JXbQCHhS0E zaszy_d~AHyz&_C&=A#u zKW%UQ8TbhNmDg=#%w|Hvy{ZLngt2VsGa76#@K=w$oVM|%oz*bcSlJ3|l|0Mf^SnF2 zdje)H8>#x%vHY3V?+A-F#PU{U@Yyzb*qFN5tW8E6VOGFqyxeUnGp84(EmP~$>qVn`+ zRGc|vH`^?1cFdSdbQHeGkGuY|XskL7vR#A7LxPzbb=N3z+eCf7mYdb8R(65!ZtD$} z$0WW>4kjATGP6o+20mC%sAJmh zjAX7pw|Hf&27^*=pI5xZa4#? zOLsF$cU^5VPPIxd>Soqu+JMzsa^9Wrtp=&84z?DaBrq>D_3; z$R>n>I~&Cq_3TVr2P?DI6~tjbn*sE-)TL{+7S@*mh(+hh106y$8eVGTj^QWqc^3cY2OU#Wb{oZ~}cBMs|znbh?hzN_prt5;`h#x&*h;hAZAodu19 z@)~-JYpGP(l&0WY>1B;czw23gDzTAL(@hl|w`?|VE1A}e-w7Qj!_6Qbtn^oaUni%= zY$=)>Hv>S97Qo1-y0nE@0)iK?8$h<{>seco=S^eK!XfZT4|>}g(QA-;cRZ3;4MCrk zn)JkJ*lt?_NAYflSLLAx0X!^&FP1fy8kaJYO1-kHJG$z&J9MSb^X5bzxtl5|xnRZ? zc9U(JZSShIS~oRtJgi-am1TE+)8?d26Z{1&vDwbl6?VERvm>t4nyxb5q(PMdS7zbs zYESCSy+*GM+)tnsA>FEAjIwT-JHSX8bl#lnwvx(p)-}xSt{fG-RJ-;}WOG_#C{H)_ z-n=tePZlXR)Q44Wpw?}CO&K!?dF~Xh!4wkBA`M`lshXH3q>af{y%~VeGgKX4FH+TR zu|JX|e?FoUmT&Zz-QIYxacx}9Ps)n!4zxLh4*OHE%&{DSaGZ@<3talOX|FcVxD}V0 zknK8+5`gTL$Tgi>4OAbgJu-I|uk^(n)2*j<0M@Y0mESfescfRL=CV?{GOEnlg?`S+ zZR-1H=9I*i1+CY|NOwq4Q?px0QEY29G)+#dr)qnu^BFaQk|cA}nk z7v)ZuUduBYpVmqYH5<&wwLV)J&L9ZKLMWtcIg5g|8PSbxwY=1;4W+r@tV*Z0KrJ;$ zKwPY+okU{dS6h_aPdJ>rSj&Zxw06stZDl>!_0&Xu-Q0HiHEcHT&D}Y~EE8jG>-Ez^ zMJ+W3gRI;eHr$++GqyWP*Q=I38|AcNT3=SV+OR*=sP1ss8&lG>))$2KR;RS}EU(Y{ zN~P^WWQ%A{bIe4Zwnvt)x7``N%Vox+fyA%+zP~I3dJUYH#bVRawN;lkGvzYngI&wO z;%yW#p~Uov9$lWuwMl;tarDHZJ_Xfs(rEYi?Y38RC3DwU6^FdeP!@WoY}ZN``U851 zjr|dDM5#R&3E!LJT~4Y2Mwi>R*1m{Y09o%?up13BH!e}NOs?45WlT-V*;}>=X5Ro{ zpw+UyT?qwo4YXZliOsl@&N<5>WtK|?p)W0seh2VyWWBKT^F*S+d0D2EPBkZu^3oaD zy?Slg9cF#a7jSX~p-7;ewo+9gJJ5QDI58WwMcr6X;xs?bHT<$;7?X0oqD-X~F&JjK zZLzM+YPno?;-vM`XrfQ4k)AE++qp}E^Ig;HgiOEFGu-8D+oW?f zyP4Y!bHzawLRsszth(%KvjGcItNASi(luA}N@qS^SxW#(b4t_f*U|zYD_T>(y-keP zt4=CGk=AI0=fUwkhv1zz?oWEP@dARYa;?!DPO5;=WlBR~R}y^5El%(vV1@yEDU)KF zT~pc+7xH@E8k2>(GJ}n~pyoV0YoF<)hpMIPqh7a|N;}g=TAcQDqq<2~TNS-rFR^qk zHSA!1F}uNJ0trrw^@`oygF ztgUW?hdI~i)yW*Q-4zHBP_icu$`!u8Olh4$U!Z4=Gz0IUje2>)O)t5INlh!vyz8zq zH26RhEwN}d7p%O}c7=`(v5KkMU;7P#U7)>Mw^wap3Tw6^f&T$E2vN#;+p4h~=XVx9 zUDrYREo}VEdRMG3y4&Jl)&K}d6V0(Q9~WS8S_5jA8JBYvv0N*F%Dk!+ySrhx)63$s zvJJ6+rBND^&P?eH+WpR;q@;s=3-_BET({p=fm!->hRupj3qoz+^9Dm1(;eRF`fy#@ z!gu{;Ix{Hv!!kG0l&TGw55R8LC&O`nU{AD$;jZV{rfUZ4n!8WOfS#f%}P6zS_!*z zNLvNEqKv3%gO!Sgp2>kor?ce&UR;gUY*m}~0VEypkRd#1zt9{m7j=Fyu9YUz)aeVO znJao)z2j`a5FJY3_NgwUQC-pGR=?9JC~e?#*(I!j;dlqmDUOH0M5&j~i1o^(BlN^{ zapbhi-b^8#QEyW~w66`)nPqlX9f(B+7T*M%=IRJksAgJFhcer$RRA~a@77IQZU*=0 z9`6Amujz1Pwyj9*+_2Se6q+zei&J81DR2kg0 zIuNJ>hd>){V{MJ$#>{e>iSrq$H!oPsygcLGjK&Suq&w&qfQB^{SK zVyC+)mkCX(JET%U*BEs#oe{FI_LT++LVBlwk3Iw2&?YilA?={=qu3UDJn%m+C%({= z3Nv?@#fH?VO^wBdmC<0HCv7p^3D;IbT1{$&Dh(*6B25%G!(jm@V9j^KHE)bzF{qm* z+lKcE-l;pu4%RS!@VVgEjdnU%LvTG=M9dtmT{tJ2m3|uWWOUBn&Y66+UpS7D3*IBV zM?+KXbU0qIzgVvWIt1r((j<77JMNXJr99lqr_q0EHj`qoysxY6cDk4;rqlWKB>w#y z(VC<^*?u#UC;3)4?+h|rm#@{{VO9# z^QWO;7*F}Xo^-%ud!WfH&cA;Z3jd+Q|H0vM?%%)gzn&aL^8Wqn2TpKUlEjE4K{H95 z`RmEG6-gHiPf-8;3&t>I#SLzviw4}FAQ-Ol*Aqa0H2}qU8VrSD=u3|7DUxMaa4(!? zljw(_Y4r2glkX~kGqhbzkgfG`jOdUxow^D<43@H$TXh6@6ZA^qv&xE%o*w&~XW`454!$T;H`RcO z`>&@$8kap~u_y02O|8vVg-?PxcQwVB5+P}cBWXzzQItzke3F9KF)g@(r#OP-38%D# z%y0|7?%s)-E9Ec~lJ7XzuRJA+vakjh71*i(FC|FTwpSy+?aIpRJuLMUIOwT=Bl!~5CH*MeB@8Mvk{?pCq;o$1+5Rq5rMLYwS3q;Xxf2$d5#`@Ay}A#?^$ShKX4`NC#Sq;99_kL z2aL8bj6)`=XMQS zSs41>+2K>FK8&}aCx4E&l`KO>Px7R}FSZ3k5d7aLF`ef?a8lu~NCKkkOf{f5+$3CZ zmTw-#GCBgf0eZf}NA%3-d%+CuANqJg&=*&a`|j!CXE#8&DDGSTE%;8~KCJ#Db8Pzp zOwjge8UAZ1E5!(I^gtf`rY%K9TM9>NLcFEoEfsI6U)oZ1w52dQ-cqrYilx+RrIf6% zkeK&%FR&t4GE`BmDKvxgaa zLThL02>S&DK4;7KV96Q^EGrm{KvsHqD7YZlR)^1z_Oc!AXuY zrhmbV&MD~ahK^!N+6(Tzb$HPyvcLbO4zm*bLf<4XT$mcc0EvNL6&WnW-j*1*h9a&@ zpI3~ec>jgF`0Oj;q4T@JvFmr^PQQ9`f;ba_rzeQ>=L_%ltZ=+b;%I`0_6QO++)>mJ z7@MRh98_o&9Qy0&z(;X9R;gvndSvJa!=INoIk+8gtl;#XErUuS8J2r?z_V1~>5>KP z##$o{7^%wWYsaU77^kRsYeg~9(y?4`VadLMo+)IqqUE`+2Eaght|<1|SI?L?(yiaj zy$crjnVT?n*0I*2@KS(A+F$jlEF8_i;X_BU1wC-MKgkLxeY)iY@qkERf-pQgBP*~> z4(h?JGZ)`EarD!0+6ivzX`s*|DGVDG=@R-z_QT6joMIj*wgi24Leo`ke+JFkBLi*d ziGCX)9L-uRhTkNHL+)~0N`pXjAuy&~mv#J|BJD!tp374-d>~G5irnL_Ilt^>*?5Uj z=>k~7cuGHI1^WqueXc%b&SfS-Qy79I*chA?L*dCEg(r_CDF$RG4lZ5{So72fmi*>G zh@bH*5ZnxY80LZa6&UXdb_F{Ju7tecF3Ebxa;53#db~Z>arv_@+Vfwq=2|3`H<(e9&Zm;0(g#fcf%+AmJC+7Le81 z5DnFv%C@0obd{IPomiI#`#mk1dX@0WPL{NelM*HyA8 z79MYxc)R?e+Xbgc5GzP8`}G}z?E84f#76(0ZS+$ZGGYEXF=l>c%S07|Vj&X?nLk}8 z!_kKMmF*H$eTuhByj}kA?Q+!3uI-pJ7z)Cb1TYl(E*J_zt(?G6PPXLvsMLN`>Mh_F zgyO$CD!m_-yl+(Md{id-NTA3I$=tXNv%d{--)-m%WDa?GBLpD5dLuTt5r>kvUPKb1 zVos^+czo+|mxNbD7X;66bnU>xNzw?R@u(t$2aq=Wd@xA+ zRq3ub0${Hq_7m|UxzQy7!H>=X4oD3kwTXOYp!QfSrk zNrp*MK+129e-BCYQ3>HKxEtaGQyAy)i~+Fhz$DI)UF0=n7xS*=DsB`hh|4p+Lk$Zy z(g?1Cc3+uP6=DQ$-07KO8P|Zu2#j!JB=S`wg;FNK%RC5ed`DvC-^9(-O6GQMB|$x( zUHPEoN(9%8b1PrWtBYzsm+Bp*P z{0-@IRiQ>YGAPviT?#eQzEAXa@&-;H9p40i`+K$Y(?M_8N9Z^j6u)oKbMuJ2VIHC5 zXi(z5LC>wDTgDJNjs_*~8}!^3BH0^*hK3IWrtcb*IUkg}F(?-fO5Zo=`RpS;d1kYn z#LI8$qXEF(P-GX?FbE#usN0h~A5;}be?CYbMXThCDx?v5@zHq_+*{Hl$gi6wK@g$6 z4F4eh^oA@6I2c!c`=XWKuBx9*#Cjj0GKUFu&s`C$wKpfgAx??dEOQ^5AwY0TEq*N)HJ8O3f$DGFYBT!rx?)#RAm$SJjpt?#Fte+Ca$%+6Kgm z0wFj{pc3Qo7ej@*yb$lMOh7^NACptq_H@G#j8mq~Dc$FdgT#C1>$Tlnqoys5%Y zLLXssEJPh9F5qscj7t(I3o{bVN|;82GyoE6GFoH zAQbPVNt73fGeC=gCN%sI9Cwu{#LGlE{PwpJ5XE@~QD_1~4#7{S$}zWzgC`7V!c#vj zl)LF5!c&6GMY&V>os=LzK}OlBCzRMnW6lWiikcj+pu;WpL=sOqMRgR#y~dkJ0{GIN zEk&6uI7K9XkSuI^8r9{ce=dr$|Ga%*k(9t-#C~(Wb!!%Q?73Osv1k^e{0J6tJC;d; zhzM$?Jiai^?;+# zF5Xnt`I9nX$Q^PhVTFh6m;_#la0Rid{$RT(teqGZG$q5ezt5%pRc+%>wUT~W+dxo{ zy&MEA#X;MEf8M1G-XE>u-K}TSM~jx>5sN25#g5hNH@XMt(@t`s(Z`WN@6lg1`iQ%Y zK7!|oFJ|=Jscq0llNl))uoSr@1+RCaZG1xm5Bo5AcQm*;jNel~mUkbd*dYe7U;2Ec z*x{(9$@kGN-n!Ij{5pRL&I>diF_*{Q!;P^$dkO4J@#* zs)4V>frSUF%#lTia|EysaMPg%X7Pf)qf|7~$PlO6v?1=RfJ zsMrXE;G%}V4{cxmybErKW+}Iyr5Cy+;TK(!9$Sy=w4hVmCg}@xXo%Y+y&7Hdns!4M zVBxbf&-E3$yiLGw|NB3;p96tn|Fc2!aX})!8DqhOMTSu+yZ~Xr;Qf#mN54rC6v_PZn( zeufQXZot~naTt@hTN<8@$-T^}VTD9x0+^#0D=6d)u$@orH49iP5@JWs*-dAc#D!;n z+hWY0STq1trNRE8nSWT~pC14{JpM~E*gqUW9sea*;#CDWqD)a}grRn_lH3PLwo7no9>E8LBd9T^*cb z7zN=K_<=BY+;sCK$J0`E>El$HZZKC_G*^*A^L&s6;<(t)ahCo14~7fXmN@Y8dIJaI zT!N(iHm$Maz{@XbJNvlH&1x{uceuV(MZ(Q^hqe;l_7}oIvX)CxTif2r&AfQY9!k;PCU-Xi;DizB42VK|JrSQ-Y=hrjxyII*A*D?5iQN zsFX!e>*u*a&TD9Z8|Fq$LUbV-mAPxwb8U^iQSJjBM`O}&d(sgzeZ!uIAGljD zz29WqTb>nVRbCx*zsuPB20h0E+uac1Rio=tdyrP7;2)hPZDY&zTob-JZwuyBv7XeRnyp>c!s> zNa%RLEAATehW^MOOB(b0sx3(MxH$@YS<#rgpg`mjytvPv2jTc#_c`Z52y1g*p!o(< zLKlX-{ta(~#LU$}_e_buYs|~CHaD13a3Y+&dq%xfQf_di;6%Xc?-@0ChO=RBD3rm8 zfZ5+S?4^jm)#x6b2-y97!=6{rgSB>R*!^y!?i==ex52k>EG|$54m_W}XWUD5^|V+Y z@%)YUo07QiMwc23d%0U~(*Lk2-}XW`AAi3K?;G`!rQFzs!HE;-_XHa{_3FU z#3?N0`B5)+A%1HYq7$dUlxIgpk8r;JZmnyS3XlBd2YvYYVadCP4R;}R->?^(5Wnyt zAg%rSUBXRCzv)(vd3dhCeRR=YKLz1`QceqeK^$nO03u1EUITHJ z`?VvGyeI79+Jot`BslWn zxe(g!L!at@DYPu^MK|0;Hrxg_JP&K2A5_pCEv2}kyKvjTi|$$$ctjoCZG|JOrwLds7x)UK69n|B6xL zuSSi7K}-Hk|HJX{7yM5B!@zH^f``w1GuVBiwK?phDDjYDQRx<%N+O^7H`i;=2=!rI zoe+$*2e@!|ipoYJiDrS6F)SGFs9GM591+d<-(Xozfj8QYB?_VrfH0)9{Ef`hXF=Q7 zaL}LH6zV!a9{;ic`O}4ly$K`<18}>!z=(6mUD&l134^ zt3?8W$i>w&OoaLP31ddxIuo_Uxq?2s0cAV8&DAT;+o4^%;v9^3?F#bMdh}KD{I_Y0{4H_hq5fp){7W{KamMcQv?CO(@p@ESew1aXKHv7#6&im{>?D~hqA z7%Pe&r6}I2CUskdWKox_>t|5CrR!%9v_4i8V?{Am6yK;Q9?Nm#u|F|Z6Js?oRuf}2 zF;){lNKHhw#IEXfB>4tMW0XL3aR#up=gz^{pBVcSV}Ign)WoP!CkKINK&-}r=Q!{j z2cF}=a~yb%1J9r9PrT(D>c6;AdIcS`4j4uV%fZ|pRuS}}>L6O8EsRsT2`lOn$i8g3-$9|AF%DO*hq2T9(_xd`f8 zU(eHs%7y>rkfU#%L4AZ`EMJV}i?Muf)Dw>h95{o=dSa|6#(H9`C&qeWtS5d{J(%0t z;H^ww2DK%*afZH~>3d$}FV6IhGkxPs-)DL$LO|DFm_K(@43Fan^)bo>Awhp6SO)2k zIQ`8!DzeWvRJvb0i4KFk$AH43kv(<6rEEE zbS9+k5ha5&9DrmHxR&H$^`Wkbzq+`ocUs(k^8j7Mu=l4s_KuWC_}2sFSsn<^M1+3; zp#kv}{4mU)K9)4Miw_C zi}NOb-)3Yi`^EDnHNlg#O9>tIQ3AL&pm70^okTyVZTqpf0SykQSV(`ekOuc)+%Yun z7#epB{gYB^;^GD$TigJNwcn|@0TuPHiHjS=#SKoglApy~zowrz>U`a|e@X@(!YV}G zB;NynmNY-@L4Kypyhm{Z@PSgOumMhE!9O^Ip$~BERWR|(&2+?w$D1kMOy6ZQ;RFkN zh@@!LewX0>Aqo2WHoA1B*N|rORI(g>hq7Lcr?QDmo~I$)`Fx*Er~hsKTL*{3jTTi& z#xV*D3`z$6rMTBS)>-Z(m-*I3!aj7>xFn?=cJ{TBB zQxC%uD2#A(SQL6cANGjEzAYGRe_`*}Dykw7u2tmzAQ%Dy6llf9^&AnAk|L~g!Sz17 zRt@!KyD8&553mGaZ%K}OQ#0icD_hfoTO!Ung>Qg6DJ(q`(dqnA$!m{FeywsfaX;gy z$JKy2hadiA8Nbp550}XERyx{*&t4G&Hge*`4!6U522HOy*Yy~{5Ca(E*vRh-V4xxg!so_D z&Yc0sz6WOjahEe7lFXqqfIcdUJM4WU;&{6BLil5Q8va0g^H92uN|Fp%j9e1cvx{RNH$_cI>k|?`Wr0%;h;vHoCL*&T z3HFxQ6C1f&PKiwfRqNSbPo24vs)WkLepck^6}cQ3`MR8$Y~bZWts4~QG~RbX{NS?i zs_3HQG(^4sz*{{^#zmJLKy#NBnMpDnvF0j&kdn_b~Pz;&S}Sh z@<*3oVUE6N5=7>e&h_9ijgz}S9l(S-hZ&Z8hG+`yDaitltA%2_XdAicYo`jIsCa8d zF_G;9vSDG#zJZ=8)ZIkOb6w5y4SB98_Ssj@ltQF?#1&X#08OuIZmLVi4TfW z{2;$PGdb}jBEAh!G&U$z&i#%tp)XGBqic~n(3+>HF9rGknzVi^u9%BVL=IzPVDVkV zKFMLRk+uG_j0_GRVrtxpsfd%XtU>)_0a%R8oG-GdzhZ}vLqxeQm;eiL{$Ka0Ih{ht z_%yOiUWqI(!t1Tz;)4i9HR1PjKtWuY-}7)3UVa3K8)EJIpFfPR93~+HN*|EjeTn$u z{<A4(5h~)z^E5HKdqV*NESS+ZdcpqS+HyCs0?I;x+nLffC}MRW z>d|as0eL~OK~uB10$PM;-4p9`i=YMNx6Z1d?UTYjy$BkT>ad=Zh$EiW5xZ7J413R- zVR3n|caOb3cSW!sekE4=tI$*~%Dn}-fZ`-P0Ls7v7eiCus@3MN`+@R%it~|If z*3>-i`k&Hxr0ECCiceV~NI$^MVG zy!fbmK8`_1E4IAeo8Tv9)EO{w1A>R;L}FX_T^I`U6TxGS@{aub#FfJm_!+r>)i?L& zR&TlK*;&D_2g*hryBT;so=DO-{E4zYfRlVu)<-Z6`=4MMz+C_jZaG^Wyacxt9ysL* zfIoJgxgZ4V?x!9`J@GNf!s6#_<&a(vbLsHUyL1M~k@G+Y$IFGQ-m)Y46A@1WP&VF? zZ>XpAR`L^Q;fCaA0F#9ie5}sCAFYojGsW1a{hWHmqueWE6_Y#WA>> z#14s!J>?YDQ55%r%mI}#Sy2x(CePe*6axXdz%%rE8V7vodYp`5zH&XbkFwMV>}VQ; z)eu-|-~f%Sw0Gj%^ck^ZA^DC%^28uUI(sZ7f5AQzOj$aFbPx26EMUZ=1JFbqct0&9 zWmu{%eO%x@ZOT#D9hqrUC%7u?=M$!f8VXo&2eB9)3WQkzggJbEbTAv$T@A{#3Z5rO zT2x6&gA))d+Dl6G3BUq@=?(85j=hiKCl5ysEsXXb5h>C4P76XGPYC+rwe2(MU#NJM zQ_$P}Hr>1rvl4eCdlTfqg{cvY|8x_-n$XRn-b#1&Bs&g8TpfydzPH6Cj3L~`w^PIS zcSCvMSMP?>P)=_be)(=t!~uU7q^-OTf5$=C7xiACK`lbxzCiR582@TSABO5`|2*WQ zDtHt4Wm-dA!u#z?#(9-W0)bHDo)2-A%I{UBlB7|QN{&Z;A86tqhX19$@dO^ZtK!Bf zajDmDxQR&AJOx23-x_u)&AP~JKI?hDKX)Ma37Ut& z0P}@lY&Z>i9%`QQRWWOiZ`nb9NbW9y(-*5dZVL1Yt=14=hMWT_Pn?nv(V$WE2v1UI z1%3*m;ir}UX^4gcnLezNWi7bj1ADz^4SgY=OTqU5IuQ7278w~skKWce>h0v`eu9$5 zKs)9A$bE$M#E_m3Ug^m3cnns?%lv)A@Wj#Eh2rl~u6GQVeh-zFIJ!k35{Af)DE;k- zP5eTy#UssccUnBXB&2#|CL_%^Xtc;atT@V_(P#029ECbXWQHX-V`R^p%r@d^0n^OB zc@LgsQSb8DYWuUTwmUKw2qH4-c?`uFqU3JfT`L|M;uyzSS-R|c!uI4{?n~%GsRjp~^m)bQ zgarIKd>fQ&8cZ1iRqA;=2?R+}T=z~w;0(>7+kFZpZZe3UdmItuN$UiXX0CNPqLBan zo9h{Im!r7LQQRmyZj>E2%Kp6@Wn;fpqilkVA&7CK>~Dvj3hHnj@KeEun=r&%nV$T! zsh<2vt|uzBc2I!1ppXWKqZ(k$W0F02a0Wv>b>3%&Bv6UopdfH$ydG^c)Y$4JOSS{QHIOCS^~Eeqmg3Zu zsduj>iv_cFV#pqL^O+_48d|)`s3+-*<^n;45-qB?+Vv7lqje`0NSx(SZ}87219M)j zFzBv@$`zuPPxlkyxFk9X*z1;qKHo_S*wYmIB}j3S0E)6dff7NlgV#~wxEyA_8JtIX zrh?-w0^i_$6u~eSWwCz}MIKgKf;>eQK@{Pv;CozL;+Okd5#&jUASS4&kNV<)Pz=^= z+~MkDU`0rr?W@-Wl}FmS(IEbD1Bm7tf{0jfnsRuC-OA7BAN zaEhirtMDQsJ;6lSu1^a@h2(u?QIA`Iu!AEAOT1DO(3XH1Q zw)$AxioaOuz~~^d@!c{w9)fO1{(yauhz0;NN=!yc$*5%jwm?b0^EZc`{qaf3cP@|! z27!{(z#t6PN8Dub`>qdB@L-%<{f1_SP_jKk2LV=WDDTlT4 zew{jR==n!W_X2=$qcHq^W7YdKMCs%%A_0dZpUof3FOH#57 z`%Fp73j|^~ly2S_=@nHu0m|rH_6$9T9|o)UrSt#&pR13;vDou3b#15@p64}i$WQji zoEgQ*zx7*+UOGUHzW}V_B zSsVa1u}%@|6tPYb>l6>uDVT@p6y(>@DS$D@Iz_Bgd^?@uSR)=m6kb^)o3xOK>}^e7{ z(PtcmXxW$BH&f5{Z>FN8ev~YINIM4*EV6o$4yVq3&N78~6*aLa-Qy>I? zBsPHobn6ELb};;TJbLRlfLolg(NIN40u`eYq&s6 z)rE~ZH5J!Aj}Bj)Jc$rTXJHMa4HkXz^2~W$`S4i5Du})m%+1-kldlCQMZs0h8O9xR zi{A1>Q8h7u=AEi?9phKGF9>m9&s~FT_dQE}K%p&DSb^AqRVW+~-OLd~2zVa=qk$>O zBa0FaAGkGvf_C2=PzXl&LNMjz2?3Mvqzl2>o*ueK_`RY+5%(6+hhGZ5V*uh@^6d|Q zlIX+UCiouC2oDjS3r_guNsIrsr0seN0KrW4$y9vM0Spno3p6#9l@bU}@+aWwEkXH} zu%}%J$2utV2d`whvM<5>9>9T;uAI{Z7hVlOc$T0dLg}Ebk|u2~^+$!=wodWDDVI3o3SW^^?m>ovI$M^zi4)z!A)IWyM6iEqgH&!xW{X^ zkHcrbjD)_8gXV7IpxFQ;9pj)s8?*FGn}A^NXo4qc2jeILL!;a(iY0@TR~)r>|K2Q$bhB`cJ{x2H9&)Zv~Ce3cJ+{;1Heo; zmIXLH?{Mg-#KEeW!h_k4B?_VrvC2@@e^yz)k#zMTI^Jg#Lw*kn?65%I-@4s< zM?YZ(-`|e?7%GNEqrVsxsD}qY&tu`-5&uCR?Bs`KL0`v!i|0iS7Xw*RO<6! z_34S%sM;VYHYgUd{m%vXrse!Pe(NnoKbW(QmPi3Y#arW=AMes4mpMVMvq6Dt)FL+s z;l~wL?ugpMcJwwhV=NJVg;*oy>)xVEay#3j*w)@YgUCml+KENx%o>f8f-yR-T2{PBwsc_+(9}k@5 z4q|{SU0I)>LrBdja)&stpxOufyW@frALnDf%nW_T#T=~R|75`vo^vsWYx(MBv41eG zlkj@~;I%|JR3jxS*c=obM}NM(d+>`T!r_ldgL@AbTgfuyAk-juo*-$_ZBQW;h;HnY zKG7#|-R#?<1ijIYE|uirNL1qK*gOq=?^HQBo)EySd}X&a`WL#bo?Nm^9reQ8D=-vBC&3TO?d$j)o3x@_oo|kk*;5X2G8?tlOzsZ|i-qM{J^)sd zAm$ygj*bT&C>>C|^f~G90@4kk1a1J`nAV5t;=II5x?aRw~0;NXB zV2U?ZX?_)c^*I|gYT23zQ!bve7nNYw;-P>VYOsB5xQx1da9j|FMh)tIzZT~2*ungE zSzW^qs)Qd18g=#qqlw@Haw16Zr$|T|HTHua0JPo|keAoIubVg|)#ExkywKk7=y&~k zepc))e%4P$ZJJgv4L?}?ABv#<`2l?5A1Z=6y++GKnviI14%;Sz!@zjSA2gSQKZpy( zn&nN+GI^xo0me8M^SaeOW_Q~1Yju^T&5szzxkd!5-RBk=ezHLJMD#XCun&&mH|r&a4;dzHd^%v ztNIS@PU40u4{NxxPglEHA02g~x*Hx0EJanHskYAGzgFk!evZNZTv+)awfPWk8FP@B zgTyVH-k*aoQAN2&a1e}p9}aS99Y-}yW9vA!j{i{Wn8My}9n1O(nMA(sg@NY7#)$w9 z$VEq>#2g;{HZb@%SJtCIU_`oSkHHjzr||=W^FbD|BEPlP4XS}~{mQsj+;4_`hnPNb z8~gSpI|<$q^+BMZH*vTjLj0l;85X@?Oxco@phu3I*$=iOTM(dp=T0f&u9hM|et}FBQ8m;fZh;L+g4HY2_ z;y=v28Su-)mfkbCfV4m3)=&MrD2#vN*+un+eplCSKML`ECBH7g;jiMUVGfihnDYZq zjeul;K~54Vri&-ZIR7<@M20lmqjMYuO(&Y_v%{x!{lu6hu@9(lNw6uxU;o_TO4}Du z$(*NU`0t@!r5#_EO3-=n*+P|U+M&50L zp(|q(?6)()q8^lgkO>CZ9S(F@j`>Bk;iM2AM00Sk!(z4J9KaN-4YAtre8|K1Jj*<6 zs0a>-W5nJafEwd{d*?>9qYec7sMPTXK`UOR>VlYX_l*0c%qK~3C7Z`<{myhgCz4S1GK+a*@3WDeL?;}=UksH+|qvOiz! zB_GyvNUvi+o1}v-@=2BsD$@V;6jb01kyL^-R~_K_vK#uDf(_ymHv12H}?*|3AbgKUePyn9#H8eK*{*kwV z-V&p+V>EV*#*We0-x-aKI+gzltO|Yqz*E7=6{E4^xWez^Bm3%TY*b7A#1xCs*gsoh zF&g`!tvuo;p$|o4vwxro24)kFlJxmsR2!n`DGHv(aD-TGh}DKSs||-ubrNqT;uME# zRcK=1T&#<~a#Nj@r@{DGQpKApmQ=5mROh&;puNIAij4|BkpP>Ekx_9W@jo6J#gX_w z1WWwGkpzIFh<|Hw7ydM8igqtIR?cG4n=j)IO zh;L7A5*#ftxbEMN1lN%mb-CC%gzBfb*JBw`&xh-MHa`PL`l{OPNLsuJP_|IyvyOix zb_@+NKu#3Fz$ny<<=ZDnMDYtmv$GFzYt83cYo0XOe2p&ja|2isi(+dmtV$9mlQ`=B z7pL$1+y(akXYXB`6gQIn;jcnW#Ca!XR8_hL_KVpWd%Y3fwI9#!I^w*UP=mVR3?Tko zrn~*=pUk@G1`;;`YLvFy2uYNrQsw=ZnInm#qNRDp()=@e{XGHASuZ{ae!O5mXV9)* z;nR~H$)ab_Re#ycqB*W!ER~?t(#hy6aE8h7UGM*r2gzn9!23tZ5Om~&xbkE(Q-y)! z(Jo5JJB@|q`c?p38dI0Lp2oJD4xZv@nd{IBcLqNt;;15yqZqYXth*Lz=7a@kYXBy! zZCim4>y)a^Y^~Fnn00lF?&JEoEu(cTF*$l2VkM7!Pf1{z+zL@|AgwlAfFQe`lJqO} ziq96}3EZ$obhDge&tZ})GLqg$Nblh?ekRd<467`ATmBtQroEph^WIOrMajVhgH9h| zT->jgYgSzr@IKrU((nQghU+(yEoxjWo_9t^-uL|WE|&5ne}+fI?&06#-U6{=r8E~f zDt9|(o^y?lA&dZ%!Ywp%vERK;(tT|%Nw<+k$k*^}A>`~-$JTDGlx6uup=Vn#0i=@d zX1>c9OvGeKytWawbOZ$YfOBltVU~lzyij|tk--ANo5+>GW=$Qp=yUVdSd_1dmzyx( z=OkJ7l7&31%ZLDb9!`^0VTY>KIZP68?4TbAW$6lkmXA+A#Ovoco@0H)WVN0pqsv$sZh3MZwCE9O#p7*wmTvN-VW z!mTWouQmp*yW{B z$4kAC`x9T(XZ@m(Q>sNV1y3aYUUtb+yQE~3yuo~LHs9`T6$W-YS{Im7%^EMSUFOoY zJ(DG4{rW%ES3}j~>jEpggY|t3FaJ9rofMx&YdZzuq)VyDMfA>+?IO^2$;yQvDIc*>@5E zK;8uMCXhFQyb0t@INY1yUCo=|9_vj|!tP{OLW*2t6wlfLN+3VNefkl6S2+=!raTB~ z>1Zz?{3p2($b~>I1act=`;EB}Qk*8HrtbD300xMB2;@T`9|HLh$cI2agoAwuWm$MZ zA3}Jn4?#=2Q=JI-XxjJ*F}Z=z2;@cR1RB8$6%cmda|Mn)4iTHA?gNn(O$x)%5=cRz z#jy;c(L6bpo9nvfDQB!XRL@&0lYklbR40MzBv74%&f#Ep>XezTRzBj1sw1JO)kAk< zY^y;e+nQ?ml$6gmR)S%*SuCa#tjU!xe1&crQ!^NIY_UD92me8YtK(ubR~gvMTyf7U z)xgSXU^yWynvfq|01LQD-X=;RrGDU94D6y?CJJ)bvB10ota!~zESz$Qbx2{=dpC$6 zoa)%vE6T_IQD79APvO~vfIIqAV8^MmvIj-3Eefx2)eD0AQE@pcE=R@XsJL85jiwWm z2Sso#S3QcT<6QMd+#T$zPnOaAso86xAc)V`P zop8_x1(jnfQ}fBshyDpa*P#!SajEtr)%K#=k5v1y+v$U4C8}-MzcPj)h5j*z5r~0& zcAY7y0I&sJ*^4=ze|}*vkygnQ*tBtnW6qHcQ#u`zA;%m!=EyNejyZD7{cwb1&Jovg z%%O-nG7lGpoj9vZxRas)9SFmMM5Z1RR3n^vNK~7~)I;L_{Rm~A-*P{KlS83_K(H+* z04{zbd&gy9f5D$$6I>~uL%|gauAEetk$D3y%PHWN;II|?>4x*(9dL94d^nx5Qbe5q zcqilbP2-kCT2%7%N|-v^9zaZS;BbGy5HN6tZw@n*v~+@DcRS|@HAl`la?X)+j+}Gk zoIA=ncQw=;il`$??n`NRxOeVhvtB22BQ&$_uA!sDKH!?8*5z3~hncTy^>&4&vR)?B zX}s)BRtoHy6~)K3b%DSwIt_H(w%*Y)84aW5$oN<=;Cmepi@e+a{OjX?{{72ufBfTr zfBM|}`03M6U%r%ROix^HD)Us=Lv1nlwwi44-3i=Fb>?N@)OfOXv%k01Iryyf^taqP z_+;#;lVV5v!lRH6b)KNk6K{?7NrW~Z@_Ad(QM_GaW-vjXY0CEQ2y(O!Dy?))_k~;O zoBjg4V7La-)1>q(^4i`nsrlafTZHcEJbg{^l+3_?kS8co%XMJpfem&z{JEaxfocuP z?QjxJ0Wu=#bq=>EFFnaed}h(hIEItP8md`{=`a9(n?D%~g*hB8lf`NrjgsfgfK7KL zkB{IKF&efC6#+9`$1nK6W`~vSD*OOm+C)j7<80$_w{5)Fsj-@Y%quCNZq|i20N)5s zDFoGEHam{b&0HI+c)n`1>ewFF#ut1`pY>b9^{Rx~GzOxD-*bEbwM(uwsn(e9c0=_2 z;SCy}ZbxlT(m$KlKmEWz+EH^@-Kt#-IIH}~92?U|eV=$R@nGV?#DhERM)w2viaa>O zY`UIJESS__QiDki?s_EC55!q4xa|1(Bn?TlT_9@zUvMByzztAj&hiv>Ms|W?(JAvn8{{{h=Mr) z+WQ`-Y!hZXLFzRAz6DGWysG(XGn0%@I&ShWB%UqIkDK*n5zytR5_NqLoT*PB6o7_PUF$aW`+m4sbJv<4h+?Q!~;U&{0~x& z&j&&v83o}{1T;@Tf6_XhA)*vVCmhELJ&S=+c`a|mTY#cWlc&k>dW!hJlAIcjPd1rv zlFiD1O`2R#1URWwjR|n_=IsdWXrIh?+7g_xT3g~6pFL;%GGWtbKH6f}S)ETF7rsSu2}MXeoj`We|-0G{%Wk}Q5@xD0LE%xFqCfVFR$9qrC)OjgzC$^ zo5ctMU($8=9qn@aJ-q6pQ=Y*0H<@i_-(%n^leLcFl2?k-ps12fJ^nK-!gYXKs!skK zX(6O_ygXj4m1jGxM_gM)O4Y^2i6)RVnqQM#=9a=s$7$Zhbf;KnJz zZ%n?Y;CEHfPEAx}>KX-04$%X}@ea-0CsBMt_?`;Lb+CM#2(K}abUl~5!^NS%>S zDeVjlO?iOMI*X79)K~)}A&GRagp^T_Bqh-BKsX)OFd)fXaT%sDuzgDi4^tzg;bjyT z`K9BoIW=R=>33{TdCw0!?RCmaZoB%H?`02QwC0qgsQC0wzp`@&Udk6V2%!X$CcFr!>As;buUynroMe0pAh3>RK8F92=YtK|jwtf=>U0VM`d zVgO|lQ|bE7bssz;F#zM9074F~lnN&%2lyF?IdG{C9zn)B5wT<3Pch7lvGXC!egpAcBLmLgP|Ri=a+x7ZjW?|F+pJrGT1$NJISGaLY)<8?F~|GA9k zL&EoV9f5_8`^GGWb|J8W6q3uBDhELTECOIrZB~*pJE~+DSe)5G&6gVmU=qbyh=V~1{OjlH`l`vY}!)P&E$Zo%~e|F#>-}$#pMg(;vGU^@~8PXYOf|9-H&!Qu0 zK~HqlJvz!!50a8I@WNy1XtQ1?b3;X0;YQyTsxUA(G=R1`UgkK9dYbvVR@f6z zzdwEMef;$4r!QYh;U7J51J6KQz{2c zc_`h{&76VHPjnOP$U_M;M4BCX7W_xe!YL1>y)u$eWk$iFhTmu9prch-VwzyIr zN|+(W?7%ksno0pv9txELesgvYfw-E^Ln(9OGxAU{h<3$1luVVsLLN%U3tB&8)B=PO zQ4TRfKfFP=8kr1aGBlEig5luCOa|{(i6|vMD*D?A%1BVgjs>~s# zm=QJr=XjKJb!UQC=;RbFVS&X?30{=oMG0P{y^(L=+7-5o9^X2}?$iXY&^+_{7fP6Z?+f1}=~7anFX!hXnH_GU^~1Y0?>Jf;t;6A5w>QqN5JdQARzI zlum%lm*^-5ZFDSVTTTG#;f>((F0(*$wwN!f(emYUE(?}dGu0SW9`C0PE1yO>+85cl zGeSyJi1TU1=eXpGbmibmBgin3RmaE*MG`=UPcxOaA;XTH>m%%>5HtuQQ92iuu5Y1~ zr1<}YAj3_RuICNL|Iu}OhRgX2JjZaoL7^fq_hlSUr@fCo+v$CdpO$gFDzfnv9*=Bi z6h9d*o;Ckvo?upA_}p?42*yFldYnpZiGEYGAtm7AaZnMM6b`^dA zvUZ|^3CCFjY_~D6*E7Ej)pqO*S>d>8Y7IfP393zCI)ZA)U59FyCEyuQZ4BUC52`Kw z2-kvYJGN7>wg{?CJp&Fi{|TyH$uKDq=zgS2g+vG%G}ZiDLA7^5N*z0^k?A-DDcvCt zR`Gm=({-(CO8=Cv>5IIoUerIn`#66!)G&&p?1D>hhs7_l0g(Rks)JzqJ*QS|U+ zMu2LQQN8a-XVOFkOIxkNX$)R2V{fZn(n^+OXTHNBaGFhKo7wjmyw8*MtK8R!lbNIp zNtq5qN(-W)P5_+u>Wicbe)Nyq0E!C35?U@XjX z$?N@u#=5y2`Hb%=lO*!B!JESO1j`FAs#XxZwir~cTp->YA59(~V_Lk#qa4dFgg+FvR zu`}>kj7t1b@Vj%tZ{{*4quwsY>zEU&7`V&MihdKckw~d~q?8fQASEjS$da>2iNK9S zO5G!+JoyZm=Xyw1=aLek8;O*^fCr-L)O7&m4!! zlo3RW+LTc+|IVW=%oEK;ThRS+XkD?lvBP;u^XCC+{)Ew@#14W06Gp2VqlGU0#;k^RRm|ckYL|JAt1Js@!}}AZ7#7VGE)!s0>*Qs$QKuuNf16BUE|9O}4vZ&HV|@gdQj?h|ct>w0AvAky zloEQ9yHT;cYHuXiWm1;!Ht~dggt?fi1eIDtN*%@1&aebaFdI%dYJ%Bx4rb#7eg>oV zu(9UZcr`3}OEtu(hFFIQ$?;hO=?K_njR=Vdsbhqc($2up)VawBoDkHc+I3d7leq}Z zsCH=6kwhYi)Gb1CJ(Ya`WaRoXqlcfW;qAynz?5_y6$r4U52Yd8x?mJliluJp%C|^0 z1ut3j2|cb&0gB3<*DsicAX50K_ZtWd5?8-y7DAa7pOJ-t9?&ah zA(TUw7tKNtqNw*%62h(Zekv3z5n{$shNzRgg`XaXCIDTb3VtMAuAjOrUHOgK4DBiu zE7C%-9%qz;@MimVQx1X>711o&%w?Z)BULiIBncTZVcOfZyX2XwlRu~CARx~FxEutX zGjMncLOFIv_(lqXcPw$Jf88jjyg6%+zHUKH#ongctE>w+DfYl0me zQPTVg?RaZBLTE<9*LN`NK85Ufe)t2YMo0=gnA@m z>L4+3RnSpkA34)y=}y-7cA&LX6AEdIF~K;MrX#o(xx^^0e`l?fcQ+x!cy&{O7j#5j z39f}qKV{kAHY2>&?Gd(w*P`-4gx6{dUaQQE&%kS;PxP92t?-I?Eti*de*$dXT96Q6 zivU{$*lM-iAYJ>7*$sZH09$Sb;bqLH0(FGgBE%L|L8Y{lTOhVd{;4_%bSh%Yb&f`C zF@9``y6NO;yndW+KyjH!@k_v#L`brx^NLs=P$Ct?PpVtt>Wh{v*1RikuQX*^IGtvKf}7hdF?C|2<9vYB1Z$6lPVva(EjHm>_sn^VMc1waXvh z!Or{(iDwJ*<7Pp}C8F$MG%+_&lO5de!FMlT)K0$W9g&WAaCh^%Gvs1xMCYzyg&B|) zYk)cd@xoT}LZ`L75P4w8Et?o&cNk%Khm0@-A&0P>i4nF`DI`Wn8ew}Fp>scsP#8v- z0mzx-*vJWmV|fs>BlEaboUp~_v5#IFX8DfA0$5jM9d|(~bnj3p%<%t=aog&1n+)W3 z>V-jTxgickq&3E%I6SptV`zKi$2}>l!OZYJ0fF3VSzdTKym7T=Ro5&%H zeVL--d*HorJqz2h9y|AJYwj`bYyMh1_knxH)c+S?4imVs%4yF|P;;I__b}-i(G(1( z8(suW_dy7iX*^ol%uHrlxDMtrv>g2(`QBQlVzt-z#yMB)2|fs!(h+-8v|Y)nNe}`u z9FB2XrU_7}0fc)QL=R#R9DHvcw?g2KhQuA;zfwfPZ7p6eH+j))maLM^au{EaHR4PC zWFE^rAl@!$HtXd?E(bX*7O{AoOs5S3+%#qXL%&;7KEup1E|4H~!W^RR|1F7@qn_OZ zwZ8Xpm&X5qC)jW}GD|dtPH9~exGvTeEh-@SxZFZslUC)+-0x^r{f`veH= zUs!RfCv%+V=`dQ1Q#CY=0dpJTcsSeKx7-Ho*&PcVF|==B_Ax+yrL~@mI{Bf_kCoue zx4zhqb0D0w+KqKuJKeAO>Gth1AD8ei2=#Sqx{vhN7{O^fb$&A2$^2w~<|hMKfYlc^ z@D!MASl-)=MSp*7&T+;=3Px_ zE_u}hQ3+pB$j+ReGkQ!nQj}iuVKzf;oGC|9qrTbQ-e`}cW$Ro<653x z@@c$LpT=>8H*+g-y?KbuI!tZp0HOQ~Bbq5s`$q7 z2#*RiNmQyyCN;uO-D1r4LW_w2>{knKu4%LBe%)lp%Bs{C7J6*AtU|SlWEIIOl2ue7 zq7AaDrb@)Qf$|YZgIDR%)9evlJvvY$Kp=rv75q=w{gN1BJ~D2}iEB`OM*&=?zGs~t zC$oZ$K86E}1ctP&KB~m{9Uw*8wI+K_q*x!z?9uoj^A&Fzf48$jSUb8`{Ju>750^R2 zK(p0sZfTzvO!4FG@NMEv;~@fqtVFyOuofa-BHmjP?`j6BL#bYAkj2mh*d+l*dmu{D zdAXaxbXW0wh0}E{FxnSyLQ`7RCY;8HHGif?&nF0fnC@se{&NXH8WiDp1hNg8uU z%tON_QDrP`3-WU_d@%U*z~kR|#>BqZ#qN{|?jD#kq4r)mL7&?Q+;#K|tE=W&5oWkb zSYdBe$bQc+78vTg?*-$&5A+p?_qM~ahJ0n1h}VUQwt_*~?^1edE>HxF;2~PYC8lve ziB_O93Lw3m+?XntoM#{2mj`?^lF3%P_l`)lsrM9mxSEg3+WD$Ec}-?c>5S&2E&{Sr z0uoHT2aTF%#ba5(9SDdy+p-rzWzCn?CgDrMw_C!OyUu&h=785_;d?s>nfpeCZ;p46 z2=0yub{iAHp6xd#f-%RVy&^cC#qd7BWYI$^Nt|MJ`yg6R=&YWf;t>li;#WgWR*pvPLlc zI{Fq%+1?Jm#ma0e#TK@KZ<#;4C0%z6c(cUKT0gby%}(vlDfJw5TlPf-SVhtXNwo`D z5zNLo&Y>+(Q@$)CR(oU|?EfQPL=(A2dKm3rP7-Cl8}t<}FIaDdW7=TH%vlII?>fXV zIkWt;oX9{OaVf979s`C6aa~x!6KH($VjKuL!`>|%C}u-6*~$xV=*7LSuekp!?pM>B zYt7*S<_NI9SeXT#7|kb~U?DmA*Jz$Ta8WCtg<^ix56_zOFgP*muZGFO zB%K7|c1DjWT;Xy=dx76Y)+f1`9n*gshD>he8napUpbN-Na2#qUb*EPL`voszeKYvN zc*dn#Gu(Myf~!%~lkc!3X3^@KrqeKo{c!{EgaY;lLXcQsqvH;S%oD06O*t0yc77hO zmJ#u194X!ULa7Iu9Hhu;pStCR>==y;^HFO`5e}n8z7Qdft#c%?bD`^4VQ7WojjLRj zt3vo?yjcSU}QFw3_NCJT@{Doua-&M_3NTU049m;+!zm?nTNXwW2Ot zHIRO@z$iG}*aJ|7iD%(LRg>!Ypl!mR2l^ESvr@5OrSn$Y^9u9Wq^sC@%hg^zAVe<^ zd2_pt(f%!YDOHwzDI}n+pK)Qg9l~>#X$uUA|M_} zJdk+c`N6Dv=YifEYq54#zbRBY4VMH>5&qa|rO`Xn-s^}66UI^U4bXyUk9(-` zmP=3#L04YJp<#<@(5UYS0rEGOy4e~}<`rC+Sn2v&a^1zA5~(b*jq-|3KS_B#L9%C^ zXKm(#WOqoilf#4@Cgd>L<1i8S8TUt_!4xZOv8g=4OJk%tFD{dd7Z5jc8H#qqzPVz5bp6!cor|cufJR z;A5vJaN%(WTY4>`XYft`9<9fwT+2GKaSp=lJHsCbRig%S)Zma&bXFvJB6i|h!DgV%n;0-ID`mmc;~5I09`C8l)ggc3o=~*seetyTl3UgV^|gr z-dbOkWvY;XtSkYsg5vIRmAGd7uyMVjyvCH%5BqA%%%31=ZQlHj46?Ma8)!m(Y``g8 z0R#s(*74j5GjrDh;{7DAhYayCSq|YaxDEjw2fPi~aJ-b=BtQ|CoC>c8cb*Xu7DuA!%bS+ieuder0 zx(|3l-qz)rPr=5DWC{c<~l;RvjDL9V>N^z=&)h4rh*s!8Btebu3 zGB#pYUB^||@eaF=Q!?`S1x)pP{KpCtJOK6)Kw=Y*`>*)Ni&uED@~eB`kNI zx2WYy4XXwX%%Io?7&RkYE!BP-6ugDifzaB*GQ`xv-IofB_b+eq(EBe*AHfP&#d=fb ztAMYp;ubJ)DsLgg1t>`Ye_G?FDtkQ89lLvIW}Z%s?BGll27ZdbGGcPX+V@m=&dSU2 z;$+H4J8&}bR`sC2Y=CCffEgGC+-43$90CabaV**M4Dn%QZW`}^i|LLJ0Ly+_f>}9Q zDb-!_*=3T<6m$3kw*CcM@4@<=Me`AU2d#1TFag#eEl!MeSRg{Mrlu4>$;;~#DxBO6 zY=p#5R?qPoN6hTS@5y8Y5$o5yrFdA^7sTBd_slWeAIovbsrH1g7j9U$W{?1iYsRY@ zSutktR$_n`N5(7EL@tUWhnVu5qXJh{UP!t5`XHE<2yif{UMT|bz6z_*FVT3O3kXRk zxgAdF&RlsyRR|8pR&#(J_*West-iIO9E1xFodeD3LfqJYoif9zVxV|H2A_}-;;_ge z4!wOh7bq$YiwAI6U&pkh{#`w6iEW@{*gdS~QogN+MXjQ8SiFhCuvOd$58$vKH^T;X z!xjT@F4mveJ*?8Z`ttcCldD^`CF*dojqvVaH98HR`a|A(hU#(sA=z1V;Z90-Fa|Sf!)dyM0*wjVX%h;v6`-;6 z<~Zk6#x8*vGuCr_f#lI{&$+MXT$PVj6?YZZ&9On<=wvmb<5PL2@q835ch-R#FNf)2 zHPe}o9o0EZ%;gQ`oxh#6d}_g`T`IDhdeMdLO6VW;P$wNZ4S zjE><3b>zP zi42$q=*1R>sF5JxW#b9&73^Xfe~+gtBcY}VEY-k^L`w&Os?`p3pz2T6M+vQHQGc?`yX z=Fw&?D^|p#kHcZSTIEG_q4~|0fIiAM=%sN)-{%ak6yS+%)<7PS+D#cd3 zEu87MI>nBe;xu-Oj$zP3(KYP1$2a_TXV|p2z&dqljo{G2m>o5@I6U0H!*I$HXaZS4 zl`JQ+oXB$eQ{mR?jRA6D-M{7JHD@{bSGAlF<9%&Zo(bZ5Sr8Wl3aFbD!);0tGWZX~ z+?)F6N^BGSqhVds&oTpHx=GhOwkz#VyPMMfWTrQ3n#Qrk7igc#E^TdE@xeW5!DGZQ zYx-JEX%AY~ftmM`a|>-KX-BnYp4)9hjidE&{QR(4uK~Vl<}~$UImn82k$qeOX=yMl zQ`7Ni)F?r;L}MW+%-pVvL4AhHfo3TWYp63U{oyoO6?UjrV8aCPehC&2!YYj`{8@T8 zu!bMv^>ZB0QHe@c>sd0|O!1!7dw59JtJOH!Oh@11cv1LiA$XvpM7z3#c6|YDRo4M8 zx*h{_!-_ttOylF^WwcSJBc*?vOkpk%NS`|}o;;295nM{mRq0pL@K#G`!!D|Uf0WgG zaQDMqmZiHVnpm__ahPIN`?HGQ9|RAPlvs9jVlOi-qR}glBHAU0$PFPPpYbhaC=ty* zr6L6j5HG4+9PrX&&}_4dr^iQ=$Hy4>(YoS9LXJ>zw`o$4hfT`NyF!jsu=;wm$1x5` zw-(B=Ljp;L(1<*U=A$iXkVa}9%rL>83Lp(AE#hX<;NyIB8e-5cAqI)Wa2!YOTCPV&}j+V8oHh6UQ z5xn)uY%}|ggOm_q zBfT>hx)(ym2CxjtJ|MuubR&I$TDYL)Vel->g6KcuS#Bg+T##-k;e1goS<2^JRo;_f; zc<&CtrwUun!0{P;W0)`X;c$JeOiUT}j);kfsUyVX1U`}uM*{K$2O@&4v&bliu_T#9 zGO2rH)QmLBAhSe9L`Gd9BSSg^O;EObrZmcO{fUf-jJiTb8S&IBb>*y%U(p@f`!9G1 zCSd3H{sDNnQ7ltyid6Ra`1SScv)rx7EJZqka}!fhz|ZY~KjMT055MeKU=01{>aW%L z9n9tz%)Z04P|e9apId$I<`VE)n!VJDteWzy{})5GzfTr%@1tLwa&%ZN;6P-$8Daix zn(=7sT(QO~0}Gg0^VjYfK6!$JiU~G}Vc04uKf%%ss+^cK!G}z+TQR|=SxFEUCm8s$ z2aE%oYtRHARnhP<+5d;SzT?U)H~5+0e$Wo=&H;&vkmkBtb#E3`p7HQOA^`6OScBl{c-MOdY>(VEzqe!8OJ{W;>nI{ z2*r^t94D*ic#VUlGygrAi~w5nntR5B^k3U%nHE{E3g@OEb)?DOk~jsPQ=Wmez=se= zOvjha!@rZoAF8L|K~us!xPDlRdrg^j+QFdT&8l( z5e;3>YD82P_Nj}X3vl9+uqR>P1!2zt*C53`dTx%C_p8kUcyF|bhFI!E(UFI;&sZ5= zVD%wv18^o?)V3Sjwrx&4v2EM7ZQD+s*w)0!Ol&((Y#THA z^M3z#PMtc{Rb9Qic2{?GRaft|)_q?~q`Z#pZaa<@;fa9p4w__{^bm>*LUVpYWf@oN zw}OXyGH>cmmy|}iP(hHA&h?nhJ@!`Y0XVa`Iced12{vH-RMiRl6*{vyk2sqMX7|v) z(*8TGo2ATn$SlK0xP{$MGYth6RyM4oyu?>B|79<uQ zOPVC4Z`9&-vWhq-{!_H(w{BdRTgt%($;7I5_QkRJ+Cd$;!5>r$Q~GSt8p^5*O*32H z(XftKUAT*T;)z_Y$loRO_Z8Xy*-E$if-jC^e z?U$tm`dJGi|0$PcfJQ+L!CYO$oNV_H8k7O}3DKU5%-yc@lUJm6t2j=D;QEl8l62cdgxW02fV=3s~njn%)QZOAsnPlmc4=mPYdD# zApPP0<{$zgQR6)5LH@FWsyY+QyiR(x7=U~G;f}J*6Z-}hO4O*|oTOCs50%WL6Zvwa z=8t5fIw7PIj&Zjf5DRWzXUGlS|9nTL>gla?@Bm1GPLsR-lo1_jlMp`GPK!MArNv0UFCa<;p z?J0u){$MLN(1c5F43}6ldBDXS*#@?%5O=}ZT4n*|3^%i&Q6-A4V&9wEjqa;IULA4j zQSD2IRXAn+!(rRpD1vu*w-2~Wf3H1bu4i_E)pt7-%n?_Pl6w*|Ki%G383vW;KlHHv zv+(J)mP3<2-hou$;dqRCEqi0NVs#aT>EuVn9a9*ncY_nXbs9p}ePVW+BF$A>mBF83 zg4Zmc{0qjd+ktbY) z!{uKD2aCPg8Xd=VC>%GX&k;{!+)K_0S1#(G(&FUbYD4UVV7wTxl?JEMYG7X^WGh|5 z(vwA%!=p+bASN{?wY9J}>VSJKnxx8OMJG~1#Rm70z<{O>Xd&1c z6}`pq6d~gvYwf1ypdi7p1i7k0vGk9)vT}L=NGZET;Yp8Tcqx;MyQ2a#k7Gb~>0Q+1 z|1n#e7}^(KWa<^$CYG_F%qpiM7wXfy_CoV+Y^lsDf&JiShQ;((R^yNj9SPRn42$Vo zS{TvFF0cbt0Nvk*cDg$VAzm}pgcck?fw(1s6r70nqB?L?-R=g%2>2 zdA}BdYN&1jF-VOeFNV1xdu@`%v6O;9is?b_BoZM@Vxgh{BSnRYcEYC!IoQc%VXDRs zCkM9GaH~+p_3~kj(2@kRM*$&#C#1%5Ls?<0u%>7u9jGG#PM$;D@gWX`Rjt|VW~^w{z530NTF%VS3g<|zDIkX@-o;O?@C-fOBY zg{N7CErO#|S30Q9)y1^J0K)T**nEe9E+gVAX0xDXK_E1I+Sq3LTiq***rdcT$4E;f z;g1E|NTV3X+mWXgly>*rDWl2!35U*hiKD2pGNrT*U}<30Swi(mNSz{fpud8yzAqt_ z0ma&7FQnE)qE9l&;T-N6XoBD&jlSm<@xi<#$=n+aVl;h{wm4B`?GYNKh;_C z!8UNBKE^=bYYuO*-2-6Yq#m+KysEkOvNW^k#!zN$oWI+Z*ELHG|IQRTesJfJy%Aau=952iL4?()UqA3G zQg;jIFK%GP@;K8$e;z_&YUAfoGzj*&I*;0*3W!F%y;up1v{Fos=urP4dThQy2+aR6 zPlrsu)JVE1SltsjRH?9Bnb47%BMvpj0`8EMM*$K;A_I&5g}JoF=q_YWHW zxmLxRh1xmC+`J{z?<>;ZB1~j1sM`XRplnbKnkdW7k%=|G+c!>$H%EH(4Ckf6@rC3) zKsb)pgLqpSQO!mlCR<4M({-7?+NTz#oIwS?qwDs|@e-Q=c~dQ2)Ah46eZ{L3?P)fF zRlm_%hx1@LfuvylBMmEX+5P%+?IRnEYr2yYOU4BxX3sJD2wp&(bI*uQK;uDDa}Z|c zgAE*NK{Ch-%Qgl=9*4syB($I<&XyXzYxW=R&o_WhK-z3q?W>aOa@OO(M-@7;b%oCm zlVRU9Svyz;qU=YtXybinX|B~rClH*o5X`L|1pWAs8sAB}9J&Y0(B@<0DA-Ucmt8yJ)&FH`OF!YZe#^{M zx{`KCR}~?^M!KUSt=HSb0UOqCtp4WaZQ;BwH9SOh_Pgf3hc3UXy}3RHEi@*rVeHp{ z@@ddYw19S_rH*Zv zuhvd;4;rYETgjE}Lx;r5vgQD8%4}69lT*>gHl(DL0KY1rV_$TJv&77pvnS{;+D<)4cO~^~5S#uSdg0P(g!!+UtVv*e(N^ zWboM-aXHQ}5hSqn2_qc6LYjm3kRJo($+EQhAt=}=B65H{XsJ)(V6>MpsSR*H9_2_r zmW(vWZOBZ053Ldh=<)fMYs8OK$L)g%-udk|*KQGhp)v!a&0v9_Af)WlK+65?Q>7)} zh^wBUE{p`c^+`-Qzg1R4BKp7eMa5&oHK(9zb_~fV5q%bk6kCMZ)EN@=45bUgM>m?ocekZ5d~)2><$sSP3vy%@o_LUh zBT~On?Ba|}%|`N!MoPPy6Hd^ws_0Y`#SlnSrGFD1KyU zwW%-!q1Pt;o1X_(^*BK92O_$v!o@vHZth>bg&JT;`B%}I;9M=2>!lsDIr~X)ZC1Rv z6Hrh_pG@=q43|4BvY1y!O|t4BOP1Vai#Pc|$NxHcj8yWzOS+K4Md~nV?5uNa{Bz8} z{dVDQaK>3L#vo;mNAgrk78l;i7$ddl3*s;Jso*%_Y(pBKFfe%SF4$U$KXpHw!lP1! zN1e{mSUgrEi?7?%rk`SR+)&HbJO{Y}^Qw4sw1L@>wbcTlSdS^^B0Gtyqj7e-iB)d5 zuA1C87Uh`rXg_PZ9`iSXYr3xhLZvW>_Gl8Vg`drpK|fwB!$1;sTMU}fOfA0D3O{F1 zF%*kuy#^tphgSNUF;PohA57&14o|Z5rP40zIp506ds-a|Up#K+H=xPvZ)8}aJsNTD zxSulRgDA`+C{rHXNb(qUvLacb*w5(CV+Q#OnRBP0tVcp<7AlM%<&CUwT(ov_#-?l> zxl&UDPW2hAalzKZEH{ZNUs7XAL{RBEMJn)z6a0olUgEF^V2zOw2(UP4EEGmk14%(V z5(7q-zs=d$QYdW?4!nbY1KTlm!1AmJZ#wcY8Rzpeoj33t|p1F>|slXVw8*gX}$cBw)jZ#O;y&t3mZgTtY$|K=1V zP-u*{bN)n71`}7BqHY^#_4* z&OzueV>WJyfoG`iS~>3wphQ0^U_W)WBoP#K9O#NI?2fAH>OmHq6a`xENTMk1j0T^g zMA;u}VS7NY)W&WG^}zPvKQN8%qaZzAXld?Y?CUvdjCYcroI`_;5mePLR@q(-Mdmgd(Yju^M<)To`Q>y&!` z=XE?7_iNqYuT?S0s#TI*G6dm;^6Xgp1^8F4PPn2LE-)iHM?DxPgbSK8{{o>px{Sdn zR2!tuZ)k6Uw#n>yY`^Q(Zk0w`k^1%@5MVNJxcW*!Y?6`})|Ji;eRYBj)T5ZlRSw$U zUgQ4$9b;B=+=3@g{x~#QUsKwDjCnwCdHOE`*K=gwi{HM;4^PIlRt5Zgop<|#Tm#7m zHGlWGEN6Qti%du33pBdo{0IB=N(!z-E=+cQKjMAC=KHP0bEygr# z*{BE_3XNlFg*DhjEN0Xd$;F>^y|w&84M0`G{E|(JmK?Q~@ChhIr7#eJ7Hkw$v&de! z7DKkgBd>OWwa#rN#@E0Y3%`_f3HkhRfw7ILx96+auw5GnsXo=wL(&Q?WbgH}p4&l1 zX|X&!qK^a_pIk-SBRg7Nkv*4lE>7hYr5bAL+u{IRjoD#F`gNIuz`nUO zIH;yFXZm#=PHvYd8s~iI-W~1t;%y_6HHvfr#xiC5^zM&{%s#`k;XM;De(SRZPUPTa z3z~h~2>*rgSS2x!#~_ff1vwC_QGiGNcl3-aw2z1@L{`xsToV6b2Br>!7t(@~XRcYq zE_Y|-Z}EW?&g*H$r&8I1Sv)f*z`PXQubBBua&SgpHow-t`59jxedy3w+>J=`OhjA6 z(i}Ein-$d}5<_=KJ1@v&@=&LkpL7Dq^BX;B3FeU3+yjCf2cV7P^AA z*4q4h&#nsbC3FgBBv6J#@alO>OJ%a*N637 zruSM)e9t}f?6b6p03tsdbK|m!r&M_%^2dzk@c#Co`|=_({{Y&? zk2b`~&zQ?ESV=I*6Zm&_e~&vyF~aBd@?vH$yDq|VieG3E+vpw)f#Of_LBswY)KZkD zK;Ii2+_;$*k9^$ZBn0<6WCPiux*5AyC)}{Ox-X`Zic{TrZ0W8C@^z(V@OQU3{`=i(u&)f|e`!d=+8M{1gkHd_~A<*K4tJpD?m${uLa6SL!#vmNT4M^>N|HFmSW2 zURZI@5QJXY4(u1~4TPOVRD8JN&PE>iL-H(3|9U)DJasbAtGKt&&|2)lYyr=!ebDQ^ zCBkxFZ(SW&rLM_#3*x?_@-2YGU$^YB&anJyND_=WcHr)?gfENJB<@&LaEbVou zbchvJ6qoeH(r}=@qAay4?VvZT_5|ec7iV}fYJ7?J)hWKVH~UOK5fCR4KL1!;T_TB8 zrOcLwOTr+bRmuQMeak=r(LDmey&Z;rTZ~EG=bS)WQ}$=&rc^Ao94jx90Lv%kAu7++ z)<4~?h;1K&Se^T%-pC01h`9zik z*!<%UFsxsXz1ZFFkKWk!OPcfg6lL&maaV|ON4;tHPwGBmO-*OPMDy0W|aiw~#Yz-Y7qPYOr(o_${B_?}P+LBY4el3NRvPUnxjxAp% z+A1eI3~+z?bI|uq_o$K~m4Dhurz9o*UVBeCU~#86;HM}N2~^A_@x1uGO}l0rh}Fzp z;5eE8Ew6`1;X8}D&@E;Sw$6NvH9m%2OAWkqV`D$Rdu`z9f*W<|_Rg8!3MRYj2}CC~ zZ$K5mVEP2ZR4Iu_Dy$&lv!jvfMECTCW(9;#@iT2+a*rg7CjYR?^5G>OJHmD zT>ix_VnYY%dGtzNIg~!n`=pJzb@Bg0pxwIq(wId#`WOXOt$99?ZmHg2M!j6m5BKkk zNK>JS(Nzh=gfi#0ujiD~2)s5m}<*J?Fz(#m^F-HYxhA3uz4T z8iBxf4q0s<$u-ZeWESkRFm%-Q+pSmXcXxQK{JsIh{jTTyg~1(~zRs}@`Qw%);g1{- z3JJiS&p@Vv#c&0tzVX|M&SqyJPYUeqGq@K~Kk?EFsA)s%uF0cGC?MWvh51-MCf%%C zAYAJ9%G-|#k8WPHkB9ja!iUfOg&G=;g6F56zUHJEL*~EgI}piGKvrQk`Zy^Df21H& zRg`4AcRkVOgs<4f@=KwLZtFWhg;Mf;XB2H~8NSBJ#4L;43S~}T59p13Xy(DVrscti zTRl-Svi6k$SaC!He zsVdA~U9B-YcaklFp^tyhYY6kl21AFSRjBJodu1h7z%sY`!z*V1_c}?B+E9$vq2PnM z&C!KAe7m|JgT&);G=L*JTw1)fMZz}=h0N6gszgs!G54Ta!I*PSe{o%u2}0qSS5ik) zZkfNWEn0K|fdX$uYKjG#YE`4)EY+b`>(2=>9ZC3{Fg-G}_q!>XUg~De<6wxB4ai=< zJFoH?o-bK$H@=tGGDv{USx+LzyI+es3Xnhkxq6@YpRV%V(3fd8Ggv|!86&FVZ46sT z-U?{MF9|rAaWCKO0{&l8N)9PmMf+UO05Yz)=e0u;VA?p>qkk1kPixDgZaT_B7+S+eJ6YDirvFBjUx>*hA6|X zbb{hZ>n_`Ab4$~e3#lgN?P4{sc;l_ji+$DW7#PyZCJABZoShK;8AB5w*HVLi`6l(P z!1^PCi6sz@3C#z;>#VE4eQ@XqQcY^#IL^blC-SE=KC+6M)2d9tnooK}K@SpRLE1%k z-@`df9FUBRPC4M6S=2i{J@UdFl%Lrf`Cz_^m&6k4^h&<1$?mUAFROxlN$rLgL8>!T zbUy!u*E@#19uS>^Y}c3KdS`%elL!hzXd&IEnesGujEI12%BAlgkj5a!W$|09T^-+& zGsg=#BI@9t(0v7Y3RPm-pf)eR&{3cjV`NIDCBX!(1r$MRZySf<>IpTCHoNCcFJ(^T z#6`@zmHt>3;MsGO=hAMiTua2sY><;ZNu0; z$W8C7D}SKNFAdc{L3ZWEgph<|LoIqw0j@oBG3{a?%Z5-h^Lhxn;t$rJS}( zfZU@1Sy1Y@CKRQjzN7VtHn=f6zY$GW@HR=X{RJeGww-iz7JJ{N=lZo1DqLOYF?fr)7pr zR}%0llQ_k6Y6&O5MzzOf`_sbBh)I2ylX~l1eY4t+lmw1xPFXIhsO3LON&!vE^DF2O zhBX=6RKf+^)!RRgq6{BL(`0HSD`h?8G!@wX2E=;@Ej&qc62q<_8xTS*l1$c*x%3DE z0iSB{42n7&8HC94CjkXJKDPqfvah!=X9gCG>G^{nSChD=PpPwM-fZZ}v;XTzc(rFU z>!rtQK4P)X$ZODF^tYqGCy6VC^LN_iP+A^)oNK@Y88CPcd{WxwO1W^3ITiT^^6*k8Y+? zZ!c{|<-~3wl>|}TkhD|ftA!$jK_+USG}f@RQ)lM6UDc%~KTKtYEcEx%gyePCuk);n zVl*f(nz{sY-IHuCZKOm~_DAz>F-0-O7ez${6sr;mjr4K*d`ZQC?1C|Up~odq9~b@l zvJ?Qo?n25ZM*_LY8tU5I-%WraW@IlA6he0xpJ%u635}b6&d_-=KwBQ*LxKYPB2aQRMynL--uND)=ucj7bM$MhYuatKhhQEdGA5Nv%L%SPHU zOZMG+noi8*mwi!i)OgxG>EUR&J`YtbNLZLbo}Db#{xgA7o`vq#pAs)PyZk-wKY3h> z_}eWI;!k6?8=wue5Tq=(ARr@jEG5<6g%0Xosm=NHdCF$!BX+Kw7U=Soxzn7#s~4Zxwe^v1|9kyU1f*i)z8I>JdkMIR)H$R zcGY+*@l)V+r%3N$(^Gb_+2_VxjMjm?6>Ej&A(Nz@LZ;n3XbA*A4{y7O29J6wkW5E? z6;m@;D{wAFwfc&paZ@IMd?A||L0UKv3QlI#$c}K=e;zPrR+l#xCtn`5`>Ct-M3+~5 zX?9&}YC&ffsy<6WCI|I~NY?h$CN^+GLy@))5&o4p;2)|0&`cbWw8fapM)YMoT$w06 z^fMRkaO*3yI)!88AF^$?I}H%BJIrT@fMr) zD!(&vRr0k#Qdg(RuJOj+yBtlM;Rf2&Mwtuo9y>*}ilK@D z$`t0%T+OEC(`4izPhE3WY{qZaHB9d(r_u9c#U!N_#oV?@c!Mc<%OKbvJ?mj6g)Bkm zP|2ro=T*3@XhEiMHNxNs`2Iq`*M8e+v+QYLGth0ljV;B1+7Ma6b70hsU~D5&8SY{K zaCp%%3BR#?apm(a&y8;v%n%7QyfVp_M4k*tO-w9fuXPg=_LRbH#f!Gh*@^X$d?n=; zj-bhn-I2tO%E*HMcIcmPRS7>gR)!lenyKTqDf#ZhggJFg1$_W{ydTn7xIz zA7(KOXm!F%%HRUi5QQjLuM6WKCa9CN2ux1Ss3+(L+aOZ$Rmy2;J+aceAn6`x3nYiC z;fG{c6es8gf5QT7ELo#j{*)K#rrStg!kj5HM(1(_c<^ly1n=ujucmY=Id^$s4f{sG zR3qf$CFF#}^W%e{A!#jWy{8MIX-BJY>aK!GUn2E)AKiBEleNuzy2~64dcyVYihJjd zh};DG>lH)bPRvZEmdoIiKnS(4G3Z|$|EMothD8F zs<|YIKsy*C#dnf77F}-j#x~p_Gjpqa%0Gn znoC`>?Nvcmb)bBCj=H{^w-A52Twr)RWq}O;Q-Rlc_*|#rBo2!E(X&Hvz6CeSrt;ry zz4Mz4&kBYA;fQ%{_1CptzxqB9(qDj#T{zQ5w&&Kf zic>DrhyrZociPFuD!9Diwfy@#5AuFH#FugxnQq|@nPJVQ99?8ENW1c7UHM342z4gF z8SO}9e5&@9htRwcf!Y<5NFNe0JU!RH#D&&?m1fc?NezXDjsAs8oM1;OYE}E;wHIcfkxX=Xyl#Z_cjfU(=u}SF(a?S#P-uzh37TkD)%K`v7BXM2 zV)*EtAUw%yzKk^atBlW;faIWypyaAHmH@ZD9eB?5JL>bLp239QsGS-JZhKs)SEKMt zLmmo{NI6(VK~XmCMrtqN;qdS#6kt+kvZ-v}dG|18^QXJ_?$m1)*4pSLi9KL5T6D(i zk{7+M2~ST#IjTaNf<-l~o)qU5-&IF{9QQ^a6EG~+cZo>q<}f`5TP(^cR~2F1jlyf} z3Pac+9G2k89yY__6XhN{5L9+1RY0hf$A|TBxrwQ7UW^Q*+6n7%Fw!a?6BmONb}YW{ z`;KWS?T8w$hH7O;LZ@zb8CGey_5nRNV%m5#BE=BVCuA92p4$Jolp`5rm<$TNyHt%EiIN1^+;LWcl4TzSpAtltl_Tgp$lRKmnpwF!7W3Ux+hzf zdL4^g_$S){4s-<)<;{*#!e_?hCS&>DisfCi3KsLxm5L?%EheBnSGO3ErhOZY)WDRi zG|#r1y0CcaDKUfXpL^|lZE+0zKdoh^_xg=!#tZs#|z`FncRoc$nC8HY5@iS zQp=KYJEM=5>uFrGiJP$i!e)_yFtR9gcn0y$G313|DddYVqLJ(x06hbO;{0!qPSQkC*PSMu`q z4>72zM|P@7B)gi%d7STa_HiY3XFe^Q@0*?TIZZ(&8MRk+*~F&RP01i!QO?bzbRb*( zqeouM^&-~cQp1wFx1qv@EF4YYH(aKs71@MCM`k9f2)R0ENn|V8ehINTYeRkY(fP+(Fnh`2 z{=NzdtadTRstDcsSnZ2h;rhqdmfC9oMyww{)3L}9!eMaN%#1t-2e2F}uWa|Cpr&n; zsq`N8N9y`&Bc=cM$_0y3DQMiaNZhskYN+d zJgRQYWD@Z$_IgLcMN32UgZ70~WymjuISNt5$=8{%!<|2oLcRH=LeZ0h0V^BCfJSB( zb7@0U$P9&Dw>s~Xhx>X(k91P77xntB4@1R@eNVXp6UZkQ`|Tbc<;h68zZO2BhV-j- z424uQ%nSyX%q+@IGmuv@B1|)#l&C)k!T4)X0srdoc@7tfcyMjzJ>ILgd?CDWFuM#`-UB zvN$A!7?^%CreDQ%{*AgQ{Evm1n(rEy8DY-yMpWTIo^@ab=UU~b51&QkCGa2OLUr+867F(E%{#gi?Jb>Sr_Ww(+0&7gt1@>%E?Wf~vC5nNSqYVAS$x6Z6 zKW$|dw6b_M6x8!F_-1{*;sk-8?*oEmA0&uq-4doSA+%6(5OtCw)8g;jP!l(egpm7G zce2AF8;aKLNvzFsrQZxC2l;uY+YQA@_)ZY?vrvFR^wL5e!!9JADQPlZvJP}9eoMX) z3Iw=>nNm&~w%s1ni&mo+0F|Nnvq*O{KC^|6_2q5f3S)WV$s_u!DA- zSCfIE+ccsMb26F1$tlmV0@B$O15Lnt|# zHe^BJ*^82oZNL41%khBvk&|Y!WKgLbKlH=g&Oow@58)_YVl?INrl!Xc6>bnP(BG;N zM*zBxVM0Kh5U=LVWac-A?kzQ0jx2i#RYxU<&}+XLap^5f8SF@+a4^~*dn@FP5eDA$ z&b-SV3)BKLQ-5@H!{*S68tf7R^w$*hJ?w8>^{L6*WUb;qovZ3A1!;+P`+diL0tY%b_2xsCW@CpPJ~k2>Av zJ`>o|2=hZ^q!J>vd?E}Ln0VP9^*ULYap*{4jX2>=QnZ%{0?TyaJdwudVs6=DNE3=v zRA&by>re&sw{zanlv+kqifKREL@5B55XxY8K~Cs>iDU}ODcp(!-na^)Q3D}&V<6<< zwdk#8k>Gnx!UdHxfKrSqQXJr>JsAaAsRK&N!YP#h6!Xjvp^Jmyg@1>Z?9zX3+c%MYadC$4 zP<2k%u>kJEOYS|#JJUK_{=Z++Fn944O4d!VdOu&8(!P2YeW`94{9E9$*A7&WEV%Sf z;y0v0riU>=wfDY&@jq*P_IjT#z^Gm;zB;lQu_q zi4;Ly|3qbY^@HtVjkPSwaU4hNr{HSTCd9?;NW=e1W2Q^RH&XIT9B5#RA5_4ZM7Z!2 z=z?zFeJXIioJ>!kN)3^V$!j=5KHK?V(;JyBIXd+1nOfUK?xwO$=X;2d(88=};0G6w zFw_J>(IUAJB=##EoWDZCda!H99~^;ux%K^_<(f5S*M`~AfN3|?hj2kXIzi|**>|x^ zDCT=2AlyBqK6qgxVli8v^lCFuIK)dj05se?v*rI7o8a93C(8E~m$}rrXOuS}i*H2O z`#JS%5+yE@#p5hw z`7BhpA$L-kRZmp(DISq<%32&{@&v;zk@z!&{fVIXgo%?<7%o&z>ZnQ3&U7-SE_c}d ziE0&|V4-nw6hE?h2b9cNw;bH0lp2?5mLX3lR&)2NJdkCf6g4q9LelcIAU2bs`yNXa zRXE$no`L-z%g>ZuOOPt}jmDw&$cHBh{|vNc%E#xo)ZY-T8XMh9GqkS$KC=f=3=cu9 zrDbjHru-QHtAF+Hxz}}jmpjD3E00yn3yJ)T>#cwM%OC#BGu}_Ci3#aD@hQ!np?FjXyUdOr~>o)Sy}E*+5}9f8pwgb5aYm1IRS->Ap|MEm)@gYj7pR?PUIz?b@w zWY-yOrv>h9gOEWw!_a=iTd<71)xvM=4Av(9T}g9Ky&V@wYBRj^!6gcS<>us$99M7B zX&9M@2?|-K%$$D^shO7BFFu;#Z)PcA zWD32KwE4eI%kMl9%TEhkCXfho4W5OtQuOEvC`;G>em?WAP)9HoYJ-$_SrJx4tzanp z*$T{=6-pwiFLPW4_?0{qSNSBsgC|Wt;Txn0;cD5Okl;hOHQ%axYfBQKG2tK8;R^Pw zD0~q!UEukj1(==qxltTnBe2|)9UMPDajf3ZR1iCyFHSqLzUCL?c|58QckYogUmDBc zXtl2^BYMpaQcKn?9w&cBh-EanLCmQp)@ulqO`r#SV>*wU`~^ZqCBhKX1%i#vW-%K{ z7YS1_=Q(KfQi<3(ueVRmitN@^ShE1&F^{1sKaWnlo&|~_Mhmlv&P08C@t^KUwahKA z+K+gNSx^PiPT40wm4O>HhzBhhQ(}rJgT8}oRDPTNSDZnh;y}tA1w{KOyhI-8J;+8m zy^x{7Hwyqr)Gv9#N>^Cv+JOEs>e)Bk@-0@qtiYy;Kc7_ApPE|Zs_qFNsuW(X3?^6} zmeBu@e3C7RP%nvaQQJk0jKjl_M7)Yoz5|KFyZy!o3@=pH+Nf~{iG#0`zQlAW;iPNE z!$F(T4vAi4Iu^53SK_IUDQCwkLJ#v+s(gErb##hVIcrNI%teqVZEiHuBHU{t+@Bhw zhJuVCpHJ$ER%j9M_s5B!$C#2{c;+#WlY_QgE;nVEu6ePru^uiyLBds;E+CcI#k;OO zsXM?%p^_m3L1P$O_Aamzj#a(dCvfGs2qIKw1`z`@)jq&%E}eP@p;KF5Iu6UIV<55^ z8u9j5BM-%K$fj&`Ahi}Jur*M$*8DK_etb&}`Y@$OTsytJgQW|0{+G_~6TbSfV&ow? zX;#cH(?ycDp3eU>q}i^T<`4$+Z`Q5RNV;{?e+q+J5o(!b!5LvJ0QdG08GDQ0#u8S} zS8+%FrheWQ`teF#j_&G|eSH+1RUmiA*1d96tCd6Vj~V-(>YPolKg*X9+@$5usI}Tk zCFaHyAiix@9^m+2#*X$0+1UqrJP>9*Fr}spjg7YQw#RFku6Bk6(o!MyxkobjDzjH! zZ`_c7oXjn_;N94Wgc?B}a&giF9oc4Zi6pTL_c$5NAu9@0?BT<6_DHUmo7r0zX^Im; zRIx9?7~03L9HJHMOy^z+DhO!e9YN^Tg$zDz(Z9TFR4=n=6?$7pok)=;3_*V#i~hfT`B@u98j$d+fi=N! zj6zS}fjL`mrZRX|oQuyenF0CFP18WA@z}qt3TE{t`?8PnaIHj>;6vC@PQluRUAMLf zEG0;OowEQHT>U(7pn652^g(76&-yzZC`N4U`)04mSSzx*_&F){VX%XgAekU;QfqU| zF&qol6J<`W0UQ3cDzAJ=0{f@N5lz;xa+l8(7vLi`6Dp zVYtW{FPV(q#F_R!Ru0J$HG=Z?GUy%DIj+VHt337!lDdKO%^7fMBy4^Wz~wM=c$TGVq0iGoiYmX177tle!sXMakms z7x(w09hU430HhFwB_(@l8rN8$guUL>nRsA@QIN)Bq9b;F0z-F)n$ zcFaYrqSk}Q4I9zx6^nM#zK|dU+jJiz#jYdiWWphl!J0Etf@y(UWi4OVBcw$GE*vQ; zxuqV$LgTNC7jCV++*Y)#zB)xLx+`a2rcoMpWn$Nu8q?I)FqG`4%om#*p=G>3uwTwt zld4w9Nm?k{B$zPo@4UIPsAgjlrn-(#y*U4^hNO+&4T7Jb4TV*0!{G33o+6rFn9?+x zdF{@Z7Xj2~<^YkfB;lYy807>#m$PAdAHk>F$YpvGQq11|b9e_&+p37xk*Nb49msw+ z4(gXGok%1_oa$eD4Ql3fQ^EH%`h*{_>mixN@z++t-1z3yHWL`#D^i z0V+7jycOy7mtge4tOe>`e>P>_BIc!ZXIQk^pl)Q`;v2{`8at#2ADf#mYWM#Ez>B6i zC|VB||E!TXr9<_W&hEY#p(-(MCCL5|!E*WkF<7tSO-Q<{qp53*f_^CggBud)rmkAn`Rq< zo%ur9xzJJHmF54qT)(!EiJLnl{zNhB?zvXPTza+9vE7f^(GVQdU($V+I?4PGk##PN zk`gJt%(|3gQr7wr6Mzjb$A!xG%0bAOWE5 z>A;a2`+wUHWVF_EH?8NK^cM{!43X*l=l(+9yIQ>1W><@!W&OFGiwb>Qm&C$dw;o%X z*O2NB+3}JHe72!EJm?;Ui!rSt)`TY=N5Mmdo%d^{L~+4Poja=kye2`Zj9i_D82V-& zqeG945q&d{%N%Ns*y4jn2aO2IayQEK2Dnq+6!HqW1>4?u(;iyjJp4PE`M<#iBX2K{ z2&!g!_UxCK90u`4Jfd+^sZSX=sOH@wv8c`T4pb7~=5nxjD^~gAT(cIopq^(*k#yY11p8fT)oeVP+ckyn@>j!C$yuSe>9J&!dCds6;go-H`R)GF_{eI>d> z0khBR7`0}qG_(KA+Oz#f>(f%(BGQv$qxIyL6gwFt=!QU`U$f3Brjk|mZstYU{p8Z+ zGE)2J{0k&-W4ENbDO9_FaFp0#NJoL()ymne!V3<}8bb0DmFu$5DBS+PS&-iuoPdEK z{oXewlg;^8x=2)|2qf}hV*07S#H3Pu^r8rnRepu|TcWCFe9HY6`@N)XVk5z3&bbto z6_xc%RF3wErmM6sk2V|Qo4Rhz#TJ`my2HqNbZXg<wCo1&0CCL= z$2F)1AGy11WEk^r+|kqh%fww~M;6_O(`iH~O6anL$6*SnIH6vU-oQdNpHw7et`(a*4I}QR%a0vUxvOW2MXV4TmW( zh$p}&(m;ykQpRbw3El%GSdmN_G^x&9e5 zD=4AVR@9_Is)zJBsda0&aI3|2GR;K&hQyq+n7*fOr z-u?O-xzl_p-Rx^59`1j9YVqjbQIFc)lRPw_?iK~TG@x~p&+Z;Grf83AqOc@8|c-={I$bNN6WL zXj*$gbD%v2w=?GW_j}VcyQo#D-M7aWsRMBHlZ^gxarqeivq{79D#OS;hc<^0i&kT! zg;OquEqv6d0|FpX1G_caW^JI~=NtlD!%)c!dqez|Ulzcr8grU1a%(VEA%Tcx9!?+P z>28cIi}f&+LZw&ul_!e8s@|WourP|<=&M#&21wLL)TPF_C4w8)P^KdaGUmZb=P_El z-_}6=ZzN8Ke-X(zPkWBh9jWeiZq%|39>JJwttDRW1rqsZ_IsfGJt*@6(d;#F^o{rv ziycv?1M3A6@uc*{Z?u*o_gh1*JCc#E3So1=;i9tsP&s`mCd>fH!98(3N$c{c?4PWt zY*`**&fezZ!f{u&G-?~k2rI)N*X64v?ymwEGw$Hg+TPYsCMFUBv@3UL9<1Cs_rwl$ zsvkx|Q{b;gO!Ya-K1QxAT|2j>kE!~D>L#is;-_hfLPA!PFTf~rR@6Od;&e%c-hRN5 zsYxD=ouv#2Ec+4PlybZM(NUG8P?Yedt0VOj~fqh^L*dwglGNeK=5^ZCF;W;lZrI5HD#fRy<5h)K-~!qnkRJWI3xvfBlvbs@V8s|#&qwKRs9u*hye(J zJGUCve``IR%c^$lbyZ6I({Cu)G_*4o9eLWv;0w3JFDTHPL64hNenPuifX4ihxK zuHh9TePXs=Z+4r*_I?pT0PS|MuTPAa*P%F)*x^W)W#Lb=ocr@ECviB_a#y0|Yo5zdA^#xe0PR(lXwU0k|atC8qVv$ODe-k3GX;q9~r9|HRnc+YWwHe z_Td%nFN2)SHcO1`AuOsB6pAD5Nwi@t9F1=m{SQnbRed7@1{Zz9b`( zW%GCw`o(8-5HhCC=My;k2Y`%m!7MK0E;=_?29v{bDm}?R)(`3@^ zG^r+i`qW&L{)vxNHum*)b&JufyUJ(ekWC|2t^Q@~x)oSMf!h5@waIRTVQvI!Di48D zXA5axGCRN8mn>hXd&z_&wJ#k}=iaRnHH-mZ!F;V5#vSZMp{WUt`Fj&9_H{a8$|Up|CxCIvdL07NMI7B>#HB29|_ zn?v+x0o?FI$f=b`;4Pptk?IiAD9`KBlE6{Q1iV!t!>>_{JRD>I5=V0xuvY7LCK47eV#1Qkjtg1QCZ|?j{sWtJCIAl8N43UQNqZn5|J zHCpt&d|&dO)u34onzx9I5P?|q5A-v-(oJT#vq+N~Zifp70|iNV^cW@V(D<5OPUl#k> z{qvt&2*G_m63X_EDs z7I2IU%%{R+3vWriY)jVWDq(4ur(+P8II38yLK!EFC8k&|pJIVvlqm(i&;e2`V*#p; z=mbub0mbFzJ|5) Q%i`z%0H3uXIWLnB0GiuG*8l(j diff --git a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub deleted file mode 100644 index 1bb604f3e7d00e1af3003b25c32c2ac1e3e13fd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227145 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

($T6IqM8VD|QJm?w1+=jb}$@E53%mt|40 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®P=R$%pScwJ#)%wd4(`JhDlW+#=|RRsSI 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�(Y0ocQsl8i8=ce~HTxt;a z)RqUT0VEqOC7TP>HD}N*V8lhKD>b<+@XIN_)vLkP{p#bN<=#@jHW=jbY^pZK^_9y3 zRCvDF0M~mv-doTp2AmFhMSS$*R`M7_!z!F8ZxHqa%f(PVUP}AI5z0yk2Ks!80{&{X zUA#}$fufu}?2x3`EGX(E6k>l5g#w?brh|4RFZ{)9>q{&UNpv@`YzniTn%Mz zE1eVmC<*~nfxqFfC|l?B-lEZ0hmqExcA~es3A62ozdJ>NOm}?K6cxAA&neMgi4ts= zFf8POMPoOTGotpm-AP~=X$>ql8_*}R@jar-PEAeQZh{~@x3MY0_&qYom0^!GatyMnxhDaJ1c75yZF3png3C#81ZkpDQO!fiPuxPrwA90is3YHuU>H9B) zg)?+&oG)&Durh|%`qa1I!JhWkcVJ(uYT$g?;=}YB7m-%1vmN+6iKKMG?=7CR!`n$= z@hA7oo)@co(R)P)a|JX=W)9!xkB5B$Fa?8!m^n33wWUlQjEj=k0|~jI(B;-WFX98X zc;IpbV)PT99-2uK0q(O6!R$RJf1ii0R?iSvh4e+AuYPrsr-TD`e5Do!Zlth? z`5?sBQuwI}F8!!QdhQ)+Hjl5{l~TaQ(~|da*Pyr?lKW=y0T8sVV&94@=(ffy=!-Q3 z7RqT{BNnEI%yM~xb)kX5CFcmz#yTIQ@f+)l-n;{J4by=2DjF4iq|t9Vw4sLKj1BL5 zhf;Gl*hVr^9H#SbRMSGDlSP0qfWK#K88>tm+2Zq(8riL#UiuFWwJ8K)Eu-M(0$knu zCPHcWESA?zi_*Iu2>M*bQ$y!Y@`c>s5Qr?Kewn4aEn4^hl_#3jS!=)B>ch**K*}yd z^^ehBgXr=f;qBs@RHr}jg$JF(zq&`wdDw|I_<{eQllxuP8Hq&8z`LgH4Z)5VFicc% zQe%{E3l__2AU%6mDL4`8^PEvQET}{Q zOf$2WkgjwV(jJ1JE7Le_LTYA_O!{^m0=Uh!M-h<{y?y(Kft{40b1g04G0u-NhlhFt z?r;W_qRWr&Y2``2j049n(C_yJqC5~_sE#7Ndq3)<5i6u{Zz?(zGBjL3!iv6a^hu)( zEw*dLrl|+kOHiWDcjQnlTUQT{t6nW)rT>LUuOd>cfg}!``sg(uoFE-_xn$jh42eJ9 z_aSJijW5S(*x{LJV<5zIL&#@nCcdl5SAU|V8?0x!_n;t3_3p&pST9~1tTLHdM>A-j zFBN!oy+jC5Sk-vx6K$ViI7D_@MvwL1YG+_`vk4pev9SaM^gVk4aZ_ntF1wd%h1lNr zB;Okg;s7cL-FU9k)&Obv*Fw&I$ku+terxyON!?R$8i>G~i@=VU=#Yd>$pblbXJ;{3 zRZ%#P(aZa~GgXIM=^at(XGDzD)QFWd%CrZ1_5wYm+YaSuA50MJHoci@P_Z;9*v3-z zT1o**@KY;B++h1!!EtpqDArewn*D~P34KPrgg@!nKrzH7c)xXzbnkAZ(`*}oW2EM1 zzqL<~eBi-ObEI?=Fu>j)J%g7_6je_?_Z6sNqCkH@?j~}>viS9$W6O-_)Y;|K=j4CR zoK7!gNUzN4Fm!LRJ6=s!sN^JaZk?MyxZxKbma5RL>-gI^&*c;S2=C>1DaYC|I*oYk zq+zM1_@`moz(Qt%|B1v(QZ|B8YmWtCv{dl+RKk%b`TYS&X z+;bo1frRaRwjWdGOI6L(6PRJ7oUa6pNcz5Q@f6AaQ1&XHuW$Wf1Ax-bjBEQiTACwCU?$vitw1IkO3Mdy5UsV0 zy~oY()1jo@Q~9=qBk+C~1jgz&cXAiL_d!jY!B`T%*La>qkp9wb%dF@Dk@P0`wBiJYnE7f?UbWVAqmm-vtU znBj-~@VDNBM0B=Anbh-}`#t0eMo44B=891RS{7#M#viqWW25h}fWd^uQ^aJxw%wxp zjo8wQscygaS&hX3gb@|M1KUv>QIFxtYD1T@EIitLFBo6t=PRx5AKSwF#}fP2hsFB` zpj7`<1p+_Bv|ELkLQfxy<-OMe4OPNDkTDl7AZNe%36PGG_A z`(Qy1XH3EFlg$-9@4EOo2w%fOvpKwxr9crb+j$H+^p6Mj|sBx)Yt zfjQ$WXHKaWz3f6(*T9d@DKSgNW|uuDrWz_+VyJD~l7jeVraN*3;YvTn8!1aS9kB7b z$fs7BFxQ-CLxJMa%-kXU-;m8vU&`jB0MPlb&(N$wHXylpbFpfpAW9q-tjrj9G*L60 zath_@FoE=^B}xsgHb;w*ijI*K(O#vDN=u#f7>32IHyC@DrVc;?AcyRw(M8NYR^2AI zLllivQBXffMuud#fx~A2FPAm~`7L#Bm0&93T6DO)x;3q}5`2u&!rY?!Hp_hg@NiHn z%!D{M!*o`7=;bZ;i^*v7kr8kx4K8XYa1XtQS2}L={nNJx5;FmPIR?yxPtR;%x$gO}HG?J>%-!@Q&5pcnSL0{WOf9Th7zebGe%+ z?0k8C#0960ZRu17OaiLMslE{=mL{5YjXL|wvfSBB3BPzi=U_9!fx-;*Ckzz1p9hd> zm<1k`TvZverU~NTWmI%6>+4i+!&z1A=!XA?#UucKfEf&mbcq=XRypg4ijT4qQNb-Ap4#HP zjIag-PE67>)F9OcpnbS?M)8*DQs;U0?&whMd!Z6{W;_8X?sNilbxZ1%L9bx1PCB5 zh5dZqxkY^V=~&%xxPQ%XyC(3@`j7k&5?>PoCc%1+>{9~~l8FL{e}91iQ68`8FrdOn z-dyd3jj8vd#ko3;Fij=9d_F?X=~%6$zb1#+sd9{Quh8)P^_sn$d5WOs@`?vz$L8DK zeShiumtY{G;)}G4Y*wV&`qq>eeb8jti6gMSkv?@*HQB_u*KT{lRfLbX9F4xRQ-x9- zD&BINt&+I;+n9=GA~I%YxTKW^o9}y@nx%C(#Io%Y`;NsSEr8iUf0(U|ExSFK|{qZ-1VLA|lFPOyX zTophp(R;pMTkaOl?PDM>6N7@^y{*whldiswooE5TQXz8^#w1x-??yorK95#Dd&$+y z`2G(|b#3sYy)V0K0oT=|j-Jcax7sq8>o8HWGa&kL$DKj63|Z2E(QfX>?|Pbx=LLN+ zXM3gRb^|_K8@~?Nlt*|(X8e`q00)8U@8hIO3MrQBC_jdqZ2gBcd|;j%-A6>$wI0N1 zcoeCb*3%9iVP-6Gth|$tV>{uXYCf=(+3X`RF#bI+dLLgAn>3h%HRV_I!Q84F zoF(2wTq{b)Z10}Hd4-a@D^-8xavQ%zVs;Sv2TS>Unc|71GTI)=rOp$0G*he%pNZSK zd?4-)!Mg$hd2r8jimJ>36FRS%W~4`V_6#&mV3)lUQWKf4GKl;a9J4|-(|NPl7p8#C zS@Z8k)jjwOfcaAjO%ugYS5nQMwPAO3gr8gvdx!?|2%u&S`I!8;l%L4!GyG^Ln(LZm zm*2;0ZG}uuWG&AfqT!~d>>C3|S{lIxu{VHBS-NX!;T!FB@drgdKYTwPbJ6Oo53A9= z2O!0gg8JisIt@NNc;fya?wPH-G1(+vdsxK|rkPa!_tzo2+IR>H7Q6#c)1zLgV>jyX z0px)pe~wVg7lF72zvmfFFhUOsV-BLK7K&3d%Z~4pf}I!lA(J4^Q9Al@rMG7~g#4^h zUJzi^_=R}BAWC8aESXwavj-CWc9Xz^Gb6-b4sKe#3Xp=RJ4kr|oKG`z83k~51qPLo z^NOBU`5j)9^h^CQv!2!PY9M`s(}nx451g~iwdsx1qg^$X6c`HjH0570vPscs_fXJ0 zT{>N4E#0$Llknkvqm(j~iW}crR)HlX&fl?l2ye|FL&@}M^ygPF%WR|Zo$P{@Wf`1< zmor8X@Ib3#8S*GmF0r!x%95eW8n`vt8`!o1-o#<+C8LQ(BJsELH@69>j~iH%%Gqxo z$jg%$-61YRpOb~1OZCF!eKo$l{X&TsGs6o)NJ|Qv{w1$Qeg+%!Fv&4@r?*TY)DUh5 zFa{cSajZ){z$O>X*c`-FW#vX?d?F*#9jwF6j`_36VerVzXNNQY|3-y5LZC5={Q$r^ zD(Z0jka{~Vpd7nn3`5U&Z8C@&@pBbmysLo-OtLGC9`t>-Z9UdSMsWHFG2Hz$3s8m! zY|B8#_Fmp5pd0697&Z|5U}&3Q0w&(^CLB)!g9C8VH^chpx2&o!j)B!gpBqI;;BVL3 z+U>UbcEb08la_SSFTQmU^R@hpASS;P*SZuZc*a=Fv^Ns*n+fQ*(v7q>Bh)+g|W(Z45+H}iNO41yrY7chO_kpPL> zfP%@cbudHeUp{ic@9U6hEa#5Zj^FiEZD8w^)taX7H_hQJ@@AllriuDR&lZysv;ErX zHD>}m6{$qxB^;a>LLKXuDBe-R1^4+i?V}Hwr(HT@fuN)q`Lvb7mW~x(0j(37s+T^S z7R5EL@+447&A{jJicG#gA$2gjaK(IJcL)^!KgSfBD-)*Q?{{+8azINcb)6IarC;xl zd>4=DM|w)gA)Ud%tN^q5wwz(6M#fbl2DGxB}HtilH!SajjDsUo83$WVRwhm<-eq72UV{hBac=Q4jPD5}6YzfVT z1`DAAVN?kQ>f4d2ANP{F*KQ*FYFOY+B}fpXQt$Ux)4xkCJsQ^J5 zsPP2m*X;XnYJs^peH6uy5?c!?;*;v!DH>QRX#*qRvUqCDJPuF4yz{9d+kVo%Wdh~O zw|K42>+Vr!O&WK}*2HrpPU&SuPCxRoP(!Yxl2jPA=^7`19L7F_7u&zU%DDK_-qo`I z4!H3LLtW!xc{IirC-WHeC4|=jd{FJoe!4Ul@$*1X{qC!m@lEyTW4~xuUNb`ss}Wt4 z;zF=hYB^?E7e)rWCu&d^x~dPb(4l=l+OFel*J?m&EG5F*Yx3fFSxphEXp-I0#q`?E&&*Fu#WM-4Z$)#akgF{6pLR~PL~iIK6x}BL5IgL)qm|Xh|&kqBK7F3`q0;Fj#cJ7rC1|W%rDj6=oyqsmg##Xb+B-6xT^bjqx zR=~`?d5OiHA2AKIKfuIXqy+HGq|Tvs>i&CB!ulFAwP`y30RSi(1~4g%VFawL^fyjl zsRYY^xNNhit+jJwyrd!LMSYGlJAR-_1rlibo0a@7FrM_WxOwZywVnylH+EMM&fD649; zv~;2U%QfFQ{^PT4=*~CYr~o`x2A3Qj@R4wOBj7glacFZugkWN&SvJ)(bzRL`uK(O0 zNd)dIbMQ)$SJKpejMkx2X6#ajt~mwU&?bxu5nDs{iWa5Q^#o&?*97XtF`$q@_lJhf z5=bz0hKF86A6dJskT%3-8X>QuBb>sjrA&C}Zwede z2Yb>IUci<7eoic}s?HyfU3-l*R$S(zpQvfM32hreQBHW6R0%ZY7I%b{lvU$KRimcz zOVY{gt}1HrPRi&8Qc*uvntbao2UHkDk$X5B0u!W7=Ca2iUZ`5&Y2JIKz)Z=3=x`tn z@AHa`vIvoR`X)Bx01AZWeVi2RX;UnF!F7Tnx3y8(PFdq@xDX_<(e^olZb3NyxA@Y{D6W^sDw}V39|sBZ!B_Ujn+R_^h0K}=f)-lPNR!L2dr0}-kd^f< zkS`eEX&W(6bfJ{}dA}7O2MDUFXjj&D_}=U<-LFJ7jI;T2rhRFE98V)nhE#Eak0NNa z(|334qyv#95q#MTlA?j71VyzUmkRVS$oM|7Vt>fVlUZ^fZ)k(Z!32fn10?d%koOwN za|(W)Rh8B4SC3DabbfaBd|>tfvc6 zt8)c@3r_$=;E1qAQ~{-E!EDOm!o#9$yEm~QV@aq7$N6c|@Q}@o*FVfRwh!|tQQv|> z#$TSCCd{40s~JKV7$z2+2k!-?GGwpt`|SMs-nFtD=GBVZ>?;DbBLM8#5MFTW^)w|~ z7|fc9*0c)FeVB228LEMgFL@Ij>NA5Wi*E$2_OWs^|J_S13KR|u$U)0Hivd;6ZMRVfgP7Ka4Aa1(UdZxBNb{XoPccGeqcrSPjAz$=zN0j(Ouqr-c3NDs{6=+ zQ^wk^-*JH^QVC_typ%5xE&%!G$3%nF$XPL9j z{VE1@cny1=+Q#?oJS04SKhCi!-)IMI<8xLVvlpJB>mHVHe9nxwhwy2Bd+DiECiR?n z9Z1@R9V~SDq@yWPNsQ_?13eTy(oyW?X*?a9-SlNE>`wt!;*D2)W>GcCM!y=E)E5bG ztt_}37nobWg|o3M@|vpu+Tu)lidFN-r_%wp6!b}AgyyOTM6f>a9&S^HH(T@$L+eVp z0=b9$WbG5RHxd`J;AtK0JlZ>{nj`|%^gLo~>o6(yXoX;oN1pL$d zPV%(!U^H%F^{{Tvn$5q>2bc^54HlO3Ek7epRnfvuq>=6+cNt&B5$lH~06WKuk1vLH zOCn-Aq&Uq3$Qu13mk+p1-Ijg>^;~WWzYRaf4R!7V`ee`mxqXdinDQb02Ii7nOd_xz zw?mF-R7;aLd{;RlZ67}5fL0Lj{3nmy$?3RZ3#pn}7SxcVtC-EmfdSqAj8Qkn)Tm|h zUO)p1{!jZ8eLo7BH}PE)Du@ePRq{Fd0@2jsm7)ZaCEzYi`#3!^G92j!K2up@=&mNM1?npzpN^zYAF?iAd-# zX^|0xAiWGGaT&u2L%<%lopB(thDxL%pC!!zJRG`yeuu>_6phyk%Hfg4ewF2Vrn6%J z_wTFHkdr(>;nCu47HBxvK0LLM3i^;em}{pKSct9+s|IeEHA>p1teEsRn}f0T`vaE>(ovlT{BO) z-<1-%ftqGHzgY)04Zt5DBQV6tW2>B!x*IGiD$oZf7>4&d*8M8wwg`Id0e2$Z&E+73 z+}X#1d{nw{Johe~D`IIl=PhcDrl(SqiTl)PwN0&6hyzD>UkgNe`uoaXbn$C)U|qa* zH3TL=)zR0dW6Qwon*KNwnWyueVsdIk`-_S!-QYKk9S7UAGzQ3m?#{axh-#$67C?;Z zlwb;<6$L-jDx$~-PG%MQCcyfo#Nd`!1Y~E~5nVHqoB?yBMgHDp+eBF^{B2y~Ur3o6wq3aNtG--y({R3oDkSu(_4%vV3YI@Ch#sggwf*)SLcwL(rH|uVGA+B8 zpEK_<5Qr!Xh%`Rg9iIXTXCB5dGBDUT1Kqwf!Iquc%ZpKd9#0w~IBItP_~ zpQGOX0K77@l}td#>Fl422zBe!z}dY;qw$gVJi_3h>NKKf{Z}8U%`NCSyYv>TE5&K9 zoxta7@E`ml$paa~l@6c*h;yag$KLM{dFgdHlcPdcZSIzJz4__~9}*1g5MAQjPbMb< z-ZY@zF$~Mz!jJKAB|bfo{>sMQtqoTu%RpHimP6bu;p8uQa|0B-%xp{5H6@}+$Zq2$5?|v2 z8=JYP5)V$iPE%j#r`kS-!vnE2qGkmf+eekJ4{U+xBFl*ES>JEuZK^L<=B5V=Pwfia z!%M>P5SY^3yr<SbI;;{M7M0wK|h*+RQXnLqy;9fZqL6b>Hyt%tjN8eONp#Um8N{4`Ms>+Tjk&xL3VE^p8S#-a!uVeoZZ7BnpU7eCXCBtxAo*> z@-4;K=Z5f>dfil(gXxI^e@I858ZgZ3cxhz*cr(3j24Jg_>E-D90tFY%YZqguc1uGJ zjCv5E4VWCOS(s3tq||{gCNQ7HIhDD{K3N`Y8O@Pm3pxYAhlVqQ9g*9;&U4fUw^3*` zZ0evCq(gF{yeU3lyVVmCrZCuzvU~s?A&p1Dbfxa%#WI5*!|Frfv0=sUHNf>tVyw!Z zZC(wan_%KMP>ij3_;J~%v-scoLnC4+HYVRY4@TW_$iyuYYN^X=YTx%}0X|{;d?vP- z-p+u5xa{At$)zG@mI7k1&Vj0Y48n3$VfyG$SfnwS`X(@g}@!t>BWD)+@3Ac7$2z{uR%W=P9rR4a{1U*vRL z5DOQzkdQ!hf)Alx#QsT&_a)gBsHmnE93IvTuWtr5sU`9<=)el{E$HDRluZ z`YaAy*O_h7!@H*{doPAGLp-LI^oRebB)4>Ki*%@WP09@N2$}R9rhru#!EQkEq$zz~ z7ANnzdoP8%?w;n}n;WI?$53W1u7RL-gYh{xTuvwgAjO3O*BCNFw-Im@CZQacShVjwBZ@1GPMJ?kBrHpJQE8dW3AB#43;!a8OIm>HnC>a7Rtvt zrYhsm1yh<>quWUtmk5(Oj9)ig6h6RKB>4$i)NuCt!Nw$; z2rq>ZU<$*}fm|-7>IrCbVMsh%2;94&W`j+r0lL0N$~WSY@Vc)*LJf4(9St+F zf8mc?T$4N+_D!f@%^W4$?(cE=05{Cl2N~yUh~kf^)*3~!z@z#?Az4uh+0?H7o8wor z3*ypPQcPWAGH=qgcdsOWH@ExA4diqbBS@$xn!>1Z9qo{!pJd3KK%=-1HG?sIy8CyT zxp($I;$tPhoyEQ8i6n`e;9=hRmY*6+B* z8R^OSKH%f@5+4jHP`AoeU^Q``rM=JBKt1cmb<6X4WuuV^#}3(l;Eg;Rwg5Q3;NE6- z@(y^03C#+ZvbZR-ZSQMfRF9!qp9hPZMz2H-AH7+7d?$D}t%K*=cWCBIU*$#G&#Sc* zibGZCg+vtsiBJKMz@e~@WP)1fjW;VL{^`+v z-o(|y^2>8(W9fYUe>O%USKCiFjJ6-amlq(MftD-du?W-%^c6|xYPkbAul6g$DDm9| z+}Qh{nK(ZDlcT}>L@a5^>ZCICq4H?`n(9r(@O<@2?t+O!r0}9B%9i9ndgjqym&*FL zZ1bh#$v2B%plxTrA)TUbBDEO38#mJM6lYj^I;{GQMk`Qa!VNCDmY_qGCuRc_53oOG zWqw{p3l*hteF1xs*6B6W@BnaS^vI+z-%-Bc_NA?IUsW{0hgbtEEFuewM0tm6{ z+oG921XDxu^)~Od46YBic(dD2yEFs7orw(q=6pA${lE!(>b_-Hf&-vjoD=!jLNijP zvY5Ar_JDb(Ye4fs<$1JVviy83^-DeBFQ{-ICEl2?Jfg5!!fEmD=_6pgOw*2 z*B^(vB&a{T9P+^2Tr(lUTp5X1Glp}Y<%ZbM%Z{GXgOzpnjCr`X)=4n`0$Ym&Oj!Co zKX=48A=6xpiV~b`cF#<;K=0r_R)ZZ0i-F<`o$gr%ARH1%>NqC>`AUf%8S~3mn_uzh z>JZGU#&dnMe;ddJzpKE8PMR^N!rM9~3zr^t)vJPQ9|tKc0A1{6OIPUBElCl9sfF_! zmPvD17iN0V%6za25B*-7`kq>Fd$3^}B~Zf1*yA zvziZ3Um8x&K_bjnu|5}LE>_`m7J(HfKaDBlr0d^rgJx+jweCJ?B>*k%1_s8dD9w11 zjjHHMTm%l(^3Zn;Jb}oIiXVx-hT}-l84Yat830;(Zk)Fu=qTV*4NCxdAT|La6rgz^ zqP|lL7MQdEf8On|;hUK$iz2p0F0P&-&Nqv15|;!FaOv;<&1c$dbLDtz1Rlq}_p3tU z@VcYT1yY-|k-{LbKW2fw!ou9iENK zCX(+j!WalOp-J6aNi!CWHMgoFA^bWFRtFtABJ3=i;xFLCBuKz4JVi>mkuPWteZ3Au zofkVlj2=OVo@;ZFh)?Nmdab3Q3PS^2s`&eC!AZHqU=U{EI=}wyW0GI;;q&+gH z-*H{E15p_t_<}$6%h+3Z>+>2LQTBE948G&R^2l58Y|!y zv=WU={?Tl2%Fj zFTd<{d%HZJ*}83WZB`M#Dv~v4Fz>zi*qB!HxC|~n1_!Q6-jq=0Fx6AHpfCiF(Rw>7 zouk;HTJ(&1#jZ5;Vfh3d;bESE{eJs}P$kJxSq;cTAD4w(!VeFD;wgM-hkCmq*o|Hf zJ|6IjJpl_8NeBhEJN1=7+!Z*n7scG~0 zlU+QR{_I@+bEdqXjB>u;)40-LN59=cO|hIiVQohcbZg{t!PV^t++%7?w>6xQW&_2x z9c;LRHpy2mr_Z%jdA_=D;!Ue9FeGW{{k|WMoJ5LDO6(Yc6EpxdNS4jAY=*HYu0a~g z?(GSxMye=91(KUxvIm6evsBBoJ^*oaAnK}00^&^@q23Tra)EFg61MxoWT~hlYU0_I zkAfqBJM?oJtbaqn;{0B@zigF8)HzvB8N)HmB~Bk1BHe|zm^n#pX}#h8AdSTnZO;d- z@;N^|0IF$upSMBePV4!>mr_>p%GGULm0yj@iGuz3c`32Le)KCouNtv|O?$c-a!6C{ z<AD(2;3&V$HKnpuZbY9i{-fd-_($ zC#{Jd%9?10wrUt|6qd~6`+%PTBzF$c>=>RYK~E(q!*pIb8zQOx&T`i4;gS$T77|>a zAzmjdm17B9i6{e3y7-HV%}(oH20F0(qCy-8hm3stfRBx-)Cm^0@?uRUv(|p}iT%>^ zKF=-RR4u3zm$+t%FTm-8p}hjSdO3q4{<_y;_0#>Q780`Wb!!qFT($ZtKqvO6_o(3g zx)0=6Fb}PnBaN3A`}fh81UEx4Wdl{6HAD(YhYhHsgvu7RQo`^H^;=HkQOF1u3K!`- z-%k$1OX0nvfIneolw+{GnCvZJU@Vj9vS8~H#9E;FGzAP-*bYGix|{M~GIDqv!T}je zxG*0SMJZkd;K79CghnY4uwgjp9(D_1iN{IeC?gn3_);}RC; zmjV#EvS0jL_$K3D{CIJ;@v%cQU^Txlkww`X=Rlv{Mm#QDRFrw@A4YTTqu=Ue6JUPh zG0}Mrm`9Q1N+@Ur1Z_5bf~zO-=V~5^1kf&Pf!agn8Mxrbh<(>nr}n`=R5%r=Uv>Ag zi6`cdA47mJTp9y*jul*+5S;T%PX*#x4jn%1x0>(^3Z{=>osLZML$=MQNu*cW5pv0Y z*l5=;)^ko_OIwn+86t5c1c>T!_R#?4&tO>jh82qPR&9f!dUXs3e+7t&(fqG|fq@!N zbged4;E=tqC5yrJzFyx^mfG6nt3h0K7w|6sQV0Cr*8z>RqVGVMDArBt^p}EvB zH`WyWm5_PZ6w12?-i=G^$62Z zP_F1Wzc_9t&Gz{FhP~h^Ih#Q!ETM{f#d{~Ea1CPR{bk5Pgd2cqa>O%5jp!@v3}v0pQ;}Y5;Zk7a3@z2DchsIE;^XU&BkJ zFJ`%nF$qmXnUQ26$(m%FL+~c3urSY$_Te)v2QB4JdJa`s^_fTOilZ7?VG)ceV&bQ* zq==avVtlm%q@cxzD`)|ul@|x4YhStV$b7U$s$W>pqcJZq74Jg~@sNeJ|M4QwknJR+ zzBc%K(%+}hRB%O1K>n1SQ;BjQo&|p+HX!_sFnr`Q(=!aE417hystmT7c=@ld$K@V+ zSc4jgN#4C)`jvmXa8Gxa<$5#$-Cs=!C#rRgJkk3T6u5N7(Z^nxPT_+a|7-N>9O`^e z2;B&mwNjaHp9hq8ku}nYa~PSW^!Nx|uE#y_PP1>Xq49XrR=-@1d~QED;+g)s^NW5B zGSyWu16fbdZ1fd>z-pum(4O|y1Hf$cE2g!S5MCpuoebE@*cc9xu`S3~-1U_o@(70w z+{qhEeOsF42#Pabklj%Yx}!>4g=qZZX%4Ai$fadqx0JQN?T24B+*ex4WP4FJ>${ei z`uKzVeXtlFvH{Awz3G@(gfU$%&};-DV^MOSIU}AJ`DUIn$IJ!2BV*OIuVN zr`cE^J_d5Sp1eYaeZk1s7Lct2{;1conUe|_ynOG@VPlG30-x7JSK5X)X_LS;^fPBn^wB=OV7R!?s*+N%y>`nL;e$T|~A3=gT;xlKKSYB3?=bbH6g z(%1!5V=dmKgQgAIKh!bx&DCV|2S;wh$4(B&Z?8qe?9EiD7s z+&l*HM!FW5|KuZf!;S~)CqpvS^|SJZJ-pywk`v=A?YfoDZ&pAGhe`xf!3MGesuYk6s7JxG*^<~ z-$Q=LtZ_pW^drZ-;D+!8?pM_+&DU@A0z2NSf`Ow980b3|75g&a$#x*O&sMk!4pl5; z8E~lsk_wswVNHzHb~?ehrv`KnpX>HYrzj7igEd@M(L%+0;B9>RrS>kI5<8U$ zFjpO?TI{f$7<4%*@KIuZJZo53T{%^KNZE;JGDxMTiy_&L&uyAsh-&vUpB)?@7plN#(_(L{21_sa zb2}PT%s=^6>F#>QQEghM_mfM`QWPhBS9!2_UA(e`lWLgW0x!hM5TTfLendvo0>S5K zhz>&rTz$&YR02&&wP!-3IA{d*-pptN<%6!&L?sZiU6qmuj%S6KJ%Jhe95BP(C;A0h zQCM%dzgr_!-&F=F{g^xs#Hr?O&8Lm)+OVLA61EIYS#AauFCI*|-2w$?csHQ@s+K9O zY(f*D=!xoqkoqi(Pyrkab(eYw8!T|fm1es|y5jDqC*_~-{7X{V=$KM*5lfV2aIk*% z&r0&I3Nl}O)Th(y;~C{q_phCXOz37g{7%WV#ma8r zyZVX$C~QHnM9lB$_j3wX&1;-C&J)<+f_9lIGeHmq-jYP%DU+dN+cuJXa-I2Q0T7@G zukY8KSd6cE>>IN4*XFwBZTNaV zzrpt0o|9eDmTR42KimZvggTLvsW%aT5p%brCr}%^UAXbENbvebJS#%&6!qqCsV4*Rhi4)OQ7$;HIK@jLGwZAZKE=M5uUnb?dHNi3ClShilQ_M}g0-2hxOjO(^6ay>kmgPBUb! z=Fn-484bajh^LcH>V#u-LmHnF2;3~BBI|d3MX!wk2;Eszc?mEBIhb+(%25AC(ij1A zeQ|v$a7MukMuMO^H9yzWbAQ5Kzvkx^F_Hg%h>+TT`1i#Keuou)gFhWJadAxd=jqF` z+%)*-W?;TjGt&c_8=8Ly5xtm~O*2oy+A8%<;Dq+&^CKzm+06*5ecBQqAoh&{@zUp@ zQQyY#vts@*&Y;JnI2y>T;ewbaX6zyT5w-_LfG^n|u+d-;IJ@%tB16=dwcZ5A;nPUw zNnI_?M7}PN)=Z5?zO!Khw#l>_!v_qWw@`245`BTbF;=f=et9!}S#fKUD(-!t&#p6F zy-SCi<2>)aq?eCmuSlx0@oPRZXlNO@$z*41TP)Dlc31=&LYV=x-=Q$qn2@xk9*t~0 zq@|1NiNuy@t`i^yc>hbzYP);u@&y z;qsw=Mfqa@r>qBX;8~#T)CM^-_HJP01I1vkU_ch$s}K=rMF!mKr&|IWFGwCbU--o~ z$O2;A0eH6U5MmxaEKkR@Ri zPCehyX<(|Whvz->94|^5=n@ORLCLPQp$`_~rgb7k3kVf*)E1i2J$cL`nR70mj;}!5 z7|N}0lr^0xp7-gcdR*Vy_5p(NGO!5>>NT%BMM(`bpkBs7+7c6F%Dcgt?sP#=`{97e zseCu%L3TU#9uB^0aOG>!0vS_y5I_{mWSohOqvQtMEcfWw`T60z0?=UoWFGjJA2T1k z2|M;6)#sx1F*CEe3ylF+nnjvAu(d3H&L+`bP z^UR-WSApYwCK|-45l>Afq%)1cH32biI{_ zCBykquy0^Rfg;oHCZa#<7a+nhhh~}r&PQM1oGZ;~F0ABIN!6d+#++rz3himbOXCCK z@6@B;WnsX;;Y$VMQo7QQL~I_Xe)dYKWZamvQe^+TTPAW-g()SUl$iC@2BW>Ma+WPE zPPVmm`a1w61a&Q+D7DqD3b-6kjtsqC_S-I4Bt@C0Z+Rde1)%=&O9sNRtyu&Lqi%Bf zpLtFo*@z3^tRuLLZjPaTGGHV+<$pqR&xk6t{={exYqMSR6-?lMH$A+wM8h<6>AJ4j znwQsEZi@vytmoI zUeWvO*vad51r0|>OW0HgjdmKyqd#Ql083h5XF-lxT;>@7umz-3=E|q>2%)vE&wpP_ zX(W|ceD}`$4Lx%lM2;GC!3BlmL7DdUy93Usa5cZUiw!9h0Uz8TVskajbXmFFT`}S* z78Ej#l14=6B=E7ZjyIT{%O8|=^&5hhV2M+b240Gcb={R`DoQRFmxj@@|wXx@?aey3}zzQSAxYW2}h-P7{v6eOMMVP)8+1) zC~M53sT+)p<4<_CIp)im_)mC>h`a54F!);J2!2^+_8lXjXFRoF;G@!|;ZtZnkG42_ z>cMdPokfV(d%IuRZ;-%-_n02&?}7`*iQCjS4JU1u18R z{9fMVPx57}y8K3Z*&u}(^eFpM_rt)NP_rMWbR%dIpKz*xfXBYATU*a|LNM{`Yo|(> zu}rl_Cnnu{Q@UQ>Vy@O6tb4QNyzNGaVK84a*lN{g(P(ea1Ou;ouh-9W-Q>gCe}IaV zxM6Ld(3-BaxO1bYaIY6RfJ~sD7$HR}SZkv9DuCqmaRv+}OhTW*tzw^fk;au0GH74Q zaCg%U+!Om5SMWJR+X{)gsdOJTG%KxNI4UHiH({u?H+gZxms`7UNt{EG9$ zpH@3f5Z`e$4$qr^o}e|qTtUmp6>WZjEbQZZYkPS%|K%m0^-0B zob849X;$5F|8ngWi%NsmPXmVMa@zJ!yRUljMPG}Y5Tg`@*$*jjnQu_|&C@vIe&TX12$N?%bDy!st%K(qv)vH9GvaF_(`?Zz zHXt83M)OP`x7YT51NKDQ4c~XED0FflQSxgIxRuhFV(WGQQ|pKK3{mxhz^EII1;f6* z3i7VX>&`^hawlOBXw$o>P(QJV1kDTrXN?h9y;|U{Kz;sB46jrCk7b~*WLA#}n;(KU z8$rs<&`n?-C_mU(2~Xa&BK*_WWtL)kpXd=AH~i|s426!Q)uyCgeWh2+B~)~$ee9<0 z++_wQ)_hE$!B`MX&|vCr(!aHj2J8{|RM}~qdvTV;_s*~?ss*TuB_UF2A^vNLGtcxW zt5@p61Hq#vL61xejrP@0RYa$x=&cJ2tzUtFu15U5k(K0jmm+Ic@gE@M=&ez|!Vu3} zpCU3tSQhu~d5uL(THBIo069R$zfbWo={Lml#eI&kY&5nNtr(EiFY?eI95_WS72%8$ zaEXUAexTj~VwCj$xg-V3i00M(Vz=s|P8^YoSaX8M_tkR^ayWOn21413eW*gAv-3H$ z6wI!BRA$!$&7yY^CQTCXCC9FOzSS`XmlDl*DZ>#aDosPuzxYl!#$O^Kg+A`AP5pq! zNvf_(hN4XERlSHdNFy;;C+Z9IqF+%7Jg&Aj0)2j4q5>(RRQuR@ZBkOGU!J}|(&c1h z%fw`F%RjseQ=P9kc5B?JsBKJx9{RK_)*Cv7MD*tYKw%;YZki zEzmslEPZT-R|9^PE5G~*{3kQ1U}e0%ghs_Se~%zG-K)DRG{3(m9KHZe^xb7&oOF;sAWc}` zAWMY#PU+k9mwcyz_J!d}GoWmF*g5@5IXKD%!JET0q#NbbVk7Mn>pUXbbr4qZc7|QI zzSmq9|0Rcwve{?I2q?&|4?EGVrtL5e%)=V#{g7IImJz&fSHn4@f6}AMXmhLeoKE*k zeZRhIT6tVo+s$wgp(+tee1s$DTk!063`+y>xZ%dIsDfbgD_EF%mJI%eCZR=#nipkA zKfS5pFR(+l1aMIxhoFoqw+~Q?=(#6*-(^AEfdbbL^D|%=dI=1?TK%CuSXo{a8Er&b zqK4#U%_q~=64(R^(_v(959c*9Q0@4>vA#c(i45ZI)5&FA zDlZKX7>03dg}g%mD96sBLg3LDpCS_a;G0p+`V}$H^{fK&`3VVL;vcPxOK8nBSNy!! zMNIe79N)nPFZQmqvTJV?Qq};0$5;gF9SY`+xxu4t7-4?Fvqr*}yfy-K02wZtYJmee zRU$4WdS93)mmi%jARb6bCaN2H*T3@K%Wm)p8{-di){EL8rU<+|wxA)RB3L~+wo%h{g;(P|k;OS*eS<7!aqINQq*eh_JcqJUp z1Lj~&TVRnD{pvOCZrtI zzI)qPZ2>4j8;I+v*KnQ1M{*-V0TVK0&xj32%_wh&>3`fl#5aL`H7rJr7x@ODNT{|Q znokdyj#Y@4`=*x$P#Ix4$7oVb`(*`%(9Ish7P3qY__fJTL#hLJs4xbx147xr;!son z3LFuO5X_HU`8HwrN;md;U-jj`f4!d<_Q%W?)gc|JRwcH@0SDL$fnxj?jLt)L{l2bx zs}A!{*jzPSQKEz&mSh9)3^L06P|C`mE07>RW<$lEqk$D(C^9t zts-6hs<+y`DfC^OEEDOk*cR^yYbv{$hFAu4hTDq>0jBcR4tI!7&JP$peDZ)oNB5QQ za$&WeB+GYH5HhZ0mtI2@-4b-({g>MMH@+;f_<&J{e}?cJ-d&u{?7Hw18=XSEK%_Xa}6HdUlAAO zJ^Gd1CheI?uRn;ao#c4G#^qi`3EPHp)I!Apb1)DaariTG1JE4iGXZfNNoQr=-zKPi z2dsv`4to5kKJ|&J2NM%Su~>&HGc#TV396dcRC;1K^#CS}6iMvuJ{>89JR78Z4-C%| zR#l>FChgnk#?|fvDKXBlk(D?1hBH@gpgUDwfz=CG+IgD(EA}b|#XX{{`j=UyeP%QMs3ZlI>D8Qo%_{zbHkGE3%w#j_1b^sNIiiLpsJ;4EU@nh`4Pq zq6>RI!0l;3(S)<&2j(oF52%;XGV#!G^YuM$$gW1&e*cWQtE-WOC+v(6>k{A1r`Jq5 z!@pC$CDkoG_`G(kYv$!g%)7SHK8 zAT|=Uud-sSFt-cmJ04A`BCD%TP7KPkB|&`%aXYvzZl-J1q4T}z)h>G`$wHI5P_y5Z z;?g`gBq|ORCuB?Y;S~u!Y$xA0Ks+4@EFdK?AS3=nNfwdomOxO zLvFX-zd>@Hcf~D9p~X0U-%jTHG7#pHZ%qtVo3JsVe$ozWO$ac!h2LPHvmvm_bMlg< zwnatS-b|FHfO_Kx9%9|MapG`fu*$zyOJsKNNz&I3_j!Y*!T$m>t(jOE)dC-a&dA^gxXjQ&zET}OP zY=LZ_de@L|k2Ng`{0->M!ss6BF;2`NtNKlK4FZQ#{5F=pTf%?{M-=uWi4+7oWwJz}gR<{8s5|xz-oRhVzwnc)2O>_R z=Y-E~$UI{Pz_hBgfvJ4we0YFF&odl`k&Y=52Eh1J-$2wIm`-OEJYJ!bAHwkFlnn!3 zj4arRh->QLsK@@gzq52Takh(Y;0O2p3OQjKz#x$5(?hNVNq!dLL!Dt^Y%#&6e^j4rQv?D z#se4vjSAxfA%6BsiMtnc*^?sbE%-g zYv9frycqs`wbjs*11^4@Q&!-JE4~*AgC19tyFANRF>1I07E<1dv!B5E5Mb5;^G1#TZc3Zrc7JgBC__Z#X^Kq=4G2BHw4J|k`gzf# z(JV;`z@7c}>%ue$(@BUvwl68>b$>TMk2-?uS8FJ#S-KXv-%6hKNN^O-EVagD*ikpy zTS(81h%%k)t98v7czhUm>(tUuDiz{4V88Do;Uf<%kN2vc1e`lG*Cys=0m-6p-;>4O zm(ySsvT^2!3rsXx%D2kf7nb)?=eOr-BU>|LAkjscgTRrPas~oqS|Fs|40M#I<`cV{ ztfgLjjFG%l#2JCbN8Z3G2D$QfafWqD>+|}OPCcbvjxi_UzWfX|c*e3knfs8MqBhsf zmUYu?@EiaW@NKEuG-uX}R}Fz@)8DUoDB@+Vz>lkYjQ!%{t0E&bUf#(iXB!=3C%?7b zcWx%%9A3bzuq9p?+yHX#Hd-6i`O7e-7BzP8Ud6ntDFcvIWXCt*w=aOyQkM95x8Cr#oQKt%3)%bTOomALfvZCKT*lj>Mn z9p8Y2CI1}KCK}3E4lD5OR~#M3`TNbMUc6$u<)Lj&qf+mpZx{}!2>OzDicWsV$3N6) zK=+Nl!4ZDJ!cZNDn@tYLutnv8*~ulwdHs-vqaf=u$&lHBV57JA66B_RI3Fg1Tnga4 zS^lZr#5BYXyBuDbK4$Vgu$oxq$(gIH>*~t^48=4Q)tW#yHOsEYEn?+rpBOOtAp zU=Z!EO_OlQR8En;k=h@Kxg7=u7#V0j3UIn(^Zr#9=d-}WQ$I>bEb=-?p<)0$#xA_H zav{J70x;FT{qvnSeC&Rn^99Wpb!~gkXU4sHpPl5UByAiTM3k205*zvZy4rI*2;9iv z3CCb+gqBDEd)f$gsXW<2FuhmwS#(Phof&OjN2aoXj`eqISohe3|isyodQAk zMT()Cz6OBLKF0lH!QkY^UHPRN_k&R=P}w^VPv?02^Cv?fXryRL#v7RA#xoM)!M*bp z*%NUhwKQ{mZCvYqqQD%}SKGUtRo$09V!iHR80#MiAgtgdw%5_MSo+z2v(1M;3vJh1 z*gqRJ)4={qsiKpf4V_vZkQJ@`FrUFd%I~PdISSNWTvD8mWp8|-uCKq_9zo-^UKt~R zUSP-zoBN3V^C~=gu&8+D+w(?z4-=0~Ar_nW@P_+4|+g+Okn5G(6xt{0z7>#L4)&|Gah>#wR{t20_Gcg+2r() zyPuc?AA@V0FX;LnuE>snh;SWrw%Z21vy6FQ(m<*{OH7wIzDou$)2bT&8KG$g=ddTF!1PreTK_=G z!UnOC)6s|Q3~)evfo>=nl#bB81SOutlO3-LLIRHDdX0Em90kzVq+A(Wqzw{4M4Vt6 zy=7kkeq!CHdkR|;Y#s-WBMTuQFti)T*}jSKcd z^jH*760ZEU1br^Mtnn4EE&N7bVcjmT4@IfIMlb%PcBxu<;`dWFB0p?^yo~|>9>sE5 zanVUR`L;v#lyP0?{a)g=Gh$yy-+UL)oZcO|td-#PH&cI{90f7@W1`pfPJ3bhwov~4 z=|>>EEudhfD9Knax@OG;{3p1~1q28%-=F{2z-C%4{D3d4$z?C=4|>C(7>TY%T!iRk z=c-B&J8e)Ss?o{Lz8jMMrYGkV1y+Ri&dp1gcsBas81JS1dE^w9!~8noC%ss@nCJ=1 zA&o$EATSkR3zG!I^E9pP&kR*5mR63W1F14RT4o$>Bh9PV8GG0iPUij-V@q{;L*)YA zFapnbI`ZZQ`d(<(vJ~47R<+l>y^q&Wu{uZf7!a-o0MZ2s8xrn{Mirja;HBhegfcRn z>p-3gQMK9bGibbqjcA#&rUomTyAdB^b_QUl^my|Xml+D32n<|aJ{Ili`Je4dIPsBe zNqs@$GapAtvF;A4_&=*+Rc1wRY&R!26vH9)|y?J71qNg*%XMRb6kS2U*aCMeS5NLc`~ zB}gJ}H6eHTJv1_KFcCKzS?Ki$_)Y;3=mz~z-EjhV8_(fNRg@&00R8vSyAADHT)un< z)^wtcrjDUMoS%RKX_P^k_ZYNHj_h~Qj5SRp9@>SF017V+Rx|E24ZS^8=#dP>m8J`6 z6g5>8(D4s)?Q3`yo42vi`~m&GXqDZQo1_rM(I8`Znd>WeolBM8Gb zfq%Q_ryfAd{pcb8J7Lvcec+mddRbYN3Cxnn>d##ERVvlE8xnK@vTjg@?GWGK1z&*+o{Bpu0bV^+AU`wf>R zq#85LJ9-qX`IqkJ85lRXAklyE_DQis@lV&E8=#I=-c3I294JYZgb`Eh-rbjG3=cn= zqk~eWn!Z6mE^0c5!Q6>ob#2spJgtd!KYsi$^!S-5WC~r8^dr!--L3Wq`ueZ_{>Ehs zxAyuS7r>CK2e~mm(1Bv5aDYT0;BdXJrEUvck>Oz&jNnnqkjq>wk-XSrYja2sEMcA} z^2Lm+3Tey01^=W|NzT;1SxJD#NnOKbu4rRb6AY8rV65K=EC@p43;vfYUu##^oUx># z9<++T?sFccNqj|_-m|~2Q_CzPc{L_(k4T1wOVNG>W+{_jj7YXWG9d21BBAT~j) zza+do&@?%KlN5&?5(9r1B{lruv6cQ*@eg&s11n@LjB3FxQ*(ecCv z9(zFKU!jdUm-&f9!}#Yvj?QAsQ7DR{AH*QI41-IM;4s4p?)LTls)xN+byb4Ach5dM zAts)Buv^sPRNYk`=SJ9GUv7n^kYcv`Nt@_MnZ4`>w3Q#sd@G^)KA{nb(UP^XJ2V!P zGnxHBW_kU6RGxox&4n+|g9w!k7Wj|JspZfj7oHg39VDCIl)yX5o6I+!+`@uvzK3CB z;gyuX>jU`1pbP=0TL==;k-qHJx>{?Alqx|z6G69?Jn^=1xHYQpo=AF*Tj&DQmv4-aT zzG@^56AbF9uI)D7p6(?0y@f9?p()Z#HZao8v<_5JN1Y07Fd2vql@B)P3!acq)K_S@ zW~soXzt|VaK9n8+{B5y4B-ZjGk-}C!$w-OMV+wo0*F7 zi`yvX=AIpAEejMM5a-+vv}EDwMpbS(mBlyNS@Ilj<31Q=$dV3!@6YjyNBXH2J;|KV zr=`M79|7n*@R(f58#bE2h#zXK?UuXZL((#}{)WGW8~xR_-5;363?BoRO4Xb`8~3zN z81wAK2wS$rl0Bf$ok(4seZBUOQlaFp|X8`jBKNG8zj-LgLX1gX`?$! zyusY&^}N0019hjG)bDsilHbWX!&drmQlzqLT@r7p<%%Cqmh%TSrr7I%Ky;#4^i|=S zoClu;Ryw2+P6satg$aiA+d`_X-CqofKVXBs(TtU#oY}X)I%a==VhE%z$nrJPdzae+ z-0|ERp3NW-CM~942)EC4E0AnUsKATynzBB< zXtlLBhaO?7JEoNfI8g1n!UTl{8tgU2|JvG*m;~H}+J3-GykQiPS0Q1=pc@Xl^8^$0 zoY)WzsL%Q&Z15v{uu5Y`YM>ktguVoUTwNu+TLvRT#hsFRH&iXf-w$Q<1z&P3P?cl5 zGOy?TKB4{k+iN}QfL_|?fkt|_zK;+0F8~^jd3#CsIQaA2 zS-e+&i$rwFAvg6*iIi9zb}XZRU#Zpnj)|L7tSHJ4V<3Ta_B8|v?Yv?jgvuSe@>NW-#)>3M+2#{J!6f4ES)NGElpo7YW6<= z1g=5IwJi(9MoKMH?T-GP0=4pfoWvmz0G7L=L~t)#)lr$oFht4K+F@{sB-(Wj6FvZ3RI^WiC_?`PFh1WmC zSypt>OXfk&fxImXlJ%8Hl_O%9*UIrc#DgD1W>egh^@{rgAk_lmzHhC~JnzazAXseT z3-wFbw_Xx-a*U$v|6-m6X~V-*d*k8H!4UddIECAd}qO^-0?QvV() zaKEvPGb2j^9aYuX)ubj5!-gLqT+;;|bN3a9#agd6QoXySEEl|;79CxX*r;Gbji_%o zrU~VKMSFI)=cHl#JxFhsdRH|P7*PN&tGfs9W{!F{nrjq0P)6?~9b5lC$gxqdi%JFe zxxaSJDlgm6t5@~=HqCms-}Jk>WVF6`93W0#KzyV1pl&7a4=o7DB(b2Ih!^!%f3jN% zA(}jc1TjK{9_bp5>UaWS%lc`v<<0NO8ROL8DmOG!>wQ3uMc0;=XzEJ6ea^9zyyxIN z+e3GOZ?KX09T>vuPDCJm#|Wr_B&XJ8W0u}1-lzSy_nmtS!%#s2)cFO2?A1p9#1)68 z8$g(vo0Q~R?KQXVy9!ct0pxO{u^QQ!GMJHhfMqSN+MhKv0n04QHqo0=I#sJP*S=|Z z2QjYWuG*HXW(jmn=1>ZGudqIAX;W~ek+LXWFRVgHZY{V-eKDy9N(dkW0W(16D`#$! zt6?~vbFrZERnn%0&d<{a$a_*LdojTjGEu=GsC^F*)DwDGO&N)y9SYW2jo^lQjg2 zBLp;;XIaMbEfuZvrq(Nv`5JMG`r8|!oPCh)`eo#%f}Sr}h2wY(;IX^$&jNqo?sA6ZtPSRnMu73)fe zFAy|sC+!~lGaTK}r&_97rvUQoW$EvuT9umKNXx*0NAuAVPjz_;;n*f%5P-;r6aC#l z9A~0(wVxa8CU~3^w3@(S`V~be^T(vexLf94OOIkVj#4G&9RNHcJr!h;&;)KMvK(;I z$tQ0$^HDy)wcdoz?L5D7K2*be@yeuVg>pYVx@RXdZC>jEE(^|jM;CWJKmFPR_zl{r z{REjzw_eQxya$%Ctd|#w6GMm%Xj?{wMN4|zprIQDz|e7^)pA#kFU|@^@oz#5V99}< zcR=&-4@{Oztq|p&TY#Ti)N86q)=!O~0{3K99I2r^C0?EXRkp(5kDz9WoYHifyDsY-OJKH=;_ z`0TyocjJ=EmoRnz`Eso@1Hyg1v#v{sOV6e$1gRb_{`+n##i#{M40 zk*`N)=e#b|NGJySo(?}@9=G$EOZ#5XkW=e+?UhENDWEY1xAgLUi(-P&1_jcv)il(J zzem7bI3k<%Jr_o?PZd)#_+ab4r8l0yFk2LK=1s7ZIreBR&g)y~ygOGJT?pqH(acmw zEk1B^Vs7!@lVKf{eXlR<@3H2q8(FD>fe<2dAzZrPFGER~#lpK1&hPy>_bR1jQe#K^ zkiYHD0hJeUq&fH~y9iGFin_-6TQ2C8bG*jg>90Yl{Ql^w(kgMxq-`iyF$} z6##wQSw8%I<_@$aFX0y70W3TpQ1u|dK>Hc?sq$QId_2)~$F2vn1ByW}c@*z-`uMi^ zVsWs)uEq(Z--~Qmlt1q5Dy3K|k;13D@@s;g$l0MG%O1^(4@T_=#(Gp@li^RJ!&e`mbVtM&5f z_jwnWsMO~}&0fz_0P@78lF7*a7VaAkw2>f^RM znvos~TvAb9l*_afa^UP)2C%4u^BJLL0d9lw&~ioja;LJU*3E6Dh1v~K-gXe^S?)Fp zbWtVtTW(hTcO|0VP$TS&(S87s8m1%K$V9&SD@4EeakmBtOxu2{w8`iDxogwvm@n4!P*K=w!Sgsg z0J0S0je98QbCT(dk%x?)X8e)LzMyf?`gR0)rv0`gl)N_~$g%AaMo@V8(1-jWHxQu* zln>(l zx_QIIcsT2e+T^m~a{u)X(3b`!MVYVKg9={>8ZLuln)piFt1K6n@<^7WjFzJ}VK#Iav(Q`hj$4!h$h@RqK;ThP1@v%=13S0o zUGp>>nnRb)vyfe;-T3^K3AU+KD#n)#-zM#8YiZozHv_s-9_8JoGJhjyPu@6{>zS>| z(Ca^ZV|@UAfw#T?s9AEHK|qC}{PMKJa`uLPy!0E;Uodk(g4s`Q|A10EA&jgKEy?0d!(Zf3cS2u6fGAAbIM5+?c(}!(m4dvR)9oDh>efmpQhPp zWTe1bUI7ogK;V4{mD&l(rg1VpW~BfMkm7xUHlpSBI`{7@3AQ@>dP#ueGsgwWq!zys zKzRG_anZ}zynU;{9U1h+ZOXr{7e;5yfsm@0e)@1|>ZEkEaj^Y!Bhrlp_&*uPx)!!z)%w6&!LoAzw{Nnk;@HB#qlg=W6eRv#%+gfe{|4l-~ ztk;lgZ((8dK(g-hVW*!Zm>duIyGQf+fT8u4Y=Rw|+7f{SkzyEPRW6f1)*wP1W5xO*VXC zKb2~l_?0h~Ivq0$2jc4Wm9sTPrS_QPHaLs{vD1~WyJH?^|9Q%(H#?tv5OR~6lZVR# z6nqCrxC|t5g!P^yuOorMd%hsgHM05JlUy#BtPP6Q&<9r*+}Cv%;*rdFUFt=dI6d7Qr~HI*zN>fp`Gh^`7^@LsYi?(ueN&>{D;f zYC`l<~x*Rd1AYyi%x|S&X`T~-`iaDS?Xoqh3 z#+I~--{!a3&=7k=lbgn2?3+_v^h;9-gkYCn&Y!}ls?*=+cqwQGmMZroukAubGap7# z3GO)wUB#HOs{+mulU>hmF@0xu$j=L?KS+5Zuw_LLm~$#OzKML1s7G%4*G^`Z=mp}t zx#SIOZxjrwj@Ba_U3Q=6<~1bAl>6YP#fJw3WuzZ9{hrzu4-Ax0+haajM1FWAbWS>r zWvPo*yaz*l8Z_dsI;<}Vz4twpM)Wr?aG~GoC#u5;&!><4%o*&mj{|01-7ClpXuxkx zv6L){c#|BIrl5NMZ~)S#Yx=T#zCnJhpj^!6!T|2Go5D2^ZlAKKW(u`B zl#j(pdA!=LEAm$0y9j^R@|@yf6E|n9>8_WzvaX!f8uVErorNU{>a_7bhbTGkwhkR~R}aA8U3! zwxQrP2fjd4=K4#h9M}B~5vSfOz6y}&|EOR0w2ldC3}RoMN6~xtLl9=~(tv3LscWBD zcb-Q9#Gel?Y?4dmrHXJ?BCII-{rTX4uAMeE9)Cc92Od*5@{%;5LTVIAg3ab4?@Gjs z9-n^z+j!i|V&~Flw@g#tq~8(lLNp*GXnlQ}ta3gA8|0U&IWT|;anKR^07gl`=R$n} ztRXfZ%vY|7T)TOs570X3Z~y}HwUX~1_Pzwq=mN}Zm+d)oupzh2>n#LuKw?WbD>OU6 zQ#TE^wd_Xn&D`lmwol7eTQ~&{E!{f>cC_*~I0K^{^rkCo69q$#A09xkS*RwS=R@HT zcG=)J;0TVrYq9ui^IO7k+lUK#A4$v>g&-@y(; zSlG9o2q>62mM^kV9ZIgB4!0OneU7&L@ot*0u_VCfkE@!y&XIwOd zBCw9WMXS9dOBmRwHJa{Z?~x={hfg~yD$LOWwec&;&2b7hl;RWU23z#LNFC$}ss@@ZSF zT^e5d!DFYlrqw@KTQ)LRSn~5?5FcF?g0cbby$5~Z`o@%_mEAE)B`B^fIkDe=2g1-J zY&yjvtUeK!BekLu0J=C8J7Ui<6)?CsX`ZLpv|d?8eVtozoUD?a*r}EQuk@g#V`VxX zx;p%18Y`M`fU{BZzxaR%kA%01QC@gjb)gKQIc$yT_jDnr@seIp-&&(!x9Rx-h|)q+ zMU2MEkfW=F2f(uf#v~>kwwqf?f|j)v(yy+NsKbnx=#0KdIS~m+vittho)`j^=tWR4nt&WU`wqp-{nGWVGEHl&webC?NW^1YkRPf$ts*e! z4rJbs=9z%RA-PSz-KQgl%p;il7W$>?W zt+<_E=~A_Kt!x7+*i~I)(RA@MwRksFDG=h)*1^F_&46ZP5|o=#^339XR)`x$<$4>( zq>%adgb(sf{u0Z6Eg`mvE5YHC&wn#b?=6Vuc?ObmDxoVn2P?rIP(Mm&UvE+t?EM=N z36?(mn^J2u4wputA@TqOV=$H9cfNbj4-2H5{w!ggM7=6enP3c^MMFMKdH`HXWFh;h zi+DF@&xbUK;g@I9*HFD+lav5LllPKAi{$@U&~#sXr#h_adI+8h^ipjB-|`rQCm2H3O!#6hg{b=y&VUJe4`eicgOUX(U=anlr64$NTDYNY)moIu~a z5AGA2^3-NG8PF#vD%M7l0Lm2!-@sGsN_lq(C`^`0T1wXRctf*HSUW%#H0>Pg;ffBf zX4X#r0G)H5xj3m;9>nTrXme)4GTa6%tIk6GZc-*rYPoN!?s{6rQgmY%#R(?xHNV-M z6~;iIhq&#haxrocAw1R5m-02RmpNNw<#s=n|8yhT$PB7mvGE$r?V1x+42Ub?0{Jry z;f(&y@x!uJ$93;(NH}=-&~QHZ?2?V2-`T}BAT%Ej7a5>sG@!b@x2ENYW&_5ETmxIi z(PX5TY-r6-jO2@lrph5vv#JP2DJD3@iL(YUh*n`h|v#^613NVUU=p zE{PNmNyOAuU|AtmwkAAA%M+Ftl?&v(;#w5KW+z!7#Hp{aCHX0l3L75<(q^zb;*eKf zQe0l}kaZK34l!2~dEUuwuL^{v%o^Nj^bxj=*C~(SSj;Id%Ew~j5F|?nEe3qYNL7H} z9G*)tkN*|Hjyq;PM9x+so-j!g?iuZXIr1a(_HiN)!sgRxUz-6|$IQOU1H<=eIey~( zj8AEmH|WR-r$f0+^bSy_XJ7}s_QGnBsTh8;==OD6s~){*2}Qwd@D#f!zpklz{DZEF zGI(!D*;gXcj;*gkV(%hE6xFn}hclK+x2aWfQH417()j3yk^91+CZ}nRu9b<%YoBJ! zL}*gClr9%k{`rY}ePfY?ZMVGI?NMWv7;)ztr22q(WNJvT0WkIRWdY$ua;I89iSz2f zp3xs{Lu;lf^=wkXfq_@Ez9Bxju=RbopGo(HFnoB9%XUKD_DZdWYg7Qx$iy9!Yl!U< z2+SeVIM>kZY5Nn6WH$HmcD+QHr0EKm3qhMgY=kI`={wb;%W)K9@ideLo;@j({%CjY zye~2U6<3%gF2Z+k5|rHp3iEpTl5`wJuC3J{D@TLD|N9biC!!q$0An#`ET9-U;#RlW zVO&_Bf`Gx82kvt^UD#~Z{|pi>MePM9C9gXUD>N_iRlo31exdZcRgbCGM>x;n+0PEJ z#u_SP{I1Pj_Cnv(=habi==N7U>pZ{B38`N%qTV^Zo0}y|vM}d600En~G7i!F`DpAL zcoLQ&z)K+AYY_Dp`n>|K?v42UN;@9!c#THF5i}1sjPN3P%sI&~7}nU$gTpc%Er$Z^ zP65?H3nw$Th4){!r=@f6Ua$Eg*JI@_aFtVk5BvJiZ^J)JuSUc&SO6uHZ#&OEU!YdR0~f$wv-2^aGYA@ceAx}Aq|8m_$z1cVu@ z>F^HT#Jew4MWnQ>GF_i-A6Q07Ov~Z@V+mq0aE94@6`?>zR3{YWDw`p(qQe523N@|< zXfz*@FltF8-lz2<_aF8`ee4XO1;0pb&Ahx$60mQuGJV-7b_O$X&x8tV*Jy{2WKa{l^xQc3q5{!Je@T55Co zrnUZ*k2NK%aPd+{<9^LE%mL|b`C7f7cVwM*JN2049+T?Kp?^5y$9lrwoVZgQIu@Y%g@th00FP6WIzXmw$UfW zB#azf{TCr1S6AqB+;CZh^4&1L!1gg<&eoZ*} zpkDSu5XzlW<~k&^(Y$d()iyI4w33e1s4L`nhYZ`=-9`a=lkOdx^;P$b^)Qj45K$xU zHG7qN3M(7W+f`;$=SBOJJ|9tdC%YI$8|+S|_-Q-6 z!Do0IDL1fzKCfXd%lIw9DE^SsFo=Ksnk2-ff4#^Wrp<{%%reA%RHb*H`m=+kvzTn9rS&`5I*4E*8>^*d$6Y%*~pBLH(J>ePgvjPg7jwS z2Ra0}QbtS+s4YisIY1Fz^^9^1AzJkk_0i~~uFmpJwlXBH%V`>H1rufw9q4Voej!*Z zZcxSXF;1MrMddFra-knCk0rV&3-|+0-vNm3LXi1djwBAaqznOO7Ke`T9g_^7ofBWS z7t|iDCp$&>MUQn%E#L{yEqaLq5FR>d-!pKBJb*`_+?#DBOmis7>OQlvP+)D107aMU zbU+6_DExfx_0&Fb3=DFVi}Lw!2xqO$eH@4uT zJG6&1xY`>7ndCb$1!k|!H;*9Cah1e6#H55V>VjN+LaUeWA3=6eXi-bLUNll_N!NXB zOXK>YQ<1Xg-@ycGvPrk`FxX{gaF#s2RT;BO66xv@8(lF5@lq7+`+D8l?de~*;J&cX zp>UApX8(mn0xMWDIz0VxcZyBlSY>+mAmh97y{a z!`gXzo!Z7){3||nn7{xyFxSNI2#p*bLattdEVy+9-!F-SBu!mv)ibg|E#M|M9xxFBl+B$? zMrbIlWg9%D7nPr&EtTDZ#VB>HQgO`X;YkP>A_=a10Fx_{gN#8n-Q?13l;qMbS!Dh-_C2lzo zgBO}E<-rTfk+WitqAix}W+Hx`m9K|}R+>6ki_02YM8?*=OcKCcLvTR9TfSY79lu!N z=riv7*ff1!@(am^7Z_~onJ=y9_fXyz4CO4|3ul?kCiT7w~tyK`bF;?PeZAD>mO^! z2EcQF5TKe#a8H-d75Z=N6!VeD}Wv^G&ZEk>$%Z%D?wz9v+FB}5gb2Pmmm@38MCIOyE zf*EHaU+zQlgAD->JqRpH{Cp%Z<$%MU4$^C_HQnbI!3rueXsz>$(6@` z;=OCRs&$o1g^1h$k}!(tl_)tGR9N-6hOc;YWii?0My@WQnrDjDhD;xY?1N%!yd z+%AOKc`DGQdX?h7UNn?vfu$WDAd>ecdKq!?SC36U*fs-jLmI^^_u;HOQHsdE41F{x z3I&XQ!|Gt8{5XHxlUu97y&)HrMDCExJyT@~ET+!Ikmj-2C}j8)Vt_#Y`Gu|~<8a=k z<9ZJ_Z>bO+A3K~%k?VhL_4&XF2oXyi#Y(^8eLM;M_hKNVC%d&KD!}3*JeFM7@p2+Sx}Oc;PU7{nnc~F>=G{ z{ni@+$TRwRN(SvMm-FdZP!F`g0z1^1&LeGBA-4ZeHJeeQvXX zTbB~~Zf%LxZ=r)@uJt7FOPhTT1f*Mi(3;mQmGQLDF(i>zrOW%p25x|*F?!{ zjYG1?ZDs;z`T6^le2;Gg{nOLO%X$U-zTZ=$Hi=Pw0Yh2{76un1e}C-EedE!3`T$0X z6|($z@CtQjvjV?T=eSkn1zdxrt=mgJYniB!E{0I5T7?bX+#!4a+T(D1^%qwfU_ioP z7N3nYaU>LGS8XZb^i0GaHStz{(5sbr6R`jd3OYWb1Xgp%(EK?$pm3Ph0vHSeOb$vG zZ6+qt`j$3t9fl)Fr5WNvjTl9$_oIcLA=Ro7!s6MTdSV?^ZXN){i-D%cj-~kTL4mco zBCuQM1k8ZA73^5DM)2_=&>@JK^<_sMYCfJD+tlhmsFh7~HjLyBP7cA#6qvn%aSa?H zps>Nel>e$7=*g0+AMAc=M%Umkxjr+G+dhWMDN_FCv4+fdoWgC*47S((pagc8EiWrS zD&VJAITQ3FF~A4ryW5>E*CIiVPP5^+DA-&NU?6UYWywo|!a#k**253tbf4Ny=$pfz zL0U|Qt$fm_=|Sar?{+#m5T~?$m1XJYgPe_E<%>~LoHQ2;>NOEqc5K7J7p-u8YjN=P zCP?GuRsCE=xKVuEe+Tn%gyGCa!UgjY++JW|vf2Xj@p%SV6gHZ88asKaStVQez9lKB zorL-g%G2V45cQR_6th6)4xPSSpt;%PxEm9^i28LDNv8NJ-y1L}pSm|We7LMvl0X}= z0WrpO32+bsWcbEp29wck@^B-di2hM^=%E|E9mE4pVb}sX6&0Y{-cSzQd~9?}evt>D zH181ZwC{Grq(oN(*Dd`{w@o1cpufp>(fp8{+ao zxCdp=e5b{&g8S}YZ-%T4n|R=Z-?Dl|Tz z5&iuUM*$IOy%eTY3R~M*Ewr6V$=a53rxo(X5Am7|KNWx%XA?~tht9Gy$ zkkkwf&c+`5xUI>R%8RQ&3M?Gu+w-9zf^>aLjN>@9#ssA+ar}K?roPW3gbXhVx5@s_@>x&LU>IoA82W?9Z#Drrjx5|8Bt^tFcJpN z?4~FLedzeO*6_5UPpN%mgeyv{cizywtHSHc)r-}1&CVr{w)}eiL(J;W_g&AOY;2{y z6pZ-P3Vs$eDxEOt{nMr}>X@GKOTg|0{*8m5wum0|9V;3)(0EUQ+-RkKM*wTNG5xYh zn#gy7bCu=ONp^Mwcl_$f5SGEE9>i@;V1=+07)Iv0?aLDj zeqRuIpc@Nz@p2dGqphJx6l+MCdGCX&lutHpvPoWrhqTE)KUXP!pr{v!i7P*vvxM{g zVb@~Sxo)aGUyS2=9Vd9qYgR*1zAg-e@WhQt@NhS=fSf?nmVZC}I`$`cCn~ zbR+pnL9!vT3aS6}Te7}{C1LI&60EX%zpUlUR9L*GYw@gC*i@}MZl~XULvCqs<4X*F z?tM%%Q3)`^eqhe5wwnejQ;1iYm%Z__^~50i1<)0Ru_G)xVnXd5q$xNgT3%R9BbL2Q zg4V$R5#4H2vb!JGZ3!q)7oiWxE3E3@tEhC1`%Yf|kE8Qga1)85=m)VNiL@jKkwF5q zBj=p?dRKcp9(P-Ws&~&l0)pRBE$bbvPIVB?aZ@b=pIepA_U9Ovc*u9+=A z9qaore>&;!`{OwA2E@?$`%ZtKT+)fwuNx^x1tWmRE~AYAj$D^?*nWK(U2sZFBYz7| z4&QX(ZY+RkIvfZZGQik1Mb|jr>*R*r+^G$m)?>mZ6#>anoNWPA>V$=6C_sI+q&zeYX@U<%H-hfQxeug7R zkmE}{&e2~n8buV120*3Gf#dQjj=Zp0!YO%o@g0>&H)D|)z^8FRUq6|r ztZj2t&KSd%HQM^F;}r1A-jaRTiO|fyA}Xx5{)sA5g!C)1hSyA<2&&dx$M$8@?%OB_ zvfYRr54K}bOo4A}Yy^2+vF~a%y;2Ov(QPA>U!ZIz#<+is*Lwovig=6LP(w9%iQSKVY#?tM zeV~7UkS_K2VW;_;8^#ZU#7a}R_q6dB0=UZ6W|CF2LN{TugGuXpE4*iDnPG;Rk7yieiq;1; zfv#JEkP+?((9o@ZK2-XYVw31g5Vzn)LUul1qxJxV#x@A9G^cXoz2Z0Ge6^8;*Zs*rCjxY7%|8kx~FqXBL#RAu5R!|2Pv*l`cu0{WijP-xxT+(d`s2i@~z zP{K|dtB9*ddp=hr)S+>6CU0Vy&MjeikDPVKvb97BBMQ_PE4tY2{`$Uhz{-t7BK!NW%g<-xC6qNFR`1; z3pXu@xc1UKhHR*Xf(n@#9D%t=l{h6~bfJFq{>2`=lf+#1K)WRC(oBMJhwx*JtAFV; zt43wI`80*LL&h>gP@Ep^#iwKa*(aS=PN-L39T)T?GD~Sj)+8Vb!=kahYbBS42#BQ! z{1bst*s7mQBPY_cLenw&U8bk$6{3~b*uIh3g9&^NGIHL9tNwHx)7Nh!p{c)yVW9%B z0U;qZ{Jn%v@1@F+mJ#~2qjAhdcqi2G!zqTziVLdtPtEvu)jhG1yfi z-i3X_65-pT8+pxrZeZUK+(T=}{tH>JGI)%@b_)#zYxPhAe!uI(a=R%PdcBOmN+zjZ zC^;EyU%6aVZ~66Rv!I`{NE}>L6P~9gY<-0y$b9G!0;3zd%tSM2Rk6gz??XVspxx=j zC9Qfq`cv8x%8 z#nf*N@SCSllA(0P5VivL@@$>E#_#&ycZeMsqnO$_BM~x%>?6L7+?BV*&QA=hpo^~Yjt(T_SD+?iY6IB&U`Vk4smvOY0D}&-`jgDby=40JAuxw%iQ1{{bhiMFOmNTA zo;t60!TBMt$!{glYZ_2m1DXYUhoP?<7tW*rO^}KUCm9m1!MrNzSN;|@Kvm$vNT0uI z)@S4IvwGrKTNZnt7WsA@;jGOp;5Wrd&Kc=*wq=F&t?c?W#db3D((`Lwcj(|C=rvb` zKj+6gU8UjULHlf^a>m!7IWOJ+nvQsRV~Hxd=|HeYrYsB8(67|LX>ANCS?&}lae>K8 zpTOeKkLHJiG!OPm&9>Q7T~Sg7qecLue^2b~YDl|%>oEvE=)IqINwyEf7W`G>#r_dl zAjeTeJ0b%Iw3p-VyyN)zAb>-imkjS3MBP2Twb%Am&fWA77r}nk^;n@4b70)R+h3nn zdefOeLL%l#$BenQ-!~K0ajMIi6P55DBa`;*d?OQ{TOlB<-jiN)|?4)QG6Jfvh zYR#4q9cQ@{5;$em+7?7ZlX%{L{hYf3g;f7Co39n|@jS$-ExnbyvdmZ>#?GtiEC-#w zCN{jf#$2sspBj}yumW6nsAv)hjg2edKAwRBY;Ip8pu|ELQl&Se>3*?#%)s=Zn6%0& zx;uVVJ}}m!SJMK9&Y%%_-L00Bg?H=K`O^F6J3ue;1#(lC{ALyI;y$Bl_K61q0hJ3m zbcV0CkM8b0XrMVH;aYlm({rB$)Rc=o;9!|v$It#YVtJd-W%{l_VFm~3CW~&INhCN3 zo&$DO)HohlORX&i4d_G%k&bN?^vW$a;idwn@bRfGLa2J*U<5Og0cO=a)bu|$Ab^T8 z)<3?AiSVH8m>=_~Zn@}~U_t0=U8YmLCHl0`P zv>#SPEy#d;rn_kHdgjhUGwD$C{*_%EHFI}82?&^Q4vVna=ijK-!%OdGwXS!38zYx**J~I04Kq3 zH90Bs4xmS{BbT^ng8!)QVDx#hfM=beAyIN##PrXriuHPi5Fq)fz>k812~;6=poY~* z+o>MHfP0 z%IpEgg=rC-4~yZa0;|)>H{YL>GZM=v+eiG-UNb8vAp+oh@gNw-vZa3R$7t9?U z8n}67d3ESijAsLt{dO>IXx@S_O8a}tR46{FUNp`A*VR}eyu7utIym->uiAq~NOtQF zq`IJFb?sv)&O`cvy;CASORZOovem1!I|0$#+C79HxYLb*D&OSCTfYQ``&~%#YO?A)PdHzN{Es89=jlBix$eal7&;z)3+tvB0S$+NO-J{GD_6QkC zLdv*msC20*eQ5yTJS!b4rVv%i*2dWsoG&Iu1X+D$)7x+`c>ql%?6)|Y0?-KcaD#xL zb&@EKv!fEGIk9=CRS~|PPhlE#{3vKk;*6hGHcHv;Ct~wwU%GhO(`VQ}PgF-E1J(lP!HqjPSFMDu=PP9l zI~wzog46)a%3qXxu+BAO3i;?H1Egv^Ks^k{)i(vCOMDn`C~yU{MFw)poc=id^` z)!DB$_4kUS@<@u}M7QJM1GYC|orG~*dw-2zfNa6G#mJYl7vwij)GWu!sK?bdPw8)C zsn3QpA0U>JyX*!CMIOrxEON6h{G|cqUOLbDbP~bKRHMh8XKd_%gi)}mT8Zlnm7YrT#8|oXf+T-|(yy72>i>9zH&t zW39=&dFZH8#${=&fJ@BZiZ6)d-8J)mvDq2>6lVT`3sqZU4rIJ`!n4?dCzTV*5f43q z$(=7WO8c2Buh1irPxhc~SdMW3q}XLCz2C>h(Z@T{1~HdF9B8*RbndPD2d3xlo1RqQ z{LaHmdc~qQQY^HaAcRm<%h&#k{@N1wwG#-7-0jW9wJH)(Mb45%9>G_?;fA;tb)H`9 zkAU0jA{_N`)84_my&Dlud%f)kzWZw6*7*31lK@5?c2nBuHIiv?a(r#zY~~0$CR%i1@UcOA6y zBZA_2Nd+fx4~Wb=Y^UBqXNPR3c=Goj82=g@0n~)>*{T7}WNOkuEQ>_($JYAf0 z?-f(RV_sW^HI>{tPkU&_KJz4AsH#`6R5U#(GR_?CRnj85b#6>OKu* zX2=FgDuGMNx2|{Na{MrcW>H|v(NHCqL5T;J4pgtyEZi5JXweHCN((eC&RY_Az7avr z2G{;2x6}e^ugz4UtpVZS9;ksu4B^%50RfG^+?Rc*=s+y^Mtj)g#ly@HM$^Y6YL*tX zTN4h?ZOdIt>Ly1V$073_KP3CmKGrcQshKi1!hZ~x{2Lp&&bZ^|r&LbnK7OjZAJK6| z2ykQ|`zj@pd7k5LV)p_3nWorv$fj?-W4u<_b^>8+e^!^p6F{q04R%M60rZO|(Xg)_ zf9m+rBfz{XwP8<#_vQerKY+h9(dm>{BxYq|IingoNBeV%R^v5Q;>nglc#$kfk9Q)l z%Dp}^az{G0RB=R18C&nc8Ixwh(I#snH`2=S4(2z1e+KFR8u7{ineVns-eb|XJAyx} zKFg=B-7uf$s{h4KkMt?9M`L2PiBF3cE`N3e?iAYMy2-qv@{A} zV|^~+aybK9o-{mKn_z&My!Px_U{#$vX~6%o&}@y!}Y0|ELFVO;w(bomVD}a_;L11osN1Ell)XzjWDn)9Z z&Ex{hDmRM!j>S0XxtB8KS&W!DVCWG=qqpXJdtqLtv*P9a?3(iZ!>zT_rUo=Pe2?3P z7qS-hPk2GCAC*7FG1yS?h~MsB@9^s-90$a@X3oq#s&z0W68l2OQP5!B-4{dg$Jn$P zDb3Avq-bi>fOpjrS%4RVjFlXGR{nA-Eqfa^Q-~4miQQh-A{~CnIbXQbH@F>mqQl-M znb@BfS80tr0h?y|s2^_mkXvV?(e@;?oiR9JyDHKj$||F4*NTZye%Toe@}>1QpUv4c z(AOy4iplC;kCy`hfSColL~=v8;|TuY)P|_i5>ps~b)%qw4H04U6Uv>^xm5eNMG)4C z6LJDQ{7j@r{u$vdWx!wiZR+x~h||8nmWioArz#pqU;UT^G9L7m#4T*@)w9^lNWVaz z311-$jL5p#9-a|?Xx%**A3{-EzBgvv;`eg2c~N?0xV~1|pEgrDIex!h=GGL|FJ0!y zlYwE|X%S`gweMZ#;dXFgPyFz1$)*YQZnFv36m$CMlyIy6UM^`xc35*h_fujGL^Iny z@O9gOUQs3A}1qJPDf)*D6)OLD_WZ#_$C9as4)jnpuZ;Q5y!?^D);!-Mes96tPS&HcPmtQ)4Wgbcb|8;` zNjEKUD|xxxk;{0G~J<}xus=T_@&mwSo|L`zmhPwNcEWta~2UI$tSa zIo~mo*+I!)0r{Sx@4Oop-9=zT55ziBx&o+cw$t%*8jCa>j1|6oF}#et*sY#)ivoxT zr5(fSn)jA!d8MQ-1Z$E=x100&^^CwH3HaB#${{11v54Jh+;I1{bd<$j1t0&hNw6SW zD^fN^dOn0k(|{#eK6V3%L6Y8}*K?s^;T$gGAD>jXAJ38$Cl#=wNP2mLThoPJWfshY zEdI*llBkadLSTcMNCceq5<&f5|0%M^X>|5k5W`XQQOHzhEcFY4gVzAzpr@Evpyht6 z&1ykjeV zHLXiA4M$D#-s!J);Nl!|yhA6O3|yd4b+xshXrGV;C=fVcx&2ZqX-1qb^IPT|Sfzg} z-a$za)>lGifD6m$bQ$IC77v)&WMDzxu&<>Xr1zUIglSY>1=IXNwLj8x4R~}hH`+z# zl@UrS|5lI3Hu?>uQ(*?J$s`%TL~=+_sPU?}8K2H@g;)tPzhh?i1>%!H0{x}(>>nGg zK*tz}KB}$e^JC%%1KXlj`IOU`jTmr6E2VGe)tQVBkv$Rrz%eIdab0Xo152@AE@1#! z>!-gn_KH$3yKUDV@@`@c54(-n^j_CI>*U$`h^)xHhW&J~dau;`X*DcstS9w3-^qX* z1Bj8uaY#6v>}F)@rLMo9<+kISuS*IVVoGr?Cu>u)99mDTEK%Yn|8;W}l$<8cK+RN5 z24L2p4ihRN4yYcpR!EtC1SL;DpgiFl1U*|?FgUu~0-q%fw)e0gaj5P9O-Kyhcfyfs z_SDX`iUniu>%;rTAocvFU7yR8q7^h7swZ9_Aay!D(2;=EA|Xy-{(DNOmWV?a2;$)Q_3f%>s_deajOZo#s??U{*}*zP>Gugo1)4*o z$2C-^5U}#>6R4$F?*)swAa?x8uRL&#W!q#qD=6jE0Vac>&h9C)>RX3qFewHU0rZWru(b(` zi1asN`ur{i0tENssy9ExYBflGxjhe6vp+BfX zv=Si(=~PC?2jI{J{!NZ2StU4BTs(^7^%WG-RlV(+p;*8u;_S$I1w-NBQEm`&wPK4C znha_@I1aXs2D`%SWZdtPf)Yf`pJYdNmLf11;;g(V#&4oL5Q|+=L2cffx^e%-sxgl7 z5d>;&LsX3#H3)gS8oky@hTE21q9(eF&8rp#-TkHoLWC!dvzsvdO zqfJ7IW4U<|#D%};h&j&6vhmjn;~{MqgRV14(-He|-qf+5Zw!Y|UZD=22&8A>3Tl8j-TP}&;+zg%unZdGSlP!bE@))&m5zn!s@kLrt!$iyE(1U9J* zmb2De<7%PJzlS^T#9faoSRcl>hri?EP4~cm2HitPkWsB7tzUtW+PYout(pPD*I`_jG@@%sVKD^b_d|a!PPt@pooS0Kk;G z$MET;V~>@%0=dHglSsKCa|!?vv{sOKKFq@0@@kpvQP^k0&U zD)g%pkO@zak+7+-A2rebxKE`8fm|Fkq(s*U|wOuoB zi`UwkGmV#bz7`mMv)-}ai`krBMh9e&q^)U6x*2z#0(ypiJaCEy#gRcf@hZ1oZ3Sa? zF$C0(jD_J2@PhM^^B9xaD)Tp$Q;TbSFqqZYhKBV6f@g7!uuF8m1IF2DgzpmXyiExS3mQYc1T8{> zLcI+$9WkjEzoPs`j(q|6UUga<@b_<1xMgD`g5z1$%!5wKy>YBsec#Qmy1~Np4rxD} zo9u0MA#<3ZWmVhXF#_VPMhOYDpuPgdfmC*ZvS%lft1%glU&GOZ*AoU`ql$Snu*YP_ zgBK5|XY4&caru(4I9I_!+t>48wi)+ouwHpH$5!Z=JX^khDrhu4NtNcC(-en?@?`B8zv=vTzRr8e|~0aKs}#j>!_AP>~B zv+GS0Z^>d>x0~&^zd6vrfxzUG3^JMXQ27Gn9Ujkh9?#lB&jxLfN|d*&HJU-GP^~u@`%ACr|!FlDu0g$TO0R~pw)E=|0;S! zI7B+X^u7naO}0b>8a+(f`5|>oaM2)p5(5jn9q?9`zoGp{WroaV0J*Z2)NStDzV1CB zc7+Le>22GiOdB^;ZMK+c8bJ$%73v0%ELeJ4e(N>Dgd}Mw!<@mW=aGND zKPjRs`*|Pt8&7MKIWGALIRzm1$p4Z4F?5~KLTd?9))Ss zxVFOCn?5A14_nJ)!B@uyBrQR0V} zHQv-{RPS;IdPyPmD+eLN#8{>R|G4)lj{Y97-Pott^*V~_mmgBA?W{A>>g&GKYwH?= ziwsYjTWwj6*AJsc+0=Qq?Pp&Sv2%!A2uQY~!2e`)6C!HEqvBV6g3%76NJ=(v@lCpp zn9#3!)iDpxpp(HF+yS34ss1{QZa!VBM@F!e-_~1iYIltQ{?$M@>%weB7AN2;)&ab_tSU#{C+p z^i8jLAq>#S16_7*+>N}MuO{EXXa8VcjG$)X&u6ls1<;S&37zgy)1}WMcSnt-p=1?Y z(pB=Pd!SMN$m(3svFWW|z0Ux4c#{uoVem$m>Y_e?Slzr5`Ik_1=L#*sVh z11>!^^evXGlaGwrrhR`&{kzM1&0q-i=BDf0spR`Zff?7!!&fE^_Urv46?uuTMyq5! z)g-yaNbOt)V1(h?H@813n`(%78M~2pu&Z^;>ohY!0RYppPvin4 z2lMwhZf`-rZA}Go>Ygm7jhPe#Lgcx7HmNA96VcXxau|h_Cd2bOQ6h+4fV;DUe_9F- zX*x;!8NW2Wof$v;2v#X?UknQ zkM)tHmSX?jD%f+l4+b*R01CJDBCWx(pzjxo{H}a1o&3T-S?c9B)7EYm6;YhGGQEzM z33tjIWxS59FU-~KmgHbx1h(kkH#9gtUHq~vUyZ{ke0cCw0?srD9S;1JWlKi~R(b_^H3+RVw}IJ#hTIyM2wvlhpc1qcTbe;gzfGuJR}tJixi6@&WVQD zRdcuV7WiS zj#s9a9@C6_nfQ1xv9ry_!77doKWV*Llc=2X?_7)hB_hiu{R*-}jU=t=6d z<%}0IuX;;mk&+1PH3Ws=7;$HFhhGo}otKrc4s15SC-{L|F=`qDwmkrLSMGt?%95qH z#Fdx)!Y52M=z;4yo>=Zbz9S{KQBSVZXeV4v%Igc5#NvNh#Q`6-0rHN4Q02X16R)1K zYufx$I%yz#db8?p$l1yOUWWqACKT-z(BTg*S8PI4Q`jWKFXc}0qjl*$-X{BNe|hSo z^WFP0UA>&#cU^8)FEt7aY#wZ}>tj;M#37kqUB)|*Y|QD&AYg(P`O5&yJa%k+!b}kc z>TI=$>wXUuFf?R&E?J^nCbq36Yl|l^(3+Z{6Xk*h!D{+^z{=BsUkBP_xtZoDNIhrV zC>IE@NvvBA1VEp}Dk~nemCDB-lv+4tBx5L?^mceU#_iM}=NR+G84PvnT&qMz)DJuA z+D3VqcKV{ik22=bOrMpaSze83LS%?|t$t!S7qd&s-l1zOPT%dh$7+sdnkwVE7Mxhj z;u7%NPff-cc~Ytbb>Tnx)=LKTeQ`>TrPNpsW0+{9vLu_uzX=Io?_O(Em_{$c`!2NM z7hxqSiq6geW-K^6gcY=$15yt1m>s5k9mHK2( zuje;oiWZIOkt| z$J+JxSjH_5s9a0~-z{;S1PCKjlF1aYGD_uPBP!4rJdxK;MeomxPI6%CL3*4*>WM&doqU zh$7WX6rt3_O$C@jL2*PTgJ3yn4Doe%fx(a{>jK`?qt=rN2Iu!j6*SPAXI;0V|rvl9DADpcvkMQ6C2E zFLZ9h9}6sKdahAlGmHeE0V47{$Y) zFBc3abA;IVN~-cGe^lJ0e8Ww%zC_pa3)%JCr^DW^yKK5LN z{=ux|Zc-a{#)Lb00P;-6vXv5Lj5ZSX1%ZN-s;adC0~XZf5ZVD%=AWGUrlJl2Q66+KSLBs5si^hCV;9j91XT1q*+*Pd&Sn3?Kv zwlvueL7V5iZa!krrj^lg=W1y}@%=}AgIsV-IPe4i z3n6)NrNvRupbL7X{W5~nLsH^}C`>N^HVyXb4Ms% z4hl<){bLj`n&yu45j)L$2_OY_Q}TG#C)c8f7KRHHc${CjHjRszT@c(N5Ky?E3nYT8 z{UkRt*)J#x@l~J-7$L~-0%(J7zj>AD|pROyyaJjTl=);XnysAiuT_eRT&4 zeo5E$qLE<*#?pb=R60&eTwrPiqZ$&~s{Ze7*D{o*_YFPb5 z|M!&GXn|b-6nF=kDv|wK;t&#wQfj^uT#G@YNi(_T95u-o6{X_j3BQW_n_B{R z;R;De3Jtfk&Fj#-U?`hoGz{>%1Fy^T&`lm0e+V%ij*_Ho;Fu6LE(6OsA6OqWT8KB=lD6mSt-F4kls_#{j8HaYgQJ^rQu814yB2Ye4}NYAc+&{<;kpG9Vc zolx}eDob;$-B-2ObN^v8`Zq@ANQY9bEqMDFpUEZ3meLN|4G=OBZKdSE*#W7xBIOGolicRa3E{x7U|(-Py4d80L^J>6X@wh3 zBTc5*Lb)HdA7A0`%i%YR-N%wgPhb~oj&>e4?wYD6BLzLmn0RWQRc|u{!58#-u&}0J ztl@!HtJ?3R*Ffl&Yp$=!)zKI`1NHo^0y7KD_(XINul`n1U`Frh$GriJ<5USVM0=$5fF8fveuPf>73ZDZq|pP(HGMKpmVnOoDNTe2}-cvEKR zh|`$AD^<`@=J=YyZ~H+jYtQOiVwz>DS2UniB8kb=jB0UC3A-y@<`mnV1o%(AFjISv zVkdnL@l5VwHtWdL%S;iow z{qcVpaYS=09h9iwyeQxP8?@r!hY#aUaURATzZvivo`9j)Qa&sly7$EqxN5EgD!QZm%^hP1NlEo@%BsuA9ywMdKy~lKX zmQ>q|4)e9#V^^uF+smAYJHMx$dKS&yAGtflRIMWH!jRo}JuBeTiEptB|6!RY8!QWf z0`5sb*$55-v46P}7Tj-UAZnrpZmLENqiK>)atu|vx&97V%ELpoy z0wS_;nHthrcJw7?wHh^0I13v3&hqADg-i0cXhOjp5^OB>M9m$6{z-8^wAgQp+y1fV+N@rM7%N!YNeM~)eF8Ze-%Ue*|C;3#ylK21;8}j7 z!-7?P*}4YEZ!k5S?NkGc&q<4xe#jrRFIsqjvVvDB?OI)~HZ9OAsOku|Q!A*l!n|cR zbZQ+`o3m$nTO|TP?*OYsU4ChF*b)P<4p*Si;R;UPbJ!lzo}U9}P+?Fh_(bR>2n-42 z+8-t!aw2N9FSy7JC^1xIj3ni@{sUTXJc|Vm4yfanTZ3d;4iq!2cggkK)Mbx9~ZcoAk!&eMuGecno`k*Nlp2}Fx$~Dn5m5oKI)#!ku3buWWl89 z82qE?JeFHkqA2=749Eh8$_afYCoBu?!Z`Q3ETTB_-D1HVj&p6sFFfB7sYlX?{W+XIRBFOG zms;!HsCYm{2TG9b{B1w`cwg;LH=bTt2=Ploxof!aqvVBQ=2r z#FE!EQJ(A5bKS}uQL~U~0{et^;6Xx=Hpz25So*q)1rcLpq&~p~ z*meb8GRpDzk^HFG!62|Dyo9WqyNw2aiyo=fe$Rf>BBmdBi|cS3%aPt4* zI^d=LzUmW9n0j8aTTxES=zGZQV9ZAcTHm*j=;r}*ZH=9i%1c@LDRMdN(&2afTonX| z{r5TEzXlZxNI_z{ovbjfJ7oRR^!5@5fWMyK#kj>ALV$F$!cbG-N(CQLS8o&}&A8$G z?iU0)OZ5H!Ub4$hHT!~5Pop7Q#Kh{{OpZ=~g(EU@HXS^fZ^HzYebUkIt-^s3URE}^ z$F)zQI$)-IHIpyqGaM;rV*c2fCfxDI*=^Q7_3>vxAJ$`RJ7c zDss0bjQw|Xmo@70G?wGS5TF1Zm4YOx2L+mG<>K!O=w;%u)=4eK{Pi}Plc;*u z!|(PQF;He8vj#=hAV;Vz2fCU}>yn$-#^J@)rq6VrUIRbrd;>b)tH>cg4G*+gLSBi+ zjg=lO^uv!FTe-2lQsLzXHt_P_?O{pM`LypE!H1G9lwN>qwn)S2UaPX3DNyizI&}l2 z+ERSboaiigz2+FETL%_mV5mq|kh?0;I<5YB;E%(rdJyp~U$Rj`=S>p42gP7z9{r)M zeiiyVdhwr+E6vvXnErVN`gAX08D9}A^gxXY$=j{lc?b~905)?%_dVXUpB@7%w^478 zpNExK5CS3CnkYJ>qe{XhueC~$fMV6|{0W*2EX`Ke9-nW;F15u#`s`0uk*P|6q&A?N znm6?q0gZCMd`m}0{mjGK_<-lS)gfK{Q6gluc8gIZo9la#QNq(Q)FJ+~gI`DhR`8b2} z&AeZ57JM3)gZD67c>%|;_T)tCr2<65s+83i7u#}h=Xu*PLUA}1pWhXCJq<&N$A&lE zs;#AYcCXefqZea+f}-g#TNQIoPc3#hH_pfaM+8vTU!M8>ewWq&yaaMud<21Bf1Ahd zT&Gn5sm8Xe*tvm{97uq-HpAm#bq?6vst5|8!p*(S)c_Q0E*?&(Lm3Y^gToBzzw+}X zXcf0(Ur*eRX3W_OW1_ejg_z;cquk!Lz0;KA?wC^Z9X7l4xSH@}=AsM2hou&o4#@$t zBsAHyKn-s%`_;QZv#YHFYwAfL*UXQdUj4D}{H*)2H;lkA#iFm%7tH)mgVqE9cnE|f z1-yvZTUuACmIfqaVetw(m~jBiV@0i*GUXc;LX7tvi+BQ1{;0+&o$%XgfdUd?|0-N> zeK>z&8BxY%m11C49<^Vh-fkYrS)U9rr5L5O;O=Bu2X2HJ03vBFZl z67KEPaewcwyYQleh6ik7q{4kN8_61C4bLl&efl=FZOj9pRZ0Trw5HduY3D}b*;53o zQgyV6NzRw0n72J@7k}8#eV=as_Vr4TohsE&UxZm0Xo&7vy|TCRD;!i>84LKRj}H{j z;sZk&O)B{nJ$>aBJ|u%>8MR^iu;DR?w;lg}K=C{M>IS3K0{mC5f3>Mx;2F+%#Gq=# z|3?v)>ZC;V`IHdz#kKbPX|IOy(>wBz{Tq4U4-Qhf+eeWFew>zY)!)aZABA6SO2c-{q zouoOBVoNu8oY{QMN||40jJ-Bw@2dy<^!-Uq0vPEy1ahhBdWVZEzS-xdV#xT*H2U|4 zDFO{1+mP)5o-`7`lkLKnUmfUR^)e~9V%+nulo$}Q(9#mJl+pZs5w1; zoWTyUl9teoL$^@jKmHVVFdos$0(#)l^9T^--+0l$4A1{wG~7bKFbqg_^q5~@_zyu6 z#c7@LbO+x-XJ#>Xf@=m|YneA+bprQ#Zb~D_d*h8o{D?MEqhHzO5|9 zC=+s=*AfgYQ)~2{&{IgUIQ(E5rb1CIx?R*om%*a;23yl8C=T_TG&dMOx#gi=qvde{ zo25yF80@)kJ3@7R^n7YE5fE=k; zd5t?|(0fdpW&pUi`3-UF-f7u91A6_%Fbxir#(0+p>%do0S(s8ac-kD{wf*vRF<;Ak{4kuSR67dS>_d>T@@q}-t9qm5y(IfCk?m?-I2cVCwgZtsf0q+?xf`lNdHh47A-gk}sH28^jRF zwHOGZ*7Lv$DX-`S*8~VI`u|EubsH=%MHM>&dUbUPPCkz2dz>-N=bxW7=ytXRp&e77 zi1&4;KG?s5oHtQARiM4IQ%3H*WjZ z<#K8uwj(Kr(TRf2_K`sGcwn#Q+IuSN0r)NMiGkS+8Ex+yD_pdfbuJ#a1R$yp^;Sw|6n=z4~roT_us9ARb|CgkSgn z>!(mBREx-$=G6d0=F^Sexo9cs?tnX_5Y$l~S8Ce!59&vTodCKkC!np6zf6A69ny_IeiiOhCh>t$J=LXk>|{ zSd?|zHvm0C5yx_AUfEr2JIs8Uy4fU z71`zC>sN~mi-E=X>wmK`Uhx3>1rt6-WI?|-(gDV@KVD4h$nsv_RuoqkwSnUO8wdpF za>C=Fyu%}|wViaFEXM*$cO34_&xO5;vKU9^q)!<5C#GFlIBj7NTpvNU98U^mIbIQ` zNxpC(Zj2QUGZbzQk3}(9wL(x7n#7R2TzxvrNQZeZN!Jp|rA-McB`o?ovz)zY zI-XDoeS-IVjs#SwaRA@FZhqygS>bhdp%9^Hw3!$jx;4r&*Pn|*a%kaK{R*T9Kuxev z(b9#Gpp6by1vfv(3U}}^lbUKpOV>Wp;E&{sUAK$Nikm9<07OM60_?luhKp57ZMO>Dfg6RE zB^u3|59prE@I^yTLRW_hM%E-^t+LlXUkZ&X2j4t(1ZTDc?omK_Tn$hN-0u}UsX?}9 zt>0;VHU|YoR*;^Zk|N&0Ec>;c^7_w4Q{)K=`nRAQr^~DIzP$ZpcD6z}A#OW$!aCam z53FQ}Y@d=H-TboU)Pw=SO5OfJ%uiU_o{y80M0fMP*UPynUH8#HX?2I0;g00ZeMl1K z%FkPR@$>9NPx~sgsQ+S3*zBnqlXjiru*Ic-JscA;R}ooT)*k!Qy2p|Ll5*EQ;~Qun z(&+Ju3~b$s0;BXsJld-Rua8=!>&~*&Ho%-`AsQer%0lr9b6`L~Ww>-ubG|C=kprrY^q5zLAvr>9Fz+x3m(ZZ!} z(;c*u`=AW-NpxTp>Fn3jX>)7LR!2_4_Z0|!kKnLvqlyGlL9rT@$zIT!EmpQBd9zl{S*h}H|5OtpqnaOh<(DqIMEb2?G#R^9lfGRGEG~0 z;}BhrbroowrG6NfB{0+cdwC+N&dxYb4ua}ylqtT5ZAyr?G4q*4@wh`~6N6teROwzE zjeHVAC0rUYjhieJ;An3BFGw^D`y7eU87kE0>i=62mANvAZ>l)-xo2|GMGsgMpA|`N z;JS8Ylk7Bih{St6v{;Mukqq@80G^^5g`1}%qQB}Yl35R*$PF2RVWk{?N!5{d2DuWL zxHnQ6= z|70w78;$vLtF>9iY}Hr6(!dY%etcor?KKj(ZZ;aXTyj(Lsy`zID8otpm5DFHdvWN8 zb0UDyrR$znpnw{%m|W?Md3Ztto`LsJHP>9sNv}vEVwLjS7hM_U)^%d8fEN?+TiB~# zU*|)%ckYMV;p)J8#+{>fUa$-J$fmU)+H1>b5(cQv44r2bkdI3Epl}pxs;00RfY4Ry z|2;14*VFJ`#GC;h6N*BO-Uk;I*kN`3<|T#Y`HWz&J(>pW(By7%PIfC+!7NN6E&dTz z%~=%pB!O{9`vU|s8B-T8r$G`~1!cKK48HXLQPfq<<9MX{)LWdvAD%xXO&(2772m9G zo&xSp|IHa+V5Sa|jO{i7MUhQA%mI+!TM#wx?NB+epg3Vpe}|Im7~DLdf)y46W&QNc zWua+C9JWEg!(=G7dwrSUah1fHRn`KPfI>&`p|m$t4<{&pNdz=ex75j7EO`6`P8sEU zxRuq9I~)=%44F%^7#^_`J@d1~X62-=ZPoJzt(E&yBA57v?cCSKsYgIAmM-6R@nU~t z7SINT`R2{aMEAEqAZ!j;npwaI_w#+VcpjjcdjO&}Oa)3u(28(4`P;8zqqwgaC$vCr z4EDwulreEl_bz^G5l+;!6$=qpeeoh2`)qcpLs3=Y9xeutfpd$&R<;*Hmf~$4=ndps z@x+(sIu2J8S+rY`9O5z;*BD@AIl=0z;$u8zFrfKWP4zP?Xn_&2oe?_!42t(fX4}Ss z1i|a@je2hCp6MI%0uqv+r&s}OX-3cv+2|YDfP*>u(UittI_WQQRI`*K%>rrv4p^spzXENsr>5BG0qvY?gSJup3L2=YEB8j=eYEQLZ8VWrkWE`IY+#FLNG20~<=OA4=hcF8-c|^0Ir%7IjYPP9J=4z?gO~u_ACLLkC15vLvOLoM|G;1V{>%UuaD=350BtJs++&_? zlGBFFSQp<22T+f%ro$?h|r;PDq|7 z`Y-Pw6}I5`=}vU9$A=<~pKEW`fkj+Q7tw|sF}ay3?akcE#$M;Bypq987ObDkV=Xv! zBP~^TrvMjITH_8H9Ee+GFNDh)kG*L+V(~(?Z z8+Y(yFPRJa=5fh+NUM6KFZVVT3fdG8z&K5jLbhMfTUNI`4+MI?QN z2~vHx5cL)jU-7$5Z6@EN*iddR13_Vfz33}yQ+kpCF$SY~9#0{O_m=}zDlu)-9z!8!isVjmn-v`E#u5<(C9_1o`rUDS@qU`<67gLNoxpM zL=Z7r%}FE?5({`SX~J*>Crd_A&)Bhb=fxA!EVda`1ii{L2>0XHWnG>#9&*k$=xkMC zco*0K4%^B0OVleDLs+%+IFEeA2@4p7M597_L}NxeDr=ONkvaQn-|Wxref<}%an=0o zo7Kka^oONGyzSgdHNTJ7dtelwxEP|&Rk;-|6J&?>#!XBhoB90MAx zeOUmoz}t2(G6$wnF)lfLB6o)%CDXVvs#mp@LSm)#5hWUxA%a(|!Tk6Z8clU)D;t1v z3;g6)!#eNHMY%leug-R6V06mz%6#J9a;26odH0R@9z`(;`7orCtd?s?_BrZ5#$+I) zXx?u8g&28hCj*_??VZ83KLV#Zj6Y?gx-9|w0%Vir<+yaca!fwxcfZ!S&E5}Rb{nn$ z2F{_;6Urt59Hc}Qj_4Dw%aa~bTmmQH5&VH37Wp*_ zY%6j1^K#BbFVQmlqp?|SuwSjcaG-JrSDS)dqP$^BtEhz{Yd%5VVpCgUPsdGqC|#x| zD5A29uP3;zNsVu94OwPTI^C>K5jmGWom{_nS&Cyv_Y)pV`4J-zyD? zAr$5Rj6lKU$0KrYE*mFIOffEg768yVIFR{u#~gD3$qBOCt^&1~rgj?)xCQTXx&}u3 z55uS^p0StN7wpU;fqqSc;uf-QLhBt)N0xB(0ytEoo+xN-)FHbFl@Y^Cr59jW|#+o=NBF zJA4bvNVFzr2?QqQy{ee>?ldXB(BV*;~l77|G)qIGAtEe z6kxSnpP#36!J>#_ILaF+ayX?mefa6XigbYIAWcar?nq-=VCPK1RtL+VDBxax&)2&+ zo4DCLEShK~Du2VBbT8jb8(zF)Y;fP^UXxyKL?oPmtOz^CS8B+VZ|@pc>*{=hyf zsO(2g-ewrcpB@2KcfD(Xh5=zrXD;C}Y^Jl3D~u=9BmuhL4U9q@ z`RNJ%K@1ZIQ35+P#OZ+LH{1;yX76TlMD+Vm5NI|bi?~+K5nlT)JC^?2va5jKR>`_# zbJFt}Us4%i6a;xQ@-~c#T1s;`4DO%K@uPuI37bEFt#GigDI=z+yTZF=D;k zH+`8u>)S1D$Tz5+dit%CNc|t^-`qbV5%d0MTdy($-gzF%^p&rRR7=Y;;8MySz_Whv) zlM=rHUjRe_mXV3P^}DhvAYNtd`1aAD&UP<5!V6bmfxIJzovEeINVbOv zL>gC{Ih%5Pkg5(^ep>4@lKBx#x7bdH{Pw0)7GG6f$lnCB9}3{qEv$@dWiZflBh~4A z)O!Q9iuRpla2I;8FkoDb08=mhBrIG}uJgcr0i5bOxjGH%g%U2_T&(#@URqni^`m+` z54AegNH0&fN30nu)VWM?j|RvW_Ff~~I)g0(JpJik)AhVhE`@+ol5qY{j>-mc8ROdx zI!||Afg8P4ceLsG2c2cv$^7b{_jBL8f3^2--LGe;T`K@(ed^P}7mqlyIP{yST%`0pHm-$ers#0yhql*5zl&MOL= zk@ipvd%gIBj5WZYsnATtJI<{QbhR-ycWY)^_Zb1hqR=_UZF_Dp2m;yGHD)f+&tEy& zi|x~@@q#Coyst_4=<9SI6C=cpJKVtcsE$-*yKNpvMCMK}&(ID6QljER)_4B>?p1U1 z?|nb}jv}D^L6WB@rjz7hz#M=(P)7j4gBnIjdwuHiB|pvTVif-NOwsAF2}hXPXjQf| zi$X`n6S))X)LLzVc?G9XwKF045_uKvcg?lWL;w|uCMT_QZBu>IT?Z$*@j@sNzKA&y zuM#Ngx9o3|I2>eS0%@E3t&9kT@Een9H+~bADj;8}hO+3Zg)ZPo-cSRPs=aD7Ub-kD zRT$g`CTE^p2h@v?F?bfK0gWSyMr%Wbivi*jp4%ZBz5GXx9LS@o3etjX1|T~t01D~e z9#BNt9P;TQ%z;!=YvP@HhQ`Yot0R>{f)!U2)w?GWm^9+wWRbEH-Q!qB==CtR{$2<{ z$-Gaqi2ncQ;sBi%)Sy{x60gDzz=Hv_t2?>VuqIe&A>|s6@mv_^*x=61ui^>5^!o{4 zqE6ZRA{hh0nv`(1Ma;qMBA?auEknRjKJpc?zEWytm@VCm#GePXd|>ejjlsToA}|={ zVl|Fc?qW#L++kym+|%g>9?0YqZoT5Y#47I}o>9ddczRZVPu};%RPt+Dan@o=;0%z z5#gV84S1lNs7QBMeyjNj=}zAOI6eWfS~UA!Hl$9Ug%6D(28K%Z=};mqV;h6`uYm&4 zu7tv{rRId0t_OVFs>FJmW#kvVMNUK~>)7;Y1_pN^k-t|P z9qcl*$Jl|PRuQ2wC`j_Fsvz^PLv%$7$?ywRQpMBC$=BI zdc-l!+V%6Xz|v7aa>k}EsFu)^8gD)^DjAGR)PE+Sds@FHUAn2qK!*5EF z&MkB$T2%M#D#BoeMMq^b>gqN!h!h{*0O=P+!8lPn`hGQf3JA0nKE25ek$>LnNnOH@ zbKp+ycM67&;8VHL>(v!0#e=Yn;nBqd7K`yd-4q~>^Zqod>6EVOL}r(kQ_Kc`Qe*5= z?WH6+&?0G%HF^Mkx;&CPq-L~0S4Vcw%v;yd4?rBzd3WB5Pn6TNC5Al0PLQqq?aT?BJkXhI0HJC=MUN%i(k(-?{_BvTKx7t!mW#$tsn?SI?>eRIn<{Iv z34WGE(vEYxXM61mQ)vhAGP#^JEj^-}^)miAM*dk)f&m@1aSn`|TBxXZlU$i*YWtkY zUvtcKXd{FSkR++%!0kF0Y{B7d+t%(!-~hElUZCWNj2AHRs5Ju&5&5FbV}H{lno5l7 z$u|Ao%Zehp`!=eY`TT5|VuPAu`M92A1KPvdeX(u*dp9~**ePj{_031%cn;_{|DFd_ z?gnI_wXprkgIoiF%SkTwyh8sbwf6@t-tPMrUdx&dKsOgLPh&a0QTp*mhP(W|WtT?_ z*y->*{KekdfQuvVZznNgIYwpqOQOF#m6>JkafOe;49cEiT*8et`$JxET*I}8F_b3? z>RABSYv5{_O93PguBh^k0=;B&AUO<0i_^%Ca7W1ZIpt&Qg%BRGRx67Ct$YZY4{Ai+sYi{c&797PE1^5XMNiqSeTSR0yj?dyh zCqHg(FYVoLTfCZVnPs5@ZYZ&;e7^i%&Z&Et?E6>#wH;5hF`LYx)vAu*rC~{XNUk>?^|tRRXuzAyVQ>mN>5_FE zPZGxI686Qif@XZT7dmu+YAbM5yZf{c!^;eJf&$ZQh3C-%Y%D&MIJyFM%RIe%F=W@5 z{_9j%C+sK!)#l8``fIB>Mxo12U@#GfVN46|*l48X^MhQg67Av59nYzp0G&Sh^xRN9 z{CJzr61-{{de1eE!_WT3HsP`0<6Z{7gO8fI!%|URW(-FJDT7%N8kS>KTX=^gLpes! z$t;KLm?hmw+Ytsnr6Ve`eJ)TbT}}K_PJ@+Sgd2REW(CQ@1G@^wCEf~4wGt9htQzM1 zitj`2Tx1M8Xcy`8Q_?Q}fek?7jeW+WX5T79*;gPwnyjWEZ`O$w#Cu`Z!G*lNFqky^ zH4O}}NeQ9vFU|C-H)W8bX}+{UvsKyMNszjtcn0^(Wue2N7J0;zNwEOblfev7(6ySJ z;Km5bWIo)JlP>^L?8)@|X^Jm0`m4gDyhm`?7PdBbX%GAfD=4l zXwCoyCcKeuR{()2IB67Hz^J!6#asPinmpos)HnwD5*+JyWw$8ke+;l-eK0aGIfgbynnkN`-l-n+rwQP5cR9s|T zI=~>RLs40>Nr6ELx-jbTOb$G!-~Rrr-_eyu_XB9GEd4^{O;2KBDK*HHK8(lqkqT~& z7v8%V!NFdI;Q7CoP*j^HqS0oM%r==j0?3Te!C%2=x>){{2ckJUda9%9?R|x1!zm-} zxy%+7QtoE1SHoq_5OM5E@v{pugF(5I<{L1xVd42)q{0`9{gXwud8kwftw*5|26wxq z(_TFrp2n*V*t{2}Hf4{}=gmZv-<5kw7P2kFk@rAFcr8rD0{+~~w9`k}6+n3$L51Ms zzk~KRBfoiXJ-?wMi<~?mvtJEdMZWrs_ULlFSDVlWD+bR|R!650BIIOf-wl{W$D@+t z3++8d%FuBjMl{Cgi^}W<(jZ8%txMUfKM7~NgId&8*0JCj%}@PD*GsTiKM*Z3*3X3s zbg*0Zp4m}jBGEE}_ua=VutTov(y#K2H$d^iF5mrqkd(Ot!0tJpChP?0*r+S- z5l>}zXJT87vjSu&hVoi?BQ^rwfuwQ`qHeZv`iqb+-8xm#Ds`L;T+=8(=?IQkFUJr& z-YLf74gUJ})Ws09^a;F@Q^FqQ4YvbjD#~TTtr*8A8^iy%4QtU^zvZQ$O2N*+NjL{IHJt@s~vi z&YefsR}rzIYO{I6Kw!AXxY7+5&@s0L_Npw1H;tS*eI~BoSc>&P=Dj|8q^g=N=k$o; zXjU{BObm;;;hel-0DDT`It3Wxr)*S;5WsJ@fcAKW$BA3pLRV)tLT}V3$oiti;cXv? zD3MI@0D`lg;AqQu<9K$+fx#9d1OE6vdb6{-0FG<>_H>L`b0x1RRC z*j`=T0mO4~)rNj|7~?H;Yxjc@C}{$lsBcP1qiSFu*$Il*d->|| zs{@v=T3T)h3ha<{f7qHz!ocj@vrXA0eb`596 zRy5SAOJJ;G1kht&6Z{49=sv_Dh>y3>S6Ow}0I`EQaEsp1J?#TQMiC$f=Nc_&_;+qWmuj%eVe;3+%!nNa+A=q|1xeUd97CQpBo!92K+%^RwSRFvs{IztT z6K%$@{U$aF(4#HgS6gq}E5ha(BfMjmXygQ}ZGNF2!%bWx1 z2n9Q^>bM7|0!LN14x3Ds|cp!IvE+Kdrf zk2-0%;_@e%PVYFanp4fN6&^ljgW`6K*CE7_k(64pyiX<^CG1}4Ust&{NuA$!C1#Ey7Xf^h$ zw1T}YpJ9SbRrvfYWmR1EEUyaAOGQ5INw>9*VZat0QA+>r&eZlc20yYG0Ik!2pgmZy z_m}R`|ABlg`?HAbU@9W(?G)LAb@2CRG>zzQO1{QG69gTP1Z}DF8`L71lO8UaXkR=T za`FTA)0}p^d+`zy6!;r2S#bGkw$d0L-S%Hi@GMyBghG^pjz%L$IAm@0s>10~#-$$c z?)3_zcm>ZM2qFcb8Zd7shy_z8Y1>Nl*+Obkl9|Os$72Q@2UWN34<7*SG5lUR3SrE`_>vyOkYv%b{%vJ>T#Vi|A z+Xdf^HK%t8Ch2@8P-mDK^U^*n$V7hSzX)lNd0r7K)-7A{r84=Z%1P?mL6xWvu%>4p2EeeFc`8=?fXm-fIz98?D*@M#e0DS^QV`?=zPwOsDcYy|5w zj<4C7hZbmk)@aMxVhk;1H;`NSQiO1_wxYv$Be9LphiC++gP!v9q!2tKw2># zM)Yf7V{4rsLQ3d11_?I#F-@%SoTp-Pj*b5eN_9AX8WuL|22tzW8XJHX7K#m^(}MjQ zR&Q;Bs!pY$aQbG_hVdmu;i@=h_s)6n*PVeqmOX$&9G1PM zdx?D2)F{}3Kwi`j%PCi1+C>^D!Hux^|P9!p~oqfZf^am5^{T!37#jmv9(;@+K_i#%+Ocd12XjC@&kD|tA<##KLg;hqXmY!)D2PJ?A{k zA7X)vT{+*ojWT5FE_FGo47_3C?OIezOi1Le9|VQJCY>{AoVz_yT6KN*W#HW@M=>NN zGwBh=kd0zQke)K&oroB!({1zMcR1sO$*7!gML(s`)4>byK=QMjNXVvBYf1R~3&<(Z z0Zh=hr|cx=t&#CkQOw1&_-O@6gzw9%_4A*k;Zp2EQlHQwA)J&hPrE2Y!B26~uN%a; z^8(UQ%p$4IuH#mO8K8I_2JJJn|lTwh8ELZB{8TVLZM-L!y~t|mh{0G$h%R`UbVi}J0RupK?kwK&#IcE{S-tEOyc|29JCA#~)ckUV3 zm$3l)2JM|G(9P7=F22gRz9R6~{Sb$a32q*XdtwDLGL~q8YBXIxJ8*Ku*1QPS7A4f! zbia>OU1cmL1G5oW;;no1{`@%k3V^+L6!&&$NUjyt_UIlE5m8rJKZMEFUGO%T;q$ZJ zG)=$lB7fbpqk?IF?E9C3JIv%(l5;};Vb&VE&w}k?Zf6PgNFIyit z=9}=T`(Y`_^eA>pTp`bXt-Z)a_4&fM--1%t`}J{AC-5hjzmKv6QaPe|7o`OA!q;zW zvQU<_HXw)9aghZIP(L@`b{sS4!18;xXnZKN3vk-Nj|n>r7Sws72jmxh0iv7(eGevchHO|yjNd?Cx(+2bimxJf0jXR#-DEUE;2Y8kX zV8pm1r;w0}pr-Z|!>l8Tv)HPS&$&>dv&5o!(6#8QY_wt7s$gRjW_flj2-di59uD zClUtiwA(Cl#2@GVoS;K=?bnBA=l#A#8hu10?AKdhFAlA~4IPgH9%stZ8qW6p3b`tX z17d#^SL{Lo|7?=;tJvY|5Gd1vW<`lp$4^22sov_p*R*Cc_kFfODPkjW0d_N98;KoE zCxeN+y`@|=wpNuoYrXD$6vp`~V!xk1oM_*os28`KX$;q4zZV&fzfuXBZC{K! z=WRBuT>@SyM(P)1=V#No&l&$L~ zBibQ@u)e_A(1Do4U%SMB!4SBG%e#_yc$SxWd7PQI=fbJcEmncpH2930deFhD$GVKU+=6Q;v>{R^*9?m#w4~* zoddmDOCAWu97i_AJ0(BpYjWQELV~~HDA{Y^*YHwWHE>a%&?W+L8 zuDTk#BE4B1X4nn@npAFZp3?w*=hn&5YAhSR*pWLxq_0wbC+}CqYBUl!FTiGpiQdRD zNq)YLg9KzivBvChI1-vZ1!#3XsCjQ*I9#SFC^03>RtKyGe1(yK!RSjJp?+)M4DcZy ziNh-DC2{`wA%P7JV-;dT zyb0CEGWct?wfc_NLzbj(P{aelkiqxH6O8m#Lz0N1#NCTUA)m{_>IoVZy+O#*eOZuL zTLLEY;!Q?Ihg*bewvW#!It)uVPL)=1@u2kG3baT|I`q(~Ek3O^6T3!If2>xXx#8Y(fsg$Kw)a` zQt{rJd5P-xCD&!5053tqfXY%sO)-|g3q?`vuJQN_n1Vl$HyAhPSXE&NTwG-I`}4MM zOov!7uWNvO#Xt;(Kne&FEKR8`K)Oo0hcihux+yj(^ur+`tBw9KIugY|$WVz(&xy+W zf-d+eLzTa9+Le%1w2_*?slGOajP-UhX>C(}@GWg@5-C$zRU3Chda&_GL+&TdTy$eY zqJ-A-(4eg*lPr38&-&n}KtY|VI+&jlE$%tnF6K(}oxlsb9M+L-^LC}SuDTjT zB(Nhv)CL?rsrSx&=%5aCLl)_)X+0rLgUm^w4Ur^jU#dBR{NPL13F}YA?D9?CFyA+5 zrovgJPGs4bi;5PCtIP8D^Z9?uDX|~>m)flYaJdDi`R49j$86=a{;*a<`hB#eifY)N zD=BQ)`#~mDTv5Q_3hYeUnLZznIQJXiQ~}<^r(RO)d9crxc!Fj9f*0M>7W0gjm6d*d zVG*RNeIxKj=2!q}S-(ywKQ{I`EJ<2l0%W$U|K{?~2t-&ny*?29tC(XIoNQ|7ATt|? z1JJ*xfNWD&+?zPEhSOb(!&zrM#?kLDd z*7s$Y8^`frcBbR$HhLlci8^E1LiXzSu{1el+rt`Imqu*IsXMU!Ee7zp-(WI4ak4If zq)58HRLkpg3csdM&|~LG4anR zl-1Z+4dD8!E+x5%syBL936*x?FQAultU_`kh{bS>(8tHlC1iMmQ2;vPW(dFm6^SC9 zr}b9`lt6H>uOf?Zv_z5E-DPD%!AdJ$3nMN@QIvdq`!v>qx{EFim|qXXqJ7T`O#$U& z0^C*|n@47MyWWiu&%(E>I=jXbcz?%S2onJLz@`+;_3|C6GRG#!9vR_p6Pt8E4LJk0 z#9^|`I6jDlQ5ePj-}cH@pxs7vMQaZPoVK8(GHZ7uxo3M*L5idOV~{hhyktn%rfZj! zn|#ezNUs-0Fw4b1Q_2GXZvg;GK()UnUe>z_6fV~)k!@>ITe44)s1n&r_&eGzA;O%J z-F!o~y>~W$%kVK%W&R|~%${LK7`+;X!?*zq1H z2%D>E>u}E+M|X5LBOIKm0lI+k{W05TqFifLAi=+g0w47(=5}$P_m1icGhAp! znRCHd(gYH3faCm0zm9FYE4N_~Oa0=d#h&A_8UXyYKnQSd5wDSC&#>K4Fh>OICC+Pr z7HbE`iMW!X5tX(X49GY_?JyiYO{bH=l2rOOi_dnwuv{~ z|9hBiojcJ=Ew8?@4%rQU;OBg&6h(Hw%NrTq8Cy>)WR^jJD;O`caLSX1jcw0GXhn;4 zjH<0;gx2s}*<+rWL*)Y@Tp-qStaABQlgbm3*(fJ)ZNvS@f796ioG2k!nJKcX`sO0Q&ry>Y#%ukc&=rsZs^*`rv(+kvg#**c8tjAU!OKGmYMqE|;&pr+?+Lhh^ z6Q5tZre&MK(y#ZzG;`$$5753iDp<>25JpYbSnnjct`EL}LU04JRpB!Hitu1@FqB5) zd4vEHf?NuiZ&Bhfz--h#aLuFh6=)ae>wu-FVW9)sQ6~1j4vXO$pP|2jPkz#`gJd9X zSd5UGFknT#!iy`+SX~^ph&>?LCCk`k^ACLM(!3-fuN@!K1Y8%to{t$&@m^&|JGCeW z;0*T0BIXT}Uazq$Mwcfvb9exB1uX4_oDsTlQe%BnSF;MQ5$+96S^ zBlWsj3r8I=m98r04?OZRM4X=1OSux7o|8003v5_ip_~@_0wlA*1)Bg8zue)FtyJ*@Y9n}EoT5P3xMZMyHW*TLOMidH z2PF-Rf?f>V9$p^cpZLHOH&LFzrGTJ4>o@m7qoun+nY&Wjbl!SG9fN4v_Q@yhx)Nr{ zh7rASJr82x>Nu(?v~r~oSA3fvX(Ors;nm}+hYe)%V%Cg*6XlFr1m#skHs;|Ebi&NT$^|8+NvHR7}jBk1N~< z+rIG7p#?{9rf3f&EeRme#q)J3AG%_;2b*NjR_4J)?_qkfyx`+FaHuJ+}oDT3xgTqdFYAG-7+Yl z6W9V^t*=1P9Bk#ldvF3gePBw{h+$6`vjz!~;-=Ivm{0nbh}rqtxG?_p#h}_58oosp zdQ^(8y$m*j1D8q|cH73uK`RXHIbMz4Uk5lkiKTthO98WN zFX(menPK5XJk99&rxi(%ll&UIca^V5jfeR_^ElIEMQG`p8nbGprM}O0OWqw|wFl*f z$;pCRD~wrSx#OI8+4xmOeb8uV22`!j*_&=7E=SKO6N#3G*yI%Pbw9m61l2l#fwmtwSe^yJ^A*bH7PFI~m=@YMD%w%vrFNm4m6)>NIzlPjnHG z?q|na{{)FsiYRc!qrUw`xdK=b3AYOhL0{<1dEGk!jh|pFM1;wqULna-U!OsTjgecm z`aqa^@wsqnE@^!tq3SQWxm;(eKhDvg`i}|_yo4ko(A33@E>RH)h}Ymkcfd%uI(_y$W(Y%mIfNJnS28pW1myYv6ZfZ%G02*Ngm>9F{9{V7g_h`qDQB zwvT6k0XzkY$0MxFzT1cRAkj=uBwJ1lLaEn6V8okuu&18ry$npd{qk;InxpYuZ6Ml4 z>AomJl&KdqUnLR)f%~NkbDI38f*T1Au6mQXVrv^YH3agfe_T=^=C7$Djf>@Wj2s zB0XhPqVoN+PLY#UU_35Ez#0wbcc!6vWbEV5)_yv=TLcypkTwPte;D|3$zMhTYR54D z79rmoduraJ0~(43WGB`e#6)QuM6_3H4+8StiUku00Em5_?mPlsRmds+Fh2G(IS@)C za%M1Urgs4K1E0N19p)92R=Jt8l=gQB1d3X2O=i|*%LswJ*Rb!$&7O8!4hivVjKOjZ zBkI~&1P>@NNW>X&OEF=>ezlC3a(Jy*m`5QcZZh+R2$ayF?7HzT0RvwczL!J-v$&U< ze*113pp*Yhb(I>qmA)_)A46Cp%SK-lI__c@{pROsBXK0m0@c^m-&0gmhc9}6!Netk zrbOQgOA@LkZ#0-;0=io@px0?Yc?DIJpJY-9+okcD=G_XNw`3stn7^7#FuG5aM`fGU z6*7qX20(8DIT%wY1zNK@n5iXZSYujzL%<&0h<8U5x6czN z(`HXDr<(MCKZmUE5r3U4`o+9>Qjr6DW0O2lrnChlrIkRvIWxYp-^1opfStvbQK!0j z8X)t*God~sanBUUZAs`%&!^b-lY}8y{d7PUPVn<&bw-O@df?r!txYnSG;7zIp^$Rod1E z&azP(BW+FTJ0|V2oMsC>u{jKQqEo)BweG9`+&0eLZgitxo%GO+F+&>T% zV2qX*J>t|1`<+sCk)BAbt0T>>Gq(Lz8`Y|;VX~p;T`pxMqRiBCgQa4gMC)%{Lp1Rn%0yXgk0102}dn+UFM-!&{%z|_K4;f z1bttXY#;0Yo?J~qSh+4%>&-jm9KBWCx8;ZcUScc>SywhZCvJJEIb-z`MEk905jwIB zfb{v;Ahj2UN{gGLgBk2-V+%K{~{*560*wg4;KRC=$T(}C!?Rs&9r__h;ZLu$rnCiFcuM`RO9+x;}%bY=Uc_}r%k{cm0YT;nNs zb$LZlaeSC@2$9vzve0=YOV|dsL(&MoDDnNK>2c7K{#uhd@CLGM?Q?aY%w*7;TETOH z?m9brfofP5$FlM11w1r~ zGU(6djw8mKvol*M`2NhF-7m$B6C(2!Z9o3cYv_Wf=LCZV7gp1FPG`!3(Rgus|q_h3oqY7YXXLZ;h9 zjw%b+KyQvCEbiR1e4Q1mZ+*YkuWa~*ws0-7;R&c8ll!<%fFt0K%vJnryUav5OyV-=NjM* zYvfIT1~W3v-uGOF>6?w_z1Rq}Pqen9o3tqqi={%rUMtP**Veq$<4qQCeQJkj%tu-N z%^$||a#UzYZ6l+V^Io)9_OF_&n~i_PV@!V>D&osH%cU(>PZ(`d=XH{lSKfNStP5St z+6qaI1Ed#Mk|8TH+avv9g369{S5%`GIPy)TRZi1P^FtWfW!qfYesZTY2ACl){6CwP z@N!O2-bE3kC6f}76PakH6&r6$DxQX=uU3s9P8zN?W5s%Rs3Nr_G2 z$6YNanuAAYdCuIDlV;ob9{^90e2m*FkJM?c9{OqNSGS2L>7GZfiIAZc$VBzP8du^Y#WthUDuy!STU$=;2ks%EO47*N>{dB;%YJEr!S!6JSLx{ zL>R-U?KRR06{9~+Q{ieYOQ_>M@j?*^Zxz5p^FVo_Wsit|6AT9d5jE7OfwJD!Lcm5}fbVngSiC6zW0?teMXLCEope)1L1>R1_HOkGf8RX-oxvPqc_&KJ_L0IppOM`$4=4| zgcnRDR9c%j4O0b1=hEU)z%hP-91@aB{Qv2uU2^N#8(ne=3PGm3uFv8w7w|OW3EM#5 zOMZyWAEJDFamv$T9r)d#?0)pZ()EX@4j^YnG@k7tbGe?F$u*s{kKdqb?YKG&k^;I? z_c5JTp(?@gh!ZtNw0G^$SNfT%Ad9B9w_#Ld^A&Q`%di*`)CSX{7}}hTIyuoN=r^oV8lA z!;_w(Z2dpWoo7=J3oQEhvBwxO8d`tXbXOBoGLCSkl}E$NQ=5k<-08F%3dv>MM)(@--#Vn86FYsz2!hG`0(QQ7VTdMRk*B+A}?y!4QkB5Ju~yv1_LL} z;J_t!!b9VK1wf_(@rVH6LHj`2E)ZXWPIq`9Qf1gSAy?q%5Ig^}-A(4UphvPC@I+hT zoU~1B$X^qzwtQ|Dfc#ZDcf%xL7d(<$W@Q-K7i$DMW&1W5i}4lJIx-hhc$s&#emX-A z@l1IV91*GH2<_krkFKTZ>ailBm3M*GTsz*)z~1P_n;VviZSd}O%v|;lw?KlHME;NYM9Je7D_5G*aYiOy7(r4eNx`5 z>qj8QyoGZp*Yol5kNMTWbPWin90QN$5xv2=i#b%SQpA7{4Cmn`K?sa`Yz!EK`x*61 z-!7JI6_5q-bjvHSsqR*Zwp*1jj*MMfOj&{S9V|;27YU8d7Gx^E_tdqm5}xzgCS~hU zi!zMY{|bEIIPudhMN^X@9Lb)C2M@f1*25=2BiW zS6h@%zkf~=V=~#;R#ikmr77nTq+O(aDpJRzDxBVB#4dyum~1CVcdUMVc%GQ|QJSApl~Vm4 zc}H(Fw*$T6dn#2$pV)jvq5^`4w?|s%=Q%KsA<~ven5>oiFKTe3=&m@fREhh!k-&ex zl$0ZbY34y7s;DB4oc)3N@ls%*Tg56O98g-}G<9*Rz^L0xw5_^Ry;Q##jsU>q0SJp8 zD1Ql}sVF>(!ZBqW^L!;7Q^OM=u>)#H5OP^VCb!+V@(u5Zaf|a>qf^f;Qyk(7y(bU= zue6sl49L_;0O?|y!hRS&2{N_C`R|6`U^l@7N?@R`eYXA$#&#Kft6j~@y_r?FyHf8i z#)a+II)zcLkyZSz+t07a8HMIKG?cz(4vohhm>|)JQrD|)vkxdm zzqE#ZMR-!4`klB*u?MSqltWCf^an@dZAW<&=kLjD#SsuLis4@T@Y2|wCp#u}5N!;= z=7$K@Eejs0gt3o?d*2_OtCsti+;T*gjGOzCo_YQ|(*1Ae2q#uNz1eR=z~3CBOhoI=r+!&A1O#!<^5tKmr~o~HpJ%ExA?gPdj7CxFLYbKL z_qp#8RPy${xAB~M*F_OG!b}NZ{W%m}%T4VbHotl}cmtiWp(RKa1?A^MzY(19T+?H3 zOAfC3gkj*v_;+>$@A_d6>xxlKqg8O(Nw8@^x@MHAomPyX41=;^1kQ&b$u`mo1#uFG z^HBk^z6Cg-h3Ud3iNncm)Ib5hP&QG5Uwos*fDq)0uInPwzZMt>$4R{EZwv6wn<2tb z%^dqjHe=K=P-^Fy&7Ndy7$F=al*>Leu(8SS^ZLI#B2^7PAa+}QdH1g+5JbWPp}t08 zucgLI={sR79T<}(1YijLmi3RFCnOnJHXyw&7`@$2RgyqqpCx0j;k;pF+zj_y67B}% zFx}@aK%lE)MB@d=laAx(Rt-S4C zItQ83V;a*v$_V5*6eQD{XZLaol|)ux1olZi#Bnk7>@YXtTv}0h9{MH+$i9a2`DpKx zco+SH<}E034{j+XmuNCfix7D-G?5xHPo~sld>4OcI3q=+? z)(T$uo2Ph^0@h)%EWPt*^cE~w7X~d>@9_>%q7_v?Wb@@$-%RPXDj~r5GoW9-z$tLxU$Z zFA!0Kb^vygEd=`uuv`$ad;zAnl7*z+AptYWLqt&32kU`HA0mfa9z$7(+f0Fcyv8|? ziiTSPwy5v@YD;Ly(yw!*Gpy9^cN>?X5S=gbT$11-62fDyXxK2cym)bQjyi zc=z7hoN0h!xD7FFI=5L6gz4YG{9;PBml*w)i34S8ie5s=Uy2_9vfA^_?-djpu&;hU zok>_~?o7h^bgumK;Bc+AZ%uhjz0Q`^%VfU2DO@8N4f|c2w)Y|HltAxl5>S96=u-d@l*^Ny{C|3yDMe+sTPn`u z)CouiZsK`g^etm~Ahd%Are4{0<1PQ31Mp>uBmV%P}&y8zC1J{v~bCBGpv8 zpV-ILKBW;i`c&Id?iLBKktEu_$8jy0aLY6qZ!sA*9kRGd?01TscUR?2R80rIkM9f2k*)L+2;GI9JhD9Jt) zPR26>tTG?=O#SOzAE$5%G=M6KN^_OIlX`jEnpJDTYoF4@M7oj05{1dZ8Jo8$d=w~d4d5pbocQiQ(%o(Z8BcY5hXqV@6R2@IIVJrBE7G-!A_W$Iyzz(HZ^XvISegt{16ac~LF z$7pcWd^`&iczCm-ZqL90M}y#nWEH%xoc!bz#i{zT9+=tGa*N-}7m*-`jzGlzt&sL~ zvA9+?#sbouap>$t?G6smyE!gS7eZ0U$P zf)D`Aub=kb@_5R5-&(eQ&)ws?)Eml?Tzc7|Ju_DIOr|3C>@Pxk43H-j31WbSPhOWY=SP~r_JIlgb z1uE(Zw!lCn5pcLOm@7V@D?Pes;}?7I1A%oGc-TyX);3L$bv((*4Asl}&G8^g0@ngr z5um>t@PlM04f3wUd-}FePn9ds%9^ymH-23+KY#}9VRfj zr3-_K9XC|IX`3!CR7Agz@bOpWa7jv;G&=tRO}ITYNWTgwMnZn{tL=MeL|iSwKI|pG z%j9&H#nfSP`k)j)Q|X}0uMp^SX!};0`?-ejJs1xdD3!2>baa*s;>Vd9x_ZTu$(HRb zD>u%JF>sABNQ>dCRLE9#2AK%<)0{cZfb;VUJRFBKGcN&+{q|pJCyt@Q2rFTlZ{HCs zHr;rg$5vbek+ZoYGJZJk!1{t}n4cBVZ*Jdm1=gp&FfgbM+El-Ex68!0RQcZL0(G#Eu+ zrwVArr;}44U|uza+m{&Wm{Cs_F^XKtIyC(NIze;cc)wx^XhGha9XuPd?Ga0WQ6p73 z@J^mi6eAa8YU?(dAcKHqf_#@+eP_^^u>C}FQIk*-uyYnQ#Blb8gVHNKZ8ovrOB=3u z|9apERl9*R@(#$Q zfTTE1`xY5S?}ChSHIc-$u{f)>06w`2uFmJij@#tikwiDvKErF!Jqr0DIQ(9I3=eqb zTI|aKP&DEAqjQUzqn}?R{Z{J!$#5wE5{HA_RKda!8|D5W zZtp|1wZ4(9uO*`h&m|*n=EK%S)TfZFnnw&zSeO9OfX(Ipw;6S#X*_SH)H1Xr?WpV z0ao(20)!)JTx;5BF0Fd=a3rNgJCQmM&^@PI>x|p053((OO;Gr~h*ArzYlpM*!>*smG)ETe0b>pfl7-!HKu{=9|@QilPTIGdfn?skaxH-?qdLA zTdHBQsy%6^ zo-?us%MgWPa8bbE55+q)_D$TD$~B-uJXimjVqkB2g-pykC#Nkp_HbkYJFuN&SO9PhZ)czu}Fvbx#+J#FqQyOl@eb|q8(d9Ud6aE<;-JMVrWQXrtDvYr>> z-Dkgctz+`#y+=z;E`j}cV46W?A1#1-1eoD!lO007-L_g@pA(BF{>3Gwh`-GNb(SX? z`fwL(2H1Dv>Ad?C&^ra>UZW-jGa$E7pY zS8!tMGuYhsg*<2Mf$n;f=K`%H!jlWOhn^LT|oA1SAf^4*=-TEj-eiV!6*8E2Z zCvWEQ8-g(Kf{lPMbXiTXRLd{mD-^xmOAs`L8DFZVP#Tv3bOUq^|R zgX6>B+6x~hphWs3QKw&T)mQ(bZbibGZ^!Uda=p3)4|t|(ywy!=M9~MlPmG@QS{rRj zsQo-ngwJGD^54YcaYV&ln93bpGpK|H)d$kLqiK<8+vE$(OXk5X%+i~QY7D-tP@vbTf;VQ=xk)&qh6i4ukN zo8#f}P%)#?N{xnB-_~t4*6$>q;dYNg7l}5tC0DwrxPst~COD;P3RhYp zSs*w_W2gMhM%=XLPGxe-7ghn#5MW_K(swkob92B%6smqe9?X2-K2;Of|PfNo3j zp}kOId@r2sy`$GM@YOqye+cyPgO+uiZ{^|5CkWYgdOg3=yJsPfhR_dTV3IaOTkRrL zsbCULOuTrxu`2Nc0C;b+DMIFc%OHr03+krKj^AOAcV2jmp1zx>3@K4RJAA+lS3P-k zg3a-5hZprc_7??6wAHIid+2n*^25O#>U|r)=w2vq3o*VNfd&VJEI%&ljS~E4w8#Co z@G2Vs{{Uod>Hez-oVJB~c~_Y6MAeId-cfpANc}MHS5&!V&8SX;7z5z>e)<-mL%yB` ziw=@D1J|oVIWXjYYq1?fbs*Hfd>{GYypCX~xhTy9L}C|gVXU;u3VISJ=}$?tZBOA+ zo(YQMz*M#z%QS1ueJ4S~Ia+CrM06oZ6fEJ+7{U`iMnrk;417xr0d1j{g4eoo%(YDD z0LNuq(2gg(3tR1+t zV+<8O7Rhvo9hm6K2^L0rMG1|$b3Ba%t&AAZ2!4piQlj{s95VfOnes(~Q6eK>*n7fK+*6+jpR$ZdW?@C+Oh0PqhpdQ!EiC?T3P=`My>QzU}!NMi)Em;rrWuNNcxNOpj3 z0`zR@IB#(xR3Oh^<22Lr9HYIIqHYoQeGhljH?(PG*l8Llc^v>)_E=4zG4bA#X=F*5 zGzucBlD-KqWMZEp*E{Hav6L_4mH^(eP9R)XO|JH3kpf7tKmho~ll`9M?adhV544Lw zg_Y&5RA?;+ORV}ZFB#|pBWC|p%rDK~l>*$XzLgJNsb>yUUHYwlY@!i6;+@-Klg6TH zuOSt?h*2do-)SrgUDEaedgi>!KwH9PmN?@?J$IlukoQ}N+V5O!nO9grh=wf$J|QDr zWbAkiW&L`k%)n<(kSVXSpIIgLBCv-Xi{^hf1v2-FmkD1P?0A3lzl48FjU=&w=zbu&1u+?_p)K8KfhnQBX~ zdF3+h5q4AOIe0_aY{%)^e~Z-uBv}(pWt3esU@H9Wbg6dHp(QWM|T{=seoqcTvtm3u+gbo&gv>Z4vo71fQT= zco<#@ED$;a@`&(h2TOgI>u;(U6|loE8JnH(u@*ExPQO>PP9w^W4SApXMhuZZr%Pyrg& z$mNZ0Hd)ugh}(Y8jl1K5q{`?5j)(mL>&v>9Kj0s-R@IOsNiz@xVmGdg0Gx2tIobEo z1;1nGVu}H5+gvb%4Hxr-7Te}-php4QqGxN1`7lGSII=C|-X^)!7`B11QQ`{SlA33Q zJ-Yjcd;1C@HmozbD7>xpB~if)*DL(_mFf<}ixTre zSh(@hVma9j#7QZcfK#U~=@SZX&K4LDt_$>U0NkO`>TCBAcL6pg-A%eJX3RziAYGnQ zKsoygs7$$a--=aozO^fym#3^PYeuzv)lYd<}|a2;qIn{M&!ht zQi!Wie-xcJc7s3^MHfVY$WW4V&U6Tovj7oR?{VYG$Cl8{{1>huZ3SUhIK6=8Y1&3z zd7*OlqVLtwpb^zL*5>q(!?OuuzWR+U#*SmsCkwT(A`GA}#l?09q0Ne-) z+8v6DzER5iT+2xBCjNfk1g8^Os@CBcDHg-+d{9WCMDBg@{bsAHsHdKQ4|wI*H@|tn z?iXx+Dj4jUoLVulLbKGHLb6xioZ|Sg!F6v8TWEAnahzHfFmZ?~?mFWgeTl)THq#Qo z^-Eqom3K??qg2>^+}M#oL<}SdYlSK$?Lb|A)t0{%q11xFBHUn zfhtl&=*V)f40Y22ujT>pm=Vk`?O5jBi5;B>*vp!=qy^v!BL06(k>7U!`v$s#vu{Cb zV2YuK6S35Ce*2}LUErutyQM()(y=JSEO}=pp-BMUe_+ZL8t513vP9Epk5~yW5D0(R zN?35S3J?w-(&e`7Mn3{pu$?lJ8fgl0ymMVVuR)U4Z3<8G7w1tqPn2c#5fTM-^;rWm zYl6Qn7;f15`l$+JjoQjGTmWN->Km>{gB}nO3G=z(?Cue%$7H*7(OiAfmh3=aAsEHZ z;%`9IxImfl{R;uO+)EAOgG!5|7d~d}mZ&m??e7B$ZO;4D1nq-7eOHEMz2?pgt}GE= zp(}!34jAdYk>4c$7HTH1+Vh!_VRzluH?*SMd_GIUgH&G{0K3);+W^O_0=nm3_SjiM z5Vb2s4ulg)C+IP3Db+m{_QE{*z};s8`=cGm8`wJ#Wes>B4a^5L^%S*R?#nZ?T>~U$omU2-y;v1 z_qSi!_(ugwy_6tQGl1Fi;UE51g1bWFSGfOXMV>#XsO{m$36BWW>PPemgY7(J5C`*$ zaN<K(gMZOk2kyltlmh|Emf) zAa>l)>B9G<+~(A^V(+CO5EcXb62Vf+jHB?4CC~>L{|R`ztCbhTAfWubI_SQiohW+J z=)&Piygj(bp7iSc;=y#`-nWr35f*ruli|7lrLXk%kep z0Q>Ur4Dh{1<lXg5C4^x*PZfc^zOw)m?^mxjN39mDuV$&5oC3-YIQ2tLCLuXN~TQbn0Vfn%zR@gQP|m-qUd*gr!>8~!+{U3ee))bD3);OAb3J~z=k*t zEVMuCJOTD}{eH%P4qW_vy<-ozaT*>=I;#&XjV5W@ynl= z<&<8_3pt1yKX`REF2DO{T6}$P6D}G_?6)=&B=>Ig@vy-x0>FygruXg<|eMBe_v3ddP53u!K=T}tpVnJAA|0i*-z4gmUDi$w_*&fte5`#XhKGopURQipI zAjI>(>|&Xz=MMcbAmuAzA6`)j$L=<24y=n$03d3JV{z1Wd)XZyH|G+^!RWNoC^fOu zpAD-v?A$G3|8~icOz8E!j9H(W$xJ)H3wN?7D-&)aNFZ`}{~`=YuXw@bP)2TkoMgg3 z4b5Dnwn}SCLjj+auQQB>amj9iLn;}DXDYL8I9)2M63@kp@=dr z#SlZC=R2~B;+CPT4ssJl-lg<}9YF_5`rD6x^_cB?3RUHutHnLdOBCrOkAjhdI5>n4 zoY(n-PNLf}RONp^Szd29TVH`7O3sqeug^Pjj}wGo(=dS;PLPxm(4Kx^Ib5^(W3+dK zs0PZO%#_Bqzc~TA#23);x*8mBp4peqqkl5~ zp{EGl4S?jv?Qw?fafBex#GR1}>#hDig~Odx*xE~;&Ija?<;QuM(t|i4`te_1|IG;` zv-qm)2UxOAO7g_Nxs+KsW&cdpiIJm*H{sECY5WyCSd*tW)uESWotj~9lOjZ0T zkI^`RHZWxWa3MWMKRTSfu(9g%h6sl<~(Ad&+I_r*U0U$?j#LGg#-9+ zmCyK(O$p6~e^Fq+M?VXHa6yw%|KgS({HER)k1qL6VekPF4>sAsk9YJgBFMxcwJCeY zb-97{4%~ZXJ3Fm6b%#1%!kTUKHrX8+c*iH4@asJhT(VQ|=VJz9XU13hKx<8wRZ0Vt zxHykdis1tChfgd=As_(BHum>@*H1ODTLp*hlOrQ z&C2OK39pY)ov-|9a^WHY)M)B+NS3{rN}6|G(E(}S4~ybf*rL3CdRO; zKS6jCy`4q{HT_zzl3NK7nG8fR{+_;FRUjTZezuCXnIzx#bF*_)-s88O*rM}PeB~=*caT(;4a!_9F7XR20x01ikvD@q+RlOQ zjR3mK@IVuNIK{OuetS}V}C%tu8=pPRg)&yorICT|oTBmg(IBQ3>d%(n5v z?5~&_)26$&Mu2twpzB`%IZ-)lzAtt7Mzzn7e1JFtsHIU9WR~8x#R+Qs>rC2P4*Gk1 zN07EWI*k5S6Se~G=bN!v??eoWAlxRVp(8Sgwr z)==@Zem_)(kYDp1ePi}I&;{_m$zy=(Y|ueKM&+Q%P;b`<6)6ab1$55u32gBRk(va| zGE9)z)<^^ZbdEpqR$KtIqTsV8Qm}c2nXm;!NewGV&k=AE_}=#RE33L!Y1k^{QLq$T z3JXDN$mj+$7g19)^AChUKlu1!A-#LJEA%iQZ?^LY72Vi$d(_T1n4(|Eli(7bT# zEsX`VEKA&)jEmqqRM~F*cd>c(W@~bC8BD7pgh@|3kXbY4)9N}X~=p_XS=oEpPXYjHktrf`4iO|qiN>z z>7xMx>KP%#U?lB*3xe|)Lou`U7UN%l5Cl`&zCkfACDUa(tn{+D*il-n{KW!iDBhjq z$s1|9C9#{*P~|&#rM!x`@j197SRSE7o6rS1kL7w@rrYa*^vmP3>8Y@*e^T*Tdt+H7 zZNsQkyCQoOJ?3@tXWcS1mcuzRw2XTbzm_Y$^}eR)+wB?wKj&FpognvGQ)N%Ag^TpV zj~@xY1Th-9)bO9_UEsVkY=xKWpCU~5F2eFCj&)fGkI!8epuZA zQC*wTYOwp_#+j@9jObz{Qi$m$a0MRza_u6b+}xUA545KSAV3lIs%pDeR%!)6$PFtr zNC27g^BJ|kNYxkzn4dl2CdgC7LigG%lbKOQO)iP+ zTwh7(JG{U&d0dKUHqn)vvFsi}iG-MaX? zS()pQi_M0<=WspioP6NfcE?+n0h6J+H+?WCcpb7_6~Q|uL70#l zLSw(|mbZHB@-HC+jbXL8pH$@9)Ln_z48s-33;QDoNpRm%mM>z?@4({Xc6um6=VH|L zs0UP+vYqDMyhBYVsow%pNG0<0HjIyoD! z@sNNgd4K0=aAyI;jks5}pC`^JqJw{ErUx4WKURVhFJN4*Z4C8#U?3(711d-Se$+zK zDW&qO=7fu@J^*OMibv^Z9P_8FQ2YQgMF?J^=o*_gAxyT9!qD+eUj3RUBZ~ni=ND!v zI}zBk;WfO;!Ow|EpT|Hwa8@ME+$X~-e`il#Y;M*V>|L*Kw{{TbK#Xjy_@&-4Ug7NX zn}2h$#0Zeqb@cBC*Hq~!(eel~D@r=N)(#OjMUuYSZaT2Ij0po8!xDwsP_Mzq1_t zK13d-2*~D>VY?+@9^WQiIytV>AH6R@Pc`wCoCQc%SNSn?8Obo$qml2Pxb<#{K=p^U zpUc#K34V70Md0b6TXae~1mGNo=ekrUFui5;K=cfl9TV)@-Vsw!3eE3RzL4bBqJ@uR z@DG^o1gA<}uE6E~7{|k5qcqRs+tD^;0wGWJIpBX`0wCb^=94{ zFv99|{ofV3$Fm6Pod{OJJAggLyD#8i1F9*zsz0m`^2h;iGWZJ%%DXMyeYzQBx+9*N8*gue-N2SdHS z&N)-~_qU~tH{2;XY#s%ae8D@yB>;lS=zAYUxlD#pno-GvK*%UTQRZH3{Ox|o9^oE6 zDA)EXzfEvfo}X9Wc}5CPB1(RO;a2+IWYwDM3TFW5`#yppC=}kW??b~J*DNt7e-udf zyo(A-Pap6Pf*~vTi27fZBuVKPaKDz|BFhJR8fA0}GounnM}`(@%c0UI0LXl3|8=OK zv6T8wQ5ik~eJ=z5Eaa=UE-1t}4)|?}=d9p{D>xLY2L_}PjeVj-mgWlv(H9sVuK-Ix zw7>k>-fRjGH1OGKck^L2D1G<9*YOrBem?V}b^JHY>!B+9Kz$7DldGOp3q|Y}AY_6z z@QG}OT`6nEF;!Z0L;$PgjG(1r9luB9PQ1i49co5*vY#dQ-)n~eZZ=8D`|PCg zWoc>&Ai`lV2szY`ECL_~OS-~aINEr`VddAQA#a)?ld`B)!FNGdj@*7MTCIhH;N@fg zN&(1WyupiWJM`P7<=?h%b7)^|+^4UhwPyw`vR<1`NwcoZrO3{Q{)7O!5R!_9>z;ZsUq25Z(d&BX?z`-_IVm_VJ;(ZUYCr%`*WI zF%!WxRmy(=3DT6x%NXmi6dFK2J6rYc!AacR03yy1st!ncAQi@v9T?0g zjnlM=Irqg;rTCFXN|YWvqprA^qz z<-Et4+8Bv2{7O}l=7EK2%7!)b67X`Z-*2`-A2D;E@euPJe{QtuzFahgF1_FMQ zhAly`B`v5a3;Zf7H`8jCb$L~EHIawf;@-S#d8q8T`I0aD_x|4gn3yIJn4x#<%I&fyi43APpzzkKc%69>m4r(;U_-Ho1ojAaDDj6LK4eo5Ehz znG*sMV20cWGl*WsqkYnT3;}YcxFqG5{}1qOVa;lS!|7g|)x#DQp?N`Let7qQ9-d{I zHK-Vok&LWXSyA<{nb6$_rVF~?6Fi8y?Dd)5jXxR!o8JB<1W@8Ss^dgdTl6P7C*`0) zo*J@cqC6-RPT4~Q)-H1NwG9zYU^EOmg{5MonSEvS_`aLGZV2Ce~Q zy}ImL2;u+qSv4Ul)-hLx_PI7H?3kE-&Uk__{Kryu;>?3 z;f}*?EzhfXgBHpQ5<=8kb)ncJd*U51H-Fy#?m)U6;}Aqq?$8O4U9<)O_G&!|{7(A8 zqt=7Axk*h;989LFCDU;upiw6gAYT0u&A^)Y4(U*im-m!#oE|m#&8K)&f_!KK?&+)6 zJ{35y%n{j-5BsZ~mwdPY>yW=iO${>eq6o?DVXylcT7+>{qbZWPdoAfleK?5;@M<&d z6hB?6_I(%iHt+A0cTJW-puCe%{ILYa>;li=#(VX=x zsX*R4pG1*C*U5&MD3nyt93rau{2AhyENt}v()tg{D%2nf0MSU1yGt0f8!n}+Pkj@x z$XvQ@2eh+~!&AmS^(G+lMfm=dKbIgw%;qW|C5dxsgg~OFth6F-8aN@}CUZOkAic9e z0xHAKn~zc$EqL!SVtF$hVp4597xUgo%mZhED!e#2LYaHuZ3WudVp2D~rb{=uWN}qN zq3*4ejW=w693yVkmw%G!kFEj1lfG3ZWDw4;xLaPO@biX%=Rs>bnm=$il#YMq6IUYb z>6@;d!)!}YjVirI?gP$wR) zYIR()?UJXrb3LOk0ge%l5V$T|Lus?9*a+=@dA;ZN2)W3w^Ai~GK*TRn-B05=27r9F zu}y|{$^hFJ%p9u*4zRCw{f&@!lI|#!Fr#EWK1kVh?g~RY8ihM-jC!;Jj#!I;r4t6Ge>A3|k~!v#``#4Q5s4T!Q!6t+gTXmWu(3 z0}`e3eXvaLoXxkVGB)~ApdF`c%MWup23w`6BiXYTwkk9}r0ghYj~_(HgY8XfhmQbY zyTYLPGT8*Q%WR@cFBh2GvJu;#gYlT&^*X&fYh=lgRNcG17P(Nx>{IrFe?bQ7mQ3fN zw@+eOy@?H(eGD(%SW}=%_p>^eJH<8XT~FxUi}J4h)yfHfwv6*q9_Ue?^jtvpu2K*h~&$W zh~M?G0LK*l)S~Fx1OUc0AW^!s`q3|??)8}6%V_N)o$jgI1*DSas=ZFLd3vhYo|Go5 z5{!26Og{v2O~2M!U*~)+RkSk6w{==fSxR&Cs%=GBhr~VFXJ5(K;{1VlO+}n!rL_PE z&2cS8pOMq_7!7mhEe3LqIY-rXi{XV8i=TPF<igMZ3K-sF0&hV`GZL z{i_7Zig{agGp~+8ufW)|0ND5k+8s4xOhBSR5i?*XEdSnV&I2Ufj&(wD7^nL7yZ@pu zTKh(Hc2SrBgCZ<=g@-ibRoB_tG^FMK9d;Mqeix4AUDX#WR_JHjg!RaKV1kPMAt2>( z&#GBkqgyr=g{SYnSD%I$sP)so0ew~S_S1vCRVS4^Evqe)UF`RB;VdVw+;j5(Jj)ZX z26wA=UJekfAtQMi0I8?xMvNkTegSu{u<4u`3W8^G=|v(GEdrKKczA)qf6Z(cofj{Q@9}DKez$7$pJLWCp zX@-TTUQPfRO|Vz>kYMYOGs!Zi@_(x;h&zw}M+tKZ3VMK5Ishb2lad4a6)i5y&SxW2 z{48>$Cx)2^3Mhv`0t)lKo#ko0gA!KLiK3~ttOzt5Wj~I7Khgpv#s=CKm|n8;J0#xS zL~K;!Esul2m|h@p$UPM=y&XHHxgOQCpT`rqT^#3hFiPQq&SrJMel=Rd!P5JT|IzAn*t^_&wS19%~QM}1=lTl4x@y><5` z>qN~&;wp4+FhkG1jfDgLL4!yiOKc~g7PMYp13C9@hXbV&ee18k7g|5Qo&`5jMQB`&)$ z5P=o1d~tx%;^@?9yOU(UN>@V^ijp67Y!D1+FooCZB52E2PxwGL8|4moWeI`TzFGc! z@KCbfyjUJ=h|d`EfKvLWve?u;pZG~>J}=APyf_XfVz6-7DAUmId(lL{JM=ti_<;Ln zKqCAUODACw)~~R%H?nXT7r=+8(vPAK(n+9Ggxydk`+Q5$v?j@>RvRI0Y~QNi+el)31AGs_VCUKar$7 zE(N4?BnTMEA4_x~=$7^M0?J7H$vv&HyG9}B@2kc4`W6lPp)`q_-szZ(6*JSZ3 z4>`)cEjA6{5KSFmm}Q>y-61x%Z!I`e@RChS-6qq79Um{cM-AI=FBTJGmr+Ve%MCn4 zU%bcDPQ`fI6jUnKn@BH8Oz@(>suQG-OiGaUv|NTl0>Ejdb_A>C1LZNGRb~QEwr4;kC=UAg$slP+0M5mr zM0T6E&&S@ZXxpe9SxMJq0H(eIA_5&vA%s|m`xd>W*OHQY01%;cR;J{TMldDA8BO>p z?oy^F+G;aLkqUv z1M?9Td6oB^&d=biIh=-{OrYky8@tQM$*Af~|2LCxG37 zE}`g=9C2vSS%))z`q}~*gtBh)FjUw#+pg@s;%Fl#Ij^}4WTIU~#|rwq+ZjhIeCv~G z2pfIY(Sx)CBZzz%d&iVC%5)EvLs*v@M@E1Zr9~U$5)Cp#mBcWSLKGV;-RABzN+fh>Y#M5efTYR!#Hi68E1OU~%wdc_2)zqjMLk>I7Z>%OZIxVh6C<9aU=v@<+*ndPDoq=nzd*O@R+bDr{)+-H+W6kN<9;m ze7dtRh|&g<7A#BsLr^3<1_%g;rycZs+oaGR=Uw3jU@`YE>=?EUi|2)K?+8Fi^slP* zC~J05v&|Os`>?#X$&SrH98SL4Aax^AXC(yIi_J|ukZyh2eqvBqELhrDzu%qLKH}^} zGQJj7g@!n5^p{p^9!N9ZHnr-QzAOowY^W%B3rC4Y+W%1CI0OY~6TAX8;_pKCyZ8U{sYRz}R;@k@BP}6&K;+MnQG~ zDYCEz^GQblK>St`^qb`6>FK0e+FEyx=@uf{E+F#l4EX(X_2QzhZINWXx=3zL5#XtS z)cv_pRQ{I9|Mnb;AZV(C30XMm&_w8=>%K>SS=>kuz3sW@*H~Lpo7Na2D<}ZEkYrYZ zEE2}}dI+uAdk-A*6U!_32Ycv6uEc3kks78b5gq^#zQ z67#%QB!9KMoBws^3j~2@lN%Iv+f=v3NYymW0d?2$J8%*H0Y(4AOw0JfElPFkalo8> zC~UvR-%aT}e!D9`ZWvM{i{OWtdh!^TOCv_NlcMNsGx>tUu5T*=!n8^*rE&;>Q7bno zJs460%9_5#9FT0Bf^K$OxX*B;n5LCtNY1mYgM6#HXHbeR&OGOr+1-f78&fN%!>V46 zA`2(Y{LC+dRkgPS*)d`hB4v7E#5hLkW6I2+HOFlEQAP<=Vs0_D)W62I(r4QrDI2MEB4y|uoEEYDn# z_0I#_#DCPUNx$I4Qr=&{+_F_p%5P9xunv|chQb z&%AuY%b_Ifj4$bX%8#kFHQT64z)t-yjMZ_z>K7Kq_=Z6yeZ)A4Q((I84z-mHPFK+> zR?>xjr&+C=%vSiN7`6G#026(w{hY~FPdR*6kRP+$K_tEtGMvqNMc(<<<(A-$Z+I>j z{Tqm5<>dRY=7F(!7jpfGD3pA*2UpY~KZ+faP{K|SK)j^dt~CtGd<-3kqYz$D*?!-$ zT{5;+EpS75vJ|`AJ%|Sw2oqIsEh7C6`s!@mj0F*ts5^zWlR0`)BfWGQ$LHTAeAlx{ zQQbL*>r-Aa4AJt32G|{jeBqa|Nhk1Xo%qIq0eNdEz~Im#mzy~yBaS@02?3?4A*h$K zn4)f6tiP#2@7J##%Ijaw7>;+3h`Nh@^NZ)-XM#`Tc=}H$+vYRDZ~n1;JeOSOigZd4P-*de_FyLuurY(H0G$WOyRNH|%rDj3OIsY$?2-_xf!tu+yo{|hbL+mYZMt@AG9 zwY3dy1fhatefDhucRc#a_h>%VMUd71v$j{^|EaHhh=yR8;nQUpGrIv0X-MM?UB0<4 z0Xfykt=OmFv#;||0t**Kc=#GC>H;&M5`%ZUE)Zc>%S}_fT>Kob?IUX)_XF#$JaS2K zzV$3x%R7A=UkaB%!1Fs{T(GilT^u3eT;HS($yZ1ur@;(PF@YWdXHc3DDueos+S8OM z&g(1__jHN>s_0WKqM4oa{5&sbK?L=45=JmIgBHM7(SN_PfW8#F)6mxu{s1CE^bdsK z1cO@jD1FnAO)LJ3tPzaEEyk7tf*|*zOKF-)pK5?a0P{&IhypZl^-bLx&i)X;t_X=E zGqjh5X6_fmGffghd$d*n3L>A+DdK1i&Jg_j6!V+~^Gxz#AW*?7f*1`X(s9GTGM&_- zJ*eKIo?MgTIv>?U$c3SwrG>ir%QvYZWXFxR3l%4B=U7PVhfL<^}cL=t^#;d5IE~Z!Qldv zuA+cYD^`A)MaWT~V;T?mtTGgIEeTM4=<>ZR&Y$)e%r`~NvDX!c_`XYoD^Rc#TIS>a zxFo10^_`gDu)%)smuDmy8WUId#2tqPbFb_92Di=|btWI2FnhYBd1%_|3MTh=!RZ_p zc!@d*n(5;x`@YIGSk?3_;3Z(Is{&TcjNWewl7M6UG|*S)hU>wSswv2*#t5E3IJkAGZ33r2E>t2)hZN7qG5SFPRd)DV} zH3O>-bXC|Ybv=QmHE1+S5a*8J`)9mpIyN$}o!M`ogW5Mgk4`LzDeYB0+sL$A%Chp- z->-6g>!YO$?@xthk(M`zvR2~KZO)c$XoOr@fR#4xZ7tA`{Q|JBQV&=`qCmPQ6$~Fs zQ#m>jRvC*Ag!B?0)8EYv>Y7B8)F9oJojOE>1!3xxyw zZBF`C$H^o{vLmWodi(almh{>TU-u&h9(%ra%4>tVk64Um`GS}|rDq`J0%$f)G1xs& zK@^U2Kms(_PZ2`|k!dt2v95uMGk2{?*~64zcbs3QozuMeyC4t?Bmm93a+58qa26O` zh5x^WT_9!G{lsqHd@%AQ4JOCqe>+$3v45O?r@3}n$zHFmono#Q8e;D0V(qvDx>6$i+&}Yd;-~$J zFO5j`=kyRhi^ITlYFg)Gs^Q21VA3cuyhv9Ci7k!;qyV z0A55(y4=AJ7lFM}0|v`qq2{`Gl&L>lIQMcZH^^r5AtFudNq{Sh#j$&5o~o>yXOkm+ zkKfQ`UuhOL>zeuqbHd;~e|G`&Rg51F*0mDae%Q#M5?i^J5r<>hERYlQWOW*Zf=vW; zbcsC%Co8GeL^v1h?-DtpLi@op7tRV_Tq!nShUi;b0L4{UM)pHf%Gsxve3Q+~R|mzi z2fI~9kq32u{#bxVQFh27TP<$Zx5_P-!YzKBFZ))aBP3>&Ao`cn`fqU$beXdWPNHAN zg5l4V5`*!7lQ?avoTHmNx#wT9LjkU-M{Dx1iz=?HF=2ZN$np&})<-c?d>fO=$JYo= zyBL}MuK{W~*ERhbWZj(=_hH`l86jbfnsrNuYc>fRI)UjEMV}Wv$DBIgYyls@(Xz4t{!N`90EM)<@(CBYXUKce0tg9-XWFkd%@Pah5O zf*aCi%Tj+|A9%amQH7awJv+MEhT4-+XavZVRW1+@%|z<<1JH>PBqjrOeu(c-ebsdJ zE)tYSZ4XO}zr0%OY7L`zsT=GP?pGK>RRBJ@D&B-*Tfe1$fHjcIt|nglR0Inm6OYt_ zydXWOMA|KHqExy?97=fj$Me-JGt)QCyALg63KBX_Ee>4BLo4bPVdmg5pEd(pn6`IL z5TJclo^@>J_HWAH`pfIza;Pzqu2SmnSGZ@i%})GcL+jiLJCOL>th*)S%}pg;HLW^v z3y!$^wSh9O+TtPq|GE?~Tz=aZ)^#oS z+dF{^Ye)yTO5d@TU4VBBmLQ=ud%+4;2|lZH=HUCqmUcf7_sI6{t3UfS?X~vQ-kB`* z_bbK@Jjq7!GDQ`CvTqi@C~~GE(zW$ZxZZdWq|w)M=Z6z7>-RyyCjPlJ_>(99C4Ctm z^yi@a8ym#7G>x2?mlyegJdPEGcaZlNV25l!lmXM>Lms;5uV^F%;OZlQS2D`7lX|n% z$tUoz5K(eKGvUjb{!rX<`EQslf;h(_io*MCh32{|~cP63(uqFH9p{oZ!m^XtG02lTW(I&tMK0ffkAGsb&wWBq_cR!#PR z4N{cEd25h4lUgEc$d4A0dWmYdx_7YY4htz0x(DE_S%HcZ@Xmn9SV8l`y_%X}(n+_n z@a)Aj*FH@coctq%?Rd?)(0iq(7eoG z)aoG=5;QV_cieq#=LOkwj#IzG8IliB%8>R59#`v!8%*H&3L(6GFKmS~4lty^^;Vbp zxQcxLoECYHS3TI+G0qLv(8aI!-&sFv={!b7kIpGzT0pCofT-d$WqAz6M{g@)Jl8=R zUR&vReLD0bL+sX_9phIFb5IBt;AS2cnxvKd{lewDa-2gHVLR?K=m}G;$lbH+bgH_!ZhUY;6On=F+5X-2?F6K{nSsU7#JXV)~twS zKGf20VOYW8>o?G_2MPgf_d&tPlDypK?VElrw?%Z~z-hG0(c&Y8GD#F+K$Hor^W6)i zi|oXLI1CsNFBq!JNNVyNncE$&V*=4hgj}$*vN0oBV zm_kSm&-3Nz8{oQU`7&66KVSA{_RCLd(1+Yci`rrJ1Hc$<*4p!F&hGmc|K%aFG;>gK zCa}W~z`Mvybi>6`{`_|O4Bs(No4EVjx$aZT zn43TorM~i)azGJpkoCc0gFs|cy|z6}(XF=wl>(A3IPtn~04mTU)Qjxbp~4dDSR(8g)51%{-AurL=0<|krffDtcFRQnmBX#o<-5+-c{VrC%HU#vP5omMdVW$bH zeXi)Y`*LeV zVLt8GU4G0in67H9>URw$5ZHiWVX%D)c2Yg!>@sXB#}oImu2h)87r&5S?^+JL-suKQ zS>Q}KcwwfR)eV$U(3A@l)46Z^q`&~ylIqBkhk) z%zvVsvugSW^CQR^8(xeCT1&$i9fxH~XA#;xgQhhC7B69J7{DzG)8*eLZv{Y+gY$~b z7C#2nM1Fl2l8Q|0ediMh?8g%Wjr6?8{0#lJxzDK_&Y8n9@7Kp&`4JFD$!ff5*F+Pr z&xqtI(tgGEkdfGgWqgma&jWz-;c2ONIVL;PBEF-7b;$bHG?GZS(A2OvsdJVoRK4dd zVf+&U2OUn$Jg8p9@6n9ia02t`ueb{QphFdC>R88@3HWR1ut3l>!2l)&G*VbxoYDVS zAI(q7g!f!-$r_+5e~!Tbldobj2;tGLT;}l* z)cark;6av<4<6_g18v@}wl@UFDS*ewo9M1Hl@)KZ6))AE^l;jp;MLu}N0jU445%af zu%Xu>3@YQscb95dz;@ui`Jq#8E~6Pfr9Ab+it`%UiR`DF_7iB5e-7gjI+2^Q=(Sfq7K-%HNJ|Wo<1k@yy)}h*qp4)p z29n1v92j3&?W`TQnvWiX`t!%N!Yu@e#TK0l3u)iG0N?iG<$iV?Qefq!c5s2jShYM! z0%)#?y&_C&=lwXv>7kq7M}1!%JeL%M1Mg@LX&KJzZvjMV1vs@<^%&=!Y7s;<~6 za~)B;MB564Tt1*0;GUnphlh}h9Hc*sQ8Tmp$N0X-LUf{FuvpHhe4zhMss+usN9Z^8 zaQy2<^A#D!F#-t`#IQa@F?pzh)opn>(>u_;pQmkw66UOwUFK?Mu96x*3pw#isufiD zHPJN&$^PRW-`KTt>%M76#Z9HnKnH0I`Q(UKWaqKK++m6Pa_3I(1ZFRDvr z@Ha#qxXw8xSjb57y?J3-@cXL7m7WZnabd@=j)+l|VHrBI9qnSo&Lw+-uzb2Od;nO1 zI|^gf%F{Fhafihn|IRpzgcn;)3Ld{I`ik^HE`|7xl~LsK7CMg-Zui=J;iRZ*vNlk? z0-$$C^gz3ol%;9zH5oWG_uiJhL=J$hiX;M;a9P#2a?;>Fgx~L?rv}&*{EGvIrxSg= z2IxJ_^7$no--sk6wi8NjK!&`jF@-)Z@J+gP*r7Tybw?!AfG6}4DYtE3iNSKyLA`si zE1jrRT=O@rIZYZix;j}n0NGBz3vM_Be|#Z>SJhm1bSXp0(3pxs9_k21mBkm#b3e^- zM{zvsxrD~(MHaPF^zIt@6AN|%eK{qvAF=u|T6dC&23x>uTgCKlV!PDyS;l9t2fzQv z!96c52tYw5VDWK)9vEc%V%$}o5EnEyVd$>;re*V~dM`BxxpqxM)4C9hpcxdHh)B!> z15>XCF`^;q0Go;y)b2zyKOW4zei$5%tYx6xlphoL3B%x_V6+IyAh|$^PS;SYw6&B~ zIUSxF2l}&=FmHDDq8Tnl<@Ueh^DrFrnXqiCsA_OfI_38XR8wMJT*#Ka0nZV4g-yV? zrMofeE=P~>IT{-;5Gpy5=fV0*L2d}%LXgph!4i4!ezz1Aq_UdFs}EO+Nlhg{lUxXF zM}K`AH9#)?_XUJurb2qzm+&8zN8jbMcQ;@5WLvXQr>WSn+G1aaAQNuE(t;59b_e|muVSyv3Mh{)e$m1slh(pjRP%Ft=(%vA(5r3=Rv-ncv7_kcaVCdWH52bF zWCvEjG=3^CQ#I{Pg-mvAm#o_1Z4L^S`7CL? z??jZH%!5v_t-pRa&=`fH6io?eIwsD2dL+kopD+^?idw$Hk12kWOpA_(@%3%8mYj#L z*YbfMCZ8E2o7p-Rl2HCkWH|%IKivmC;6PJMR@t)}t(_zj|BZLO1{>MkU9PDArWoB>j0Ug^MxIbL!KHP#- z2FQ$LHshxa^RZBXAqicEK~H|~=svrH7-?=+{9jgl@r(HDOgcL1byv0=;IV?&`2{mY zX5Pl9!5}n_M7IWgbAymbRVkV36G5yc->K9j@tRXhRZ(%O-5Ey;=~Kno#c5>Zqx*T%45w!SDO zO{-kpjb|OJD}U#K@BR@#mlZrdFFAbIpbI@rlh+tOHny44W3fNGWPg#ATc)7SMf*9@ zVCnCGTw{(n2PoSOtDAF3Wbo!0$i{uc9gwP=?D%&bGoZ=1DloTtZ3LIXRjSrP21__w|(M=;&a=Jyg&5*E1Fh?%@$*gA$%tuC9??;BhY6)%x#MRED>`*fa2T6J`6 z9ll_mGxDW57%~5QEK~8H@MxQED)HK36!{X{Va#U+N5Era)e)44T~oHzntY zc3;de?Y)=MQXk&((D)N=pLD`g!4{g!ibU4aW# zhi?_d&FOeIuNkEitzUGsPsXCXKxgXqjb>=IMeR7qw76?Pikf@eopTZm;_#Ms@@DGh z?}>rwaeZyjuqB<*grm8N7i_@tUn9c8iAA~W zmVaS)=>lL`Ez|(8kz7SM;$i?nGH8HISsEVX8W&{L&hEzRz5!e?ZUa4)s=7dOS-(Zt z`k&$@N@SGu$deAx~S|F7?J~3L}WEc+L4K7ex4GL_O~qJPSt9a4n~ta z!h1|9yfp!9W&FW5Kz`WcqQ?!|>plGmY=IaXRLFBUwFBv|s&j zIXkelR!F-Ap0~gR@UPXj&N5LbUoF!eRa6eV^N?ccSilsM|F+p1O`kDFL;mF+G+AaAcD!)vh)x@m%MNZohR z$gv^2CvJV{K}%G9j!{o0+kX^bOQQKghimZSclya2vrRsDCK|8bv_$NDet4~y*YnSj z&E9h76>mSM)~^EZQhiB|G=;BPCMXJebdDmjW`1uk0LH5qlUc~!(6I$! z-hS(~0wUpgeK30PniPPh7|f=B-_HMu(`Hd$OwLhVqV^@*LA60HL~aHdGUn> zT3%rTgwB8Kh_sIM#n`GqHP&q?^Ay;-$V7^AZopG`1y=_&LB1WPKoJJ#jmrGxPPHrE z7*k(j3tjLF#z3Kvw!>wtjLa&`*@@Dj6m~zgKYuRIL6}4M*jYW)NP91>U? z4(h8~@kBBTux!n7Xu{^fO?%8!k|%nm< zJV@gSZ-If*I1#lbq|bWig;uSwKiE(NrUon@-OAa3ww2C8;=KEDFExjeCW0j+(7L$> z@Wx#fvlK#Zsh6dDhn1$FkktRqN0~6{J)P?>j=VR>OCW)bSbV?)Wae~niS@3j)Lndk zAU?aAuKK94)9?3pElPKA##4-65W@iZ;Js_|1`=K8Y!|N*N7W*xKR9lquOI+l6cqXs zHYGGr#=g{N{7lHtBJwnvyh_+o&Xwe zvlu@W7P6??W|mu)bP&^VIuswc2**sX-`90dAZI={45le!It={&ku>T;Q z3dIFJpji?;P;V-*d}6VmjNT%-3hjzUltm!w(%A+~^8whlq-C9dYPQ7sayKq*ABRS1 z%J&sFmS;%iTI|SqZ1M{iKpqBS(+W!ZgVWpEROTbDFmu+(j}S}MNIAjB9y&PKGTB*% zzfT}Hzeg$f=7%>g2k1bYowbS5)${y#Xr7rF|6`hwn$`F$8Rg)U$ z+sC<^Yde{O^tj4lY;ufNQOmDnUWrhtE<9J}bN*20y;wieG|w-=TN6L{7KSjkHM{Q6 zr;MxK@5EmL5~tf6>Y(9_V!tSMDD6ei4q)kMX^3GSV@2K012+LH&lA=N zJ+}u?C}6X9lqsy6#r@-UkkMi{H`^^Rbg|SR9}{a$^ZOD4LYI-{<_?(m{yoVD0FRCW z6AMPJ2cSf<{M|eU$_vNT1*Ns~73P;K6c3iJBG^Bhq!%!;P;4%uI31RNyq1B^5prD; zN8VmHl-23evlaq>T9#Z}K;|z{z&gc*Q5Y6K)NC_CG?nQ9 z({lQrati#%HHWXBQE2cREh->}+UhIpPGU~HfHhn-EtBMk`=SxYp&da6m) z5%@4ePoH#hWe6Zr&vVP%```5z$^hoE7>*6z@BAD#DxoX9gm8$+t1PI zM_lm7)gSU!(%_@TRkbzUci@HJGWv`NMFIjV?e^8HUjYh&?|pEV@fs~52dZFoo&Ytu z`Z&mp>fWWfbWhRcWA7~Oyn3y3v1|Br!)u>(HA7sOdcKx)-%NVAh~mDKr)7iiUSGl~ z!#4}sIYW9c<_8D$@$@08IS7N1?UqUT&aW6pw+-L$zSVRAP4ob8XxaHDq z&FtD0sO0aOGu_lWs!O)#e*w{B4lX#77agq>iu zK1) z8iSFy`tcj}0qt2og%I=^n!Aq4mfw^@H|;>=gg0LJA{}fZdJ`&2wZG!;ad-!7a}M0Ql70Ri6XX-1 zdZm=#-&fV~F^=8a#i+SWaIFz2od8AjI9I1dvC>3`6G;#Nd5|DlwlIbCNDI}(hbU4W zfAQkzeBR;&p`;mh=nd2zZfZ)zI+VJJTjNX}?9-AEFfK4G+TIEl8z1iNujjY88QhWE*%tUQxw)8*$Yq?s%SKO8u~w4$8A(mI`S@j#Lv^^n z4d7H>KI@}D*l-!;-T zS$g3XvLmZ#S8tD3mi(d(od^1)?65-nVm~S0G*r}s27(JRGa;khT6_;X0T00ZhJITg z`~EVUMvF=L33}klZJ+aNpTz4y0P8|8s=ZU2%T=ICBb-_9*O$CD61XY9VL!`X$ag^jExAh6?8HO`AV z8Vbr@Z3;$%NC|Wg`uKr(+&;o5EUN|ysBc83?nZng#|U*>yja|f`oHtO$!!Yh=jWE& zZ$SmP7i+vFYx^Sa##{!;yc>1lZSe<6(eAhWxkiaW#UOV~(dfzcXG!PH2TDN?Pc9D% z#w%Io@zH?i5st;;5VMv~G7lH4?INrJ3$gv-!BQ&(7FoSt@Ui3NiGP?>ABbRN3eC2d23jzYk4Bc%V8~ zxPHxVy)&yzJ5A`4XGG_f@c|hd>pQCphh0HUd{=2=V3c5~iQ#q{caiSHc12qywXJssQc7ChLdJ{lcUGQYOF z)fW>36UrD!2+g{&6zw|1Lahr42Zz=Z;h;}KQhtMD?`fI8Bc8&BDn-Y6lm?Lp_KUA)>j&?82)GAJ)4K4b^+%e)h>c{vS6hKc8B59= zqG^1xx0Fd5UPht;gcd*8z3Uy#&WO-@1;&sis!6)!cE%92_b~xVhVZXXIKByOCjr>7 z*60nI6bMoT>-&AnPVigF2N9xde=9VQe|{AJjI$Npugra+0)X&KM~qr^K~<%mk!CRM zX}FsGBMsRiSh-Pd0R=u1A1|5=LccO7xC}f10-_PtNk7BnARrlm86BC87;Qqr*vFoCtMEKj)O4`in|4}C(2@l3P3Q5$7620zg0d_y*G0-vwVt2H{^hxA54K! z(hmS+)wbZxk|-}(OoE3qP&!s7f2;n^cp$ODn8Wf$iVLdSJQ|^O#y6?BW7@&X4q6NT zp>v2d>8|3A@Bka`_JXx1jfxpQSrg@lF8!fVT?My*^Up}+v^;(%S7p614phhDy*|HOU*wuZFsieQaL8{InS@^ZD+Q(> z!KV6xbhjtR;>S1JG~r9o)`tf_e1IbhcsRwFvSaaRwiLQJw^!+cE@!dmA*FqNs2xALEGJhDtxt#=S=R;g|5o? zo3o9k0oZfP&W&x*iLqmHIVci~G;O{IkY&7>AU3ltPnn2^YQgqC)xBVK&5@5tvJF2v zO5ggafCRe6C)9CfVEgfOg+=~VB^FCzgT3GqtDx7haw4nfD)E-&io$AO#ZZa_5Z>}Z zmI`n)|E%^W`}i+nA@c;Q)pQp?KX4GrkB(%_CFhHh>EkW(UO`9iu>m0;<20>Xyhn)| zs4a--cSg0}=!KHnKRnrpl6UsmJ=>hFg@D9r#W;rZe#QcH3&W5v>>cV+$-XqEL`eNC zey+GJP>a`g%DpksR4dhZ!F`H-5qe#wujtnV`F9-XcZL{^>no)KrVd$yf&7{YucOd0LIzfW#ksu*#pKtBgKzLFUn5R> zX>4QMKPXwOF^XC+NJ6V}Q}*n$X#w{*5MdO}ZZ-)-jaH{CV_r!X^eYBE8}wN3C9VOtW3@*K#miTa&>*bDq zA-oNn>9?l>+3=m$k#6$$9Rcfg5jFh?1QJ$eQoWM-tC^vY3BoQOE_iPGSA}%e93}SG zaCZ`UlPGzIoCP|^Jqp91u=x26g+%A5fWMv@DquAZ{O_f$(1>N03lP>@|C<8dkMt}U zSSi0U(M<3v$YU8Zn1eg%0>bjCf$;kll}uwXX^GZmc=OM4`Pu|j%Ui)5{`E+4-v;CG z72orTN?U(`fvg022?DPHH-33^7dz7+Y{PFA52jBZhsfm95BIz21PmubV+G9kBXBy> znA6NiWZluiVB6#O1^GO)zI`@=BC|-AL}T6z8`LVJ+AN2E!c~-2@VGEDtYw~{DA5A1 zM)RfAcLbWHa5GZ~8w&=A z(rsu)DB%Dq+Ir5UPm99GxT@N4DU<~a;THtDf8bj1b2IW@=*2&d^>nn&sFR}*B0Q87W z#y@4D-*DgcA4E#w!{++-@9aXE=LaL60lD3U#fa!RIqG#6u-2AEYPLn{g)jlD&ZL>-JLAMJXOxCEPsF?W2G}D{gIPHvBn+X6`#~K$bI_Ct9&hVJ z1_7fLJP*fs{x0u4e?Ugc3{dirz?~!^yk(X*UB33f-srW2`kgaC!MXHo#7brI1s3#s zN-5(4`i14)b#Z%I^+N3r5`q~b@kMX_k*+)i;)q3Ryf=!el!Lz7|>1rpa|*(C=AnA{%MlHz>57jG2! zuB_gpNPrl@xo0PS@B21En;HTH@nHW#@uVvoohydP|8LTK&DuPD}chH|Ow@?T#9 zGbQJOo3Ot`aRZ{;q@X~;c#$vhgJe}RkuNYKBK-w*@AG&qLs-X6c2R{g3`G+lX`8*u zU!pqAz}g`Iv_~J=P6uBm^sMI(WI!teT9+M(vr)~%1by%>mYBBtdBE`ESfhw@T5s&2 zMo|_9N@r(sk~Lyu1TM#`vO##Il+*gaK|k9RSs;0*B|jmeZn4&F$Q)%Jh?n3Z?r+R$ zI{Y@rAtSSm$juF{EN23vPVwZCbmicNi=uB|YE4|Wk!4vm{r4G^&q=-Zrmhfc7i*N? zm)9;X7HU~tx8ACJ!J{bj94enHyeSP+!VV36!*jFlAC8?L0R zq&$kA`9m*g;WGUCBcQ^I+0OuKXgqW3j2)C4{Fa~vF_sVdq9{N0CTBJ-AY;BR`#8?| zDJ_lcsPiQW!E6Ia`V4q*%To-jo+@MQ@9gB9e0J$s)GdXF_>0>?v1Z9x+-kr zjAQyB*2|@PSzeaBe)(S=41pb~mHE?a6Yf1GwYp0AUa}7aN`LBI!>KdkXvG22%y)jIO@d)X>3Il@R8!SHiP5*=&!7Zmr#k@!zI ztm*dAwhjOLzE_FFTsRl%;C_QOEnNpK_uSs{mOlAk$t-4 ztpxbEoWl2#Z}E0@0l;?OC)r3)ndde1RKmp=?H1ihd}+Op3eDnvFNy2zm%t$*ZxKTg zznB(gW$oR8`~${a;Ny8ae|i&f=HeVPE*MtqdK?l{Wu%X+$n@e{;wS8Qh-3V3*`h?v ze)49SheA-oV4+b>1#@9xa%24)2M07sljat4W@&r{WGdB>cFTp)VY!9G0Ca5Vu6adb z6x}ki7|h$f3o=ABR8`um6pTg@pj{YK#~-VAL?;pV9E*IVI9d|238MUilYCTJKH(@0TDlaH|$g zN3!3D_^wX7oq7w~iHC6`xvO+YqaT+b2&RW?`>`57U(hS6oS2(-aJ-@+h^>sXFQ{un#B*?2d70m_0bSQyP z?f3@h5dFqBB8v6nlnWhTw7pl#1CB-TDTCHCjyLKAbpViLQ|N7>xS)XUepraHe@6kH zM8mu`jqMc7g4G0!9?5G8Wm$*$QNCZt1u30~OAnXVFJ{=Kpb!4?Lm1{@nk`KSdNg1Eot4OkOu|cB4s-PNR^hNFU7#%4e=B{0qwP4 z1}C&YdsS_sk($eYUNl7Db^-e|O}>-NQ^te>wcy2B=of|y&^52L5MoYu+f+0*nI#I$+rT; zEF;71RNNrp{s}`@UWj?P3(vcm>$}vhZ-}>Ymk3nC>J{5jw=3 zeBWS{458|BzF!%mD()BHo}CxlhoA73arS{-slFebG>%*xJVNZs3;7muEPVu4<_0W> z8C)D6OF|3ICagS<9sndXbpAcx(NsM?%V48uznx`km@3z3%4_{&80g^XvY-*SHu7KaDifkrVm`L{-}?jR?$_VX5=?tb!6AzKx2rYLci%>86vFo^>tK;{KiO@7j7Gd ztBJV;wUV)V+X`0+!-r0tyeNhMwbq7RfwgcR?PY}-DvGN^K9Kc-BBJ>idG-d44b4K# zsU7T{fTg+5UR(Z4fTD}85O4o9=JTA_rit#)n@F0uTK{J(xDTs0B}bs9cI)d@DO6sW zO@gRb^>vHA1wM>OgxLdU%q~ZmZs~6Q7?f?{DHPVNh*#`|7jPE9}FYhb5y{-0a`!YbG@!(9> zOD}jve9+9)k8XyTTv?Vv>A3d=jrF}e#NEW67KKtMo-|6uWCi9ZpZH!X3zZ$!c+CZf z`l8C5>9-gutk)7L?JrtEx%rsAm%&X5yArMZoy?&w+jP!V$be>)g6Q-D^G~o^6eEfV zsvPk-Vl8NJv9ESesiZa+f_?SDA)+;OOd&~-2tkq#q2v7kqIh<(I?hG|$0W*Co9{QU zta_n#r_Zia;gb39-v%XppQDtZr#gW3_OS5&TS>TnECzowvu)Wz?+x`!KU}IZmWJtp zdH~!?iDjt?{+nNLdix9N>7ipI8!Zzw6sg+zf_k}EH{WOU4DB>kZ_MAN-Zb;!%p?YL z-5os3OZONVmg`G!d@IO!1<*Di;EfO}xHlgFzsU|~uQ?;Xh#GXNwZToSyUi3gADL7l zTsX|kme)dai5*Q!j!X1tl^n3B`FQWrN{0=u(Y?-Sw;{@}fwqA%4qkPNXUBaTK=dH6 zogY3q#-Od6delkL8s$FrG$M&QHWP&3wy%q99;QWJ_9CM?YEU3RkXb(1kL=SLGJw)` z)VR-4btaxR1&`ESB`q)`TCHENzEHkDqNK9#R|kEO9BgSobDOk$<={ zGsO^x7>C~9grJc50RWnLqko?u1$Q&e!2LaRYU5zJEL5q=wv%UJ@PKU?CW6FF-7`vp z{(hD~ZMBexJiZ3V<-R2u`+x9@}>ZqK|8n%RMJFy(^%cz4k=)T)Y_ z&{gSspOK!v(`cN3+X2=k2-2u3rbuoK<_K{O4s9*^LVv*F3k0yPxZ$h@C{zS zS1060#oM6ZRgPZfy^RY0tg}(NO+MI-tv0{2I4dMRUvVMJ5e!#AdXsEQ{%q^VG54^K zH|wFq{?V)tJ$*r%v(h>)-=0~DzvD(}JGfU2R4V^CI&TFxfhdZ85Cu7v5;^DC0RoAf zxCnjPkhV@k<1*aE_hBB)$qSw_#Yt5zXtmr z3DkVzW{@rG%TxpS)+znoVujDv=jB+Rh`GZdRG=wMPs8E`^ar7$2zHstgPDCkLRTVelHN!`L&!d0MYQOR$0^_vRXzh`(}rI9%!^oCQ!vVs z*%t*r{%4(J=TAq3`}%s9;Y}{-sX)yMzz;P30WW7?3_a`a6eS-^6Go7J9F|4A2i_oLoncSrQ6R23{6@l|x)lmxby>yDnZ35HpGW zGk6UdyQZ@8&-O7=N338Fqff*3oE95e1x%z|+UMKN2tiC*K?4qcon=|f4$!R7rFH4Z zx98B91PyyfUmzB-NBn?C^9CmrQ_YJG8Q!bP9+9DC9$W@6u~>WS4)F&}(AxwkOP@x) z=6L~2&ve$T6#8&$Y1EtkWen$>a3y+mr0;@sFPL3KKOrB?_O5PBU`nC$ev7G38Efnw zHqb5Yl`lAYD{QOfw4j)e<{#r3UKFj18E)`{{|PWdyq3q`j?DOdVvIS8?YuVM!N5Xq z;v0Ldr0BPD@_Lz`lO{XgtLZEHz6Mkd*l7yE&_?Z7$%i3Ifh_PpO0C^GDjR~O%aOJ8 zJPv@v%kq|q>_U>0#}srj?)~BAJLcn`Kg}%<0#1YE;;9sp1&J2LFNk<7^cn}JOKhS- z=T1kE_PEZUMr6Mv$r$_A41G7c4cZ7fUgx#_Wr8AtY4^wTxzq2>GGBofDhq}xJ1O5s zI_z8v&7zy{*2smKtbQ$SfoIB_2R*ciL>>>Nry3*cU+^2y zxZ2`inflf5E*W-?aQz0uzQ=y+z+ZcVbb!qOVJ$0WF@a^*-*d0`?0d`|Z(_y%tbhdf zFq9-Cu_)v&1QhE5BO(ZORD;t(0;)qlv##wMJZYDHGg&n>QUFSNiY-UQk!?!)S9r8P zh@OG;LVFf)wrkt7uyHm|(0)I=3b4+=n~V1BMyh|r8nE6MYua{#iY|i+C9{5|ZKpva z>A6Za67hzA%AK!l-)P`RJ>{0;=zUX~G*b60#!raM7;Yw~uqOn|ffjw=YiL~;qv z6+7}mvK__P#2Om{d9)9Y)>WuPC`Fs~w+8mT3u?Q19t25lGt%XUnt*P}CUTppN&^&EQpl-SJ3#5+X?srbQR!ZJIlkVFQx1aEljkYJ7^V2%zOPuyZn7oT$fg0S1Un zrcnqgk1$W5kxlPi#q!v2DR~{6-Bkhb3{Zp~ghQ;Lob(8;N@5!tktnM)Dpd4dxj0#8 z^|k?g)`%4_>HA1Yrj_(x2VjHG7w2vJBIplByVPTBAdeXya>e;KaN55tm)zz2IhwUj zuM21!LGt=6P<&_zEGshkJWb0{g(2GvA0V3AJ(xEi6x^;(-o{uEmMRSw2KM$tuoi!| zRg04oyqG{rj4M}F$uwcAjQdHuPYqDSKb8|Y>CO3cYN7|0e?Qy>#6^oK-|{Qm)(Jly zG>^3zF<&j@=mU9Y5CBYqyLb-A>ayjlYA=J^Vb74*Xa!2_%sZ!J0>$RX28XncB9HUf!iz-VdlLaKjwTVERezbuEed#8I@7{eGco_D) zmgj_M{pyZ`E<}ksH?(bD*2%ef!N!C}$2Kj7^N z_oM97F+u3@k{UE~1*|N<=G`qHfXM^@Jbu++J`A~K`WX`7+kZ~fWm-46n4Vf}14{yfy;~9!nE(tbN&(*dP(&Q7q+gKnYiTkrkymIEUo4EY}Mn|8^&DOr1D0aG0PyA8m}{$0G2=j)??%!)eKcA~QE?sAZ`=`WehyjK>@0pwe1 zrLm^R92<2dz~C-C;#HOtPZ;%jy=IH7qzOtKLvN!(Fb1yTPspb5sJvdFk2W%hAjPMn zQ5o0EPF!qtqa}|@KkfCFoB(~f09^qNmY4Iokc+qEucGu#fu}%`O0O#MeM6$~ZC_Uw zmUF^nvX8CJ5j_@n zeYQ7SL-3{m6nAcMdIx~uiih|9IVk{G=-wLY9^QW%tx#v9LVaQFPYjEPblqFZ1ioD_ z5c_zFcXH~5GpiGSB(@?(LeTK$;wBQZScHJ11oihCJNX2hIJLRh#a9Y2;39!j=ALLj^TxP=!_ApG;SflFngINcCi5m?2A#D@zx6xoJ zV$EpG#83p5M`=%7l`j1Ynv@V&=Q*9##9ZdO{f=*t+a`CqxGpiC(c(I$$);G(s;>-= zG?Zf4cijmdANuBcF^D=bYY=D zU+OH+(lSD`q*TKU8-%FI0vz0ed?=(?5M)ZJ%C4}!xhri%*aH_j;21S_&LH&t!9lMB z=7hLxIBK+Iwo19lA0FOdJWqCbazu#z*}=iMqVojb8@7aTmS!M>w+?JGz9ja%mktd9 zOHzA(@2X_amZWoJl$SjIJ(WdFDD`B`PJt}+&r4r4)fs)e9U(k+;0Zne^W+Q zU1ofqsPRNsUh{4R3r;_}k9sC_2?dnw&yp{B3%;N{MK1V?X^N(nhJB3#OTvoa&5J|n zWI%HV27~$jO5Fn>qkNdCfa_K1g#4IgGfQDO&k2pC(}fU?iqy-c^T~FD;EE23w?1yn zmT+%14Hb_|SMj!YhHt*!pee7bd|$zm5vZs6%3NGr22vVcPb6?y@;M0U8qL3>?t}#a#c{8 zk~9wlG=alS9>Yw&(C{M<^x>RVj6XWxz3L+c$cr4+q_po-mW0kQh+O zmN5mAj-5_R-Wi~`3UL!7m3G(1)@Ouq=}Eyrzg6r@lFXPFhUbE_sE|&!kLi5Rn1}~m zy`{AqKz12`fZNo5tOzW125IBK@DgivlyL{ktBlf;0b)-vU|0q#5%%wq?juOM7|H(8 zH2bz_y20S@)bEE=L@T%5Bvg|ORV4@UN%If_eLe*XfMJqPp#sqcCoH4kzI;haYZrh+ z84*5miBECsPPgz_p=GSC^8#2cd{+M1C zEp(`g7J237e2Ynx=1UwIW9HTIYj3@Xc{U(TNBm+O3HjvNtR#(U2Izeh_v-5pbWI?Y z23)fwh18e7-aInST;l@dgj)l9M;Bc&5|Gg{-}wmw2aw#&WvQ{HP2uA`{-ixY^NAKa z1z;f_Q7b)P75+KGdQLDPTO0DhZ#tSwgD5IMuvRCVJoRE7MNxNUoPUF zc%aX>k|xJ$`YGJ?y7Gz#da^?EeH)XnzQNd;b6;xSd#}??CYD=vDh~PyKt{6;1%M~C z+;nB(-T!}DGD3WnKD88meoMtoCQxBLfOH-s(mT&J2_@_!5Q^V>gy(;Ksjwy0GWbgp z%#1gG68!ss1#V;?ySVdl*GIe@MPiNhCPynIR0Qz{Pzb#lEeFn>+(e)rq?H z#nIADED#Id+f%`o1voT0N5Gg$==R%vm5hGl5i1Lts336jD#d8HK?={OW=t9CK2I-M zC#@USmx`w5BY$h2K0Yx5rX;P^zxx5!?^sct%T&Rf5d>S!yLN`>hfc>uM-1Z#Q|MY} z=~nDC&e8RI$K>fO4+vUTL9*`5GX?r-u>eA z))A1*Q7p!x8NA$N>M03TD)>)bCe0hh>>dB~h5b35c%U@LxhJ-!0UG4<1>9DKes=A4 z197F_{#!u0&@ganXt0l7E;aawiWqSB<9#ch6Tb&jtQd8e#iqL`N9ll_y!pAD8siNW ztN-_gE&^k(RJ^!1V{%_Dw|OHnzIFrQQ4niolzHe4+t%zx`WeHMaQW2Nb|Jz1@k&#V z=~kT@BT4j3RBsAM>{6-QWZJ3bvo>Sg8LWd23nqi_lFCXYlY5~YetX98tE^b{N6-B& ze!3#RT@KU-MjO9X3V$UU0Z35D5P3Cam?;-MN%RBISP3TZvdtR6#K?}Qed;|G_u(@c z`R2vmZ_<4Yo0XoY3n;z=x2Q721zE`RCcfSiff8Z_@UjX#nZx8cQ*uP}qOp3? zJEu55;Yh%#t9}i=LflN(=QI(*j{J)C`8;n!5Z~ZOQfBoPrWhc2Yw}IL!oyvzTFfK` z79+^~a!@2|CSi3*L?%z!KIdH(zQCl2{#noQ^o+ejH&tkI31oodo&eTCW#!E9HLI~M z#|Mi#h>1nb~E>np~)-;G(nq&8Yi0%cwEXPJu<=2N%YUj!%<2AV*2$f^SrkQx*eA z60YzCtd2+sPFk%k49-?Omp#nftvM0x=Sx%Qye5$MEx^#Z9iVyM)0fNBiUCyV%!dtU zAS%Zp!R7a~bs?=+-fw0vZKkKL7d3<%LlcGDn2raIZsZNh12U7y{gU3e1m(t%u(*wW zhqX~fa+YC1s#x|JX6hRIv+)~3u>FbjCM$+ zK@8Mdv_eeD?i2NiG+)i2-K95E%Y61XI^U&@mM?}gqVZ#nTlsT)c93g>XI&>%N|nqE zXKJ`EQ3zn3v`ak{z{3936N1)Po&1O&%VrQy=I!)!9cq&mPtZmhvqbMDWIyXtH!RAq4PX2p5ALKBQ+dBn_tbZ2uA&8HV zRoiP;_z~2!hkEqxitDcu&U9DCVTOs86cCRfG5z@z=Eu4GwE@#NxVx7|DvVdK@io!N zvxUl{g&3UrtR_<7BzPQAlt!kD6VgZxMA7oPOQEK%80sop+pOi!7(5{{+#gI*SiQOS*u!M@Gj91jFYEd%}L?m&m=WgEIEA$ zIxLpR93sybsE_KdGXmLxraePK>I59Ovj+tGRgt|M)S`4sm_Xo`1YzD(u zdjHHJj%6W@hoKqhIX$^$0HQ2H9_ef z(9s7yf$s;jSI@vTUwwn^%^O1Wn{pC=PnAMTl-SXBD@9C;yLWBAnwm3TX``V)5zJxw zVzj0&9Z5pp_q{HvVXU3wnm9iI%;UD*R-0=i^JfyU)rc&UJyopbiwzhF#~$n7o|F976X4=b?!Xh(j7TjIM{o(SLptzypl`^n z`cs?r9lxVg;lOy~Gn(RA=A=`g%}x+^eOXvHnm4RPdQipm`=klj;_)|pVc?@B%*oJ- zLtcZ|>rY*MNYpM2=yxgJ@NK0sHqzIX7U(Zw@)AbP-5i|Hg7&cBmLKWuf*-NJKn;js z4c^r7`zuTH6OSM2S^?)ZQWI_XHpqgB5<&@r4&zo$3|6_xAT;AI`xA0yG+-l!!IAfw z`DbQ#(rGv8)VM&XXh&kGK$;Re_XX7*vQ@5jp{AXWZEwy~ZQ6sq=x29NC@7w?Jdmx* zAk`x?&iesq7l+~S1H*I@!TqNuF+hw3r!Hl25 zXNEW|^UK2^J4??21063Z^Zcc-VsV$r1qlFE9%q;3=l<029GDS`g!!ZT0G>8KX&=N% zWtsLdX(|;4WJ?5v?q&rSO)SiXrQX$$_;D&yMX|S5*=L>CtKRouBd=x04LgX4Gyo}> zeKfW=E!j$=K9S*bpmN@k+&==@sz_4fa`+g2(2+WC21cP5>K1ARLMe!aw0m-E;hS_7 zs(&#Qu)mEe!{0Gt^7%*0nvs_+J+w?@`)rFVz?b;K5D0s4Q$(^o#-NRRbh6Dw$$h;N z?wd_uSBrpww!yBkn*-6YcyWkSV{c@KTzq)pyTn(+u9$!wiQ^!>2gG;fl#GKD0KR-x zwD#X`&j8gDS7UlWCNdvqu8#iaf(*P{sS^;oRvhEM6eIy_11WNXzozB}-W7UpdC--) z|5*U+WYOeDE`Jwb(!XyQ;)mO*DG^{fbk=co1HTdOP;Xz7AqP+SlNj8S#UBvPp4kxi zM78Uup4N_kbL&h*$6B)Jm30%gezWTO8h{@e3Uh{lT{Fr3;X26Db&wduF%0@|b*bK2&+E!NK*1qFi0T?RI3AKd`VPCnpKnAzuc7 zl1TanP&vk$ELdU!fG{NyWLYhkd9dsSar}YLk{eR_+o-R2z4B1WgZ5qcvFhG1;F~_2 zPXYr;Q4sR^2>_+@nX!H?*^s4-jv~CtQB7>03K32ftC}=|O~g zD~=XBpX^x1lIrE&5Mbv#Iwx|IoKYM5yLA@@p%4>}slDtUySHcKRi~lz@@*2X7;w!& zwBP~s6^|f4es@gkJz~bp{)&WGZKwxGgZAtss7b+xEzZJ=JZ=r@yd3 zCV2oWK&uz_A)A^1y7=kw{TwY5WNF{YaP_$=Qtl9xDa%52ey=b?ov3I*}g}%#r}m=np$4%EKpf@J7zpt>PWDkx!be%Jzf>K3iqF-9h2PhSzw_xhy3=5K(~F`)`$?l$?;Yz0 z%oWS^v3`lhgAC~vcUsbY!ysDn52crL9>7jGVo~mwpaGjOVp>G;?qin@T>xY}zPl2| z1jvMnM+Z+kGv`&3W+Mr#D_zuNDK|FQoVP)=ydDksX$s(R)<$jy z^{}6w2%XugGsQ6T*YH#B-_>{RmSpz*X~z77fN=HchmVGZEs4f5k6^AqCQ3i1-0kcZ zyczcPIB&SE5MwytF9GGE_ZGSk={}ktLJHoiY5m44T+M{FUtd_{ zztHG;k7~QD^u9SF2ww(FfB9T8ri`#I%Zk-A#q|#LwzPX40E=XZxi2rG>Nb+8FjITG z2;sl7e5Fs-f$&w{UKrAJ$|9!)L`GoGwkU{EAnr(fWtQ?^-~H5;kQ3bvRjB}p3J4Mm zGsc0q0>y0hR4>YG$b}HmEU=`0K>ON=*^1INlJWbG5wew-(!AmGEaArieK!H6&ivx0 zzy@haWbEM3WVr0H)iP zJQ8b02?fS}#_I&JDgGUH<93D*|-f~2b_Tm2~H~Zv}Zt)WQu)#A<*^NmhgU`x;aZ@XFuv@9|%vC^}G31BaZ>g0ZIPG zfUks4n;u?AR)N@{`n4*9^Z>AFG}iyvM_F>@)HKXkO;5ePq94>%vY~SzQRW%KyyM5P zJ&?l~ZlX4AAgJ|1R;&4B1>W zmJL#U>Gj0Zz>Qy}RpZ|U{IFkC3_d*kQjps3-ASmXux3-+>()YK_^^}xq9I($c>to0 z1ZM*}&vg)XrGx~9<|EpFAR=ub6qtB61JSDXoe|**Y*X-)DO>DfL(|y5z~SxF!N4yB zT!@>^J`~KIU%TX zF;i7^HoBvemxVhJ`7;fTAGlNpVdaOiw$Pt2Bi)=gkSg2_jqj^}GJLvVAA%(y%rv4> z;V*;f>I(RW-RS_+@^CP4eBOKY`vO!{zCz8_iW=_M40AxqRnG!Ac1w$o-$KIB3XVNa zrjjK{bs6eBOcN~Lb+9u9bu0$#m-H*zTsLDdi$!PP zy|vD|_eAu4_nJOe?w5Lq-J&+UL(yd2S0=s`dn1aJ=dN|DV#mEsYbj{+nJAhsunr{+ zH`!#)jr(%^AU}NuWQ=se3V9T(h}cVdf6?i^Vjj#n)0nH$c}irC(h+u#_$T9|%F+a( zK>()qxJ1WRiH;TLdYfARZftM!KcG#CJCQl-UKPi-NhLd~z>=!9%>7k7W7G;dUEK?8 zGO$4;{)_kcLUuitJbw>l)x;Lv6E}g$;z%dYGCmy-&7JPNDfPP!`c#GhF+k40yRrlH z?8fQnaPdl_`Uy80^qCdnQGf3?3%$`ptln=lZHVxZFnY};iOPKP9aQOE9EzLV z-~C6p@UVIeDT#h3W;pEKpy{Tr*CajFYTsq|fh_m$9aaywy#`FX_+f3EgIVXE=$%aU%kBM;`nMPc zIgws_Piop+{Mh@Bz-LQ61F$heC+2dcIpqd4@d-4~XE7pfb?L?f}sbwpMjEcPjXvjT$9|Hg+7wD3{l=AgH zt2db_h}{lbW2$>B{H}=*3B%WRzbeVVUo9AH%9X2kp!>e~gz3oLP%~sWc-bL$?5=^dL_4PDmV~ zJCLsM7J2V1?%g!1`)pn?*Ue}XX*bG_n!0yl6<}mraI0SNC)otESo7cJFY!sSJ*y;BWf(UoN1k8V37j zH$JL!ee&(x5ussJ^kqRUh;8?QWFOG4-!`IW8}=!RWw6ALEELrucDJ^zFJBL*TM8Hm z_N)RW8A&S;{|~3?@85T`p|uczXsGhM@t;k{;OeUoY|SM7Aic(@EiDj+5>w@&EFd=} zw`Fe+3FxRuHzQ0fYS!zE139wuK&D<#-GM9Sf}h{W z^OLjpBf3tXTM@P3DZ9?i^V7)w1R$6^mB9HzAB<55fT}`7) zNyQyYL^$m?dd8-CX3^E;c-Ea-1f%vnc^v=5`7$VF&Mj-omNpHLYOzve^(ETVSJU2z zmRc)V3=KIM>{a8B-N<5t|2>-pTWi5{$N=ILa#(8EK)94zO}X#R8eZ@k1(AxR{lecx zIbE{aZ(@*hj>?-F*Nv-!Qw~_p-}Zv6@!q|k?Qzw+$IW+d>*@uy$xVlBYACO8xaBX| zE;c%qtWV0=%%=3Pi{{B3ZJe-N`c-pCNH2`=f^m2X9x8&)663%BOZNG4V*Y9=ngSe@ zo2Fg&2AMm_8~EQdD2bTamJa3xlqHvsI_Ek}{`}EO2QdxmcrSEZO2SUl?(yW>IOhDj z@U!9@daG@Nqt8m>Iyu+#+b>UBYPV4!R9S{3ioJBPwZU~>o(8GRuJ`WU+;H};%gvMM`lS^K3Y^d+9d*M{r%jvS%pu*y=aZ%y zN35r890&1GzabOa*5ZUyAqePv7%j@d7nhDpL<8<<&d_&^2%l!!Khq5^KxdrEodJX7 zdP@hX<22mx?u8z=j@_{TJ?I*22W^y zR&zj>w5>OXRmvxeO{RWGGB+JtT6@q}pGu|b>8%3wSk10m0AlT@#C@cw$B4R9`WSD5 zzNK$Kw-!r0BGMpvS2-s;GWmVu=16L9kb1!R@B*o`Hr^?(vI#4*Ks{XtEY6B7wpj%k z{btMBC>9ljcK2pAea5WcGPn*2LXw92Il#Spg|0t3@(vgicg8inSU$zF9uCgUR{1Ox zH3>C3%j?4E$*H%kM}QXhQSB18S4fu!zqfTP^yH|ty>r`C1gjY1(^t;JrGC8(N{*4-U#m z8eZC{Q0-U9!}hhE6or__r)H*9jnZ!)0WrMq;Ow6{(15+}DU4&~D<5$3d0Z0Nk>8Q9 zW>8UhB3#HLeHzOJ9;hxwBno5^EE7Qruho2Ia_;C870Cg*q;ve9pYE-7d4(JDV7_Yf z0Oa1C-o@Z7U%!K**pMgxgG1dP>-Yi~UE5J~84>UbSiTjbKlbfUA7lq+I9c-ub=4$G z>h(lYmla(dBrIQWVUz&JA`n|%_I?@1O=X2S^otMY7dwx6wXv6$qjx!w{L`|dXnb?_ z2WKfSo2> zC;>no|$#tj?zxNNhAUl<-Tb;t4CfaH^)cte3J65zGt!&j79fnFx0`xUuOESZLu)l zmvtEVY?(G$x=n27{SmVRuHQu<`bSQ}+wuxTwoDh}8dA-oa=ikd_AzlKB8qWKK|r=y z?i4ThX5%oceGO0^4nGh9!ZFcDM84oZu6m{u6zEA|W+0DjpyW7da)m$@1oyh}GBE~K zO}G-yctCe7XDhT3Q}FM}l0+G^6Z3k>rV@hbbS*?^$7lOE&@LFiOSN4z<3A5|sTQ%& zq-E0#te!A#lAY#NpDx|aWhpa~ZgKnKtBlX(3-?uA-bdsC*wjlWWk9q@u!VqhmOf~< z!nd;@t4RG4l2t8N;y_d#@-Y0|A?Z@NJh?E5MTKtz>R-+Wk(m`;H;zKrz=xm2$Q z-8Jyi9749}87)1R#19?^pypAEc?6QaMy92uSefeBKc5hJ8xlCq$^}bb1Z@QL3ty&#XH6n7O8@@)Hmv+fTr3N~ybYwS}(2S^y+`<#^K6m+XYiHE$`o41m!{%AM`jj%lm5iqYfMe`OI~JUR ze|wFXY#vi!4c)|x=~i=#l9D^#>}<*ghOfdRQ@{e{!}Flz6*oaSutW82%h|7D=FOe)NiwTb!H-)c2$aR!vqdGfvvf+Oc^5 z{36JOLO3O^jY}d#W28Edd7zX5~Pq-y^Gquae-U>jY^g>MPgHUmWW_A>u94k!dn4j z5Jbtu_PJD(Pf}BA90htS&L%*%%_-s-+`{`YB-isI@C3mAV#e*)!kti4k zLQ9~2NKlv9q|&0GM0#1>T{e!6Yk4DqGM|=*bZp}; z2LHia<;BeL=5g_zPccpJqJyAp6~{#gO@jyp&0_-uSM2ETXk%&%n&q zD-U28ha06p1@wCi9Sb{9?my2O!W>@{-j?f4tXtsEr-yuog2K#1S~^_2K_gLXuu%Q% z`&-K>kD@S-rH;0mk5-=d){iO@N;?4BA}fs0QsnE$LwabXr%W}^269gm@pL!+4ezHo z+I|9!4~{07Z@mgEwg#^Q;H^ALJaOPGJhP=K?H>lDc|SNV@`rDR)hY)_GTS;y}^!W128)aP50T4jAXs zoA?xyee@rzV$**VoyT&cKomtkhy^)lNtO^f!;YMD5b*VGSIsPDs;siKE#51UYLxK2hB(~0k#bYvmv_u)*XmE>^_E9*}$-ZZ)bYjuOG;aA>ZS zr=HRljVI^sHQuYl@~u9}bmG*9T-Gehzn1($sZ^F%+@yA^(hoQWz8#Naj4x{3%79tW z#}FL-b<(Ep*b8;j{eX*PN7l+%l&Y!PF?KaW>@6LvRSn3&^|h5_#_+5jQ@fFBdc^tN_j+dqNH; zGh}o4R7d!ekLn@Kj0wHK1e6oqKMng)l~-}rcRzapp$-W&kX3}105hE7=WLAurF)_5 zcCbs!9wPVdU15fB$fH6*A_Ad&(WTV&BqT^bhX|&(A*nVM_`0dF_FGSmeb$y;{ppDy zDSm!Sf7!Xm#rd1{lGlHBGQ;wQK7QZlJ8YKh_rO#tIORRFuN8{)#_YALD;$dUjzC+E zLM&bm;jNVC7eEc|M7vtQ2XXM`1*IIwR!bb8qR19?FYira>udZ2QylT*EsN%mL@Wf; zM!rosIA9+VXj?dpKz&mcYwMue-lpl!zdkxs=5|^Pb&F~IiU^(_Cttw>d3*{ggL8IS zj~i*+a(Ze7KQK^9ZA$t9KsJ!Gw zS>dJjoUt5f`6*5k4_p|XDj@^^IP^-yLmzm>2BH!(X&vJlLi(PHGzJJT1dhp+R*JXK zKD4NS<{0P4wKTCeh0Cgf^yQO$ejz9ijDPeR&CxH6%s!=h4vXFGf`tLXJd2xs0?etP%NYk$kcY}I?1l*=!gKES?55`>qTpf&0Ps^IQ{V%+ zulCQFkUuU!!4nM$59s39hy&fo?__VHYXTL>7zp3$SmIo8hnXAP+j!t6BydZ#CLG;EQMnXunPnuSpi05`DMjuj{)$G$Y*Ssr{JV>Ico$!9%0ao z2xN=D-i8qQl^hF?Zb5`@I?i>6V_c*RmnMrcipsZu~(^1SIOU`F@RTiwZ%>$FtW2uz)qR(=TZKn}w5FE0S-B1Ifb`c#rjuoO|fLl6}VG zmV7D?SRNo(9g^^!dMp4!TMwGO=}WbX*?JD@Upj1=?(=Ho{H7jX>J*&_7!4$A;@FYR z4B1Wru30P9WwvOi<1NNS@ZrsP+E= z!A6kJa8n~ZW}NXcO9O>)HKZJ*(renrC_*pKAFSYY3t{^h;;bonqcATplI> z_5K$Z7^D&hXKALl?^$y|HXE|jg;H=7aIZbx05}XHr`iQn8*ITK{|Q?&*!bN|Y?(;A60=(+6LO5?g?Aa^6H-f@^IWt>k%g z;I#-pa*KSD-fnMmYO<&FMU{jE!{}aPj->Lg03BZXtaDV#lCR{g%*=S|VNG@i_!9_8WiI&z452~}?3QhtCy9Z@i6LGt=K_dw1mdSRAr1c_vkmIB=hoQi+#jSIMYNob@ zXoF|JA<|!byrcsyjSo;Z!7;GdX)+P{OWOLmmU1qdfH!kpW6XI=eBjPWpHQJuh9LP7q(k zkl;DZh1@`XEF_Q^(s1;ERs)2I(D1+OHhIVi}n$ykiAk&3B? zz(cUF?H-diK9Z*MV@7o#ZMQwy-RM(f;qhb14-+rprhUf#&I8;G&ivHgbU-}nyKs0xQ=^3{K^uh#6*}|!1vzeHYMO^X!qZ(uMc45KxCXF zu^7#Tp^d~=#6YtN3~x1X8uE52D3F~EFJp`zZ_#F;`SGdUih1#7HaMok>b%U6rfad}>CnzXE3vgklnWTYDF3}-z z`M5B5;-?~8PxQMgT}0>5H5&{gx!DS(_7*hl%_qv4QMuGvmD+YQ%2-UGL60oLT=-=^AqvubGmRL_M&9p= z-*yZ6QI|gLp*V}S&dcJUR^E+gXGVhUf)F#PpvRL9-7D53&_%3-nZSY@-~B2!#LZxC z3M#mRbW51ToA40>*2c~of4PgPQSL{nGGW7QI+4#9<#(pJi* z4^l*8>m$^iX?$P2u0>MswRdq@{|~A7iD-jImpPExB8M*6Rhr!$A>?qwX55$p2+a4s z2L-sc`7%mZZe2z80nPvV+NI`mMZOn5+e=3y=F|lKv+7_jBDa0~W`rHfo|qLxJY&7% zb`r-%%A8FkLtj}f#XlN*tp2d?NJ93o!f+Fu5o5)>H zT|>yFHn4KI3*zaMq#^;at=E-!Bc;G&oAD*)b~wKcE~EGM&C1BB-#NJEH{kK=4=te# z-~vS`TCN1Bw%o8(32F^r4-?~7fMkIqnQD2?58BJ`z9k4+ztjl?@r$uBkR}0+XfvSC z#C7B)U;+0^CnkKi7GH5kn>C~Ccq95&&KMEL`H8DNU>;4^ycfA=U*v1piU0m8Q(cO* z7g-yBr*CyZE0ALg+85s+EtGm;1%BcDBcxKk6Q!MEgFYdANTt>0PRzTb-4+p~;oVi< zKdbx%H#MY%WWoLqz$F37x%~W6q+;9-rq+;Hn{rK3_IZF*q5jroJYS#p)K8@Kh58+L z*;zoYx=D`ID?GI8I5`Ob0u-7teZzP?Rd3|=Yb*myDr9JcOcLeIn+u|0bzakwWR5eB zqXWB~GcQObDS+RUrX46?*wCEc62Y&K5U+}IOODq${7hYSYtg}2tbu)W;xevkl)w#G zh1qC6=}YOsI2!BQe@&_s<9J8N7z3$BVB%HMXJqbv)1grKSe~Z8lWvEpZ3a$C*ZyYj z!{7G?DWL0qeltRS9L?hGigbnl&}*8O|2`{1qkkRR=CyV-qOR!i`RD55bUgm9+LMj; zfo?AaPs?Z$gIcgP##Z7;kPrg4C|>LyK@zC|VP4E@4AHNu6D@R=LXEUUs%m4TI;VfR zRk@Gtp)vaO4SF}FPYc=C;cgi(4C$=99E8x8=jmZ)+8zer5#6Vc!fORN+yLk}U!AL;njFTKd&AseMg^&!$IP3v9lD}JS~IBvExtF@z! z+Wxsc5Ph!ZsZ`Sv7bo;O@Mwwc<(2F|Fgg@{-K!UF!w{#I#mft`LOb-s++fg1_K|DmDn3bM*n|9V||4ooYT-*+t2@l!PEoDkAQ8_@3gEMuYjuv_|A8F zz~%>hX14B-sp2q&dGql5ZU7En?O<9d4N4N@S&v-5kt<}VMkN)Flq8+~X_7^u>H(KJzdJx_3BJyN_} zTTBMZLf-0Hv%|D05W4=7wnD+meHp*+-s*@SXc$YegfXJo+Sl9=b992K7*8+z3z{?! zf8EGF(a{Sy3Ho{3dxU-*0P6!8{uHD8R$de9J|F?Z{r;}b_nlRE0>+a6*EHX%Ht3@Y zjbmm=t5E%=I}JhLj$^GT<)@0j9Oa9;FRiCK?rf4*MQE*IF7JSmx8e zUero0LXHr7l{E@nk^pLNC;m(v`3^zHcS|bBRrhe{%r!#1qOjOm3l#d$2|VB_5Fmyd z$=H5|PFMQrAq}8G=Gz$7X`{RRA##F~V=>5qDK4o33Obt#yTfXA6`!Zy=YmZ>%a5k9 zMGgHKr{vgdA~#N{OM?YLT+2{YCOHAxd76ve+v;hWhjCfpwI#0W3it|2CO@dJuXN%Q zM4zw?eB1~H{c6HFpG_YKT(k3eS|v2EhtjVR(~FP9+r!nek{?aEA!+FQn!4trciuS?6pouatGJHhtF4^~zV3Rv$AmqZ zRN(sVgHDx&(}G4t2GlKdX@4)*IE0c3u-%U5P%Ze$SGxbyXM|yf!&~BbV3{zg>3z0$ z8n(@Wys9VMQr$dbF%&B!*`fkl@Q**l$W&gpj>F2pqCTV#)JHwiWpu$z9?O~%!mzE@ zpXmz@K_$MdDt8_H(*QVl4`2B>mrmNF<-NT^n|+5ln8ZYalY(ZL+bHc!a}^ zWk!tq{9-r)DkpWjAgBUt{P84^8L-Nb@P3sB?DvPZ0CP=gfu`F3D{9zijdK^b$jlMI zsk*O;HD!GrEM(JIk{)Wyf`UUrCMs85^+C_0{R@g(?0=6dAR{}O_bZ>Yp1(>~jaIwg z@YIuHrvbWv(mNpdzfkPuzAg(32@S7xtuI7OZ!_NdvG^6ORy>?|JV7o{nVJ!8(@6Kw zC`(LqGl;mOop{=Ppfn)_?LY==`?x`t{*Q zP4%`AN3ytjj2fM#i?yo)bF8Tb`QScUgG+92si_c%*3weiU zaZoa8fi!kNXF?Eqx@4~jabaxHJOZKt#>ii=xwVC7D@l!dvx1Niz*{x_1!pyCcS@$~ z#DJ+9l171i0*q2GM8m#Rfo0oQl*S86;g+n6Mfo$1@nYgbv`G4GBte#aBlpqmI`}mm z>5{6{;L=gS;m>(hQ))o=Vg8ia3WcI7+N8dn8zhwjq^`|w>jzcBXG>$M4rCkpRX_0e zs%i0RkaPxkq(cgD;?gv!Qgz0%`{UxDr+TedtU679*~N+yWE8&i4496GlRYO1Z{hhp z70hm5Z_=mU%Z?WcI5amH@EfQ!hX?{utX0Xsx-m7KF23*?SHcZc8oTr&R~UkNU$kUe zITIsruG|^oVwcRHsf&fnBm~$)?Qn1aXhB4S7~xX`OfAyDsP$r>nD~nhOBMc17;wza z*a1kAg(N;ayXiE4fC>W9tm{02UiLH>)Dj_zXm8`jr?PsQ|ZRnrRJwLEgKcHQVpml!`UR6KSfp~iX)dmiqY+_e7s5rYL-sjbgW+UzJ zI{;;dWni|`26ua4JjsVA-sEsOchC?FfcCtg_6^hza26sQTvxDH5)k;$rF*Pdh4@du%1tUONg1tSE+CyD|u z&=(rZ7DmerPj*_`sPGMU%*m7xLA;>bm(NnX8}_Tn{xetnAe}Jb!oUJ|BOF*P1E>$% z@`GzSiU5fCkv!O3?xv>{oY$XsHhAl33y#%vM#_(@FHKAL{~~zEdvqqy?`*g-K@Sg_ z+r^Xp%FlTrK9qE~6Xid)9#Jmg9nc;W^|)qVbdpO^)|xWbt?WmbH@?t~?YF;Onq31e z-aS;bievPebeT~#U49rteaTsO+pvVhT5$_?;AD*MkNE<& zc|o`-$gDqj4=sG(L$_=F6t;P>xTL@JBXK}FYT-MRtjBxOw9n6duV=?FU;b0c@K%em z*W26_2lXM}15>xjXE69&-*zBwgGS@-;5N07FIARDYHn)}+}`9XfjWCVkYgvvpcpoD2m-M95Ar3zDe!YHX*LEcq7%aY^ zK5|8sBVE%(MTUs8nBaZu>QX13h%;a{Ka02C*n-x>`xrEx_?)h+3k#__DfHrBBrKDt z_W^d@2l|TbXSO{c%#yIjqx*TD@PG^DE9t`)> z`jIa6eu&5+I207Dt(4! zrpUzW@@M`srL!zaseiE4xi~amE*uXQh^%gcR<)3M2!X}f30PC?>B%CutYdR>#~Ubq z3A%DZ^#4rrc*EQ8&7$vwP6I*Bv%Q+4IeN{EjZZY9Io0du;P$LO`6+_LHeQReWe>o-@|lr_%{IrmX#W zeJI}iAUBvBqbsRuq#_R;_TZ(Fy{=tx>|cfG1YDe&LbKM$+K{R;#5zCnds>~%8fz{E0qE6>oSdro;0hOsZ*&d_jH z)gg(b`8!o->3ORhSLx`(1*Xtp0{v3oTH_?TqB~4Yt1;Fq!6-(6d$+(+;#5H%AFtY7}4as#g8Z@oL%gUw7e!2~4nO%zKt=|NoMq-;I)J_gLKOMQXx7+x;9H zi8sr#Jdz31FC6+jC9K490k?NsWW507HS<&Jf`=0^P{ZLj4-fQKV1Ry~59?ar&3?J9 z15BC8YBg(#mb;E)?cn=+v zkj?k;TcXc0WM;M*+#JFlhCO%wcA00i{oP1zB$r70xwsFbd{hSr$+tv{=yBrVf{#qd zXyq=!89W~W$QHgm+Aa7NCs>BLmx1O5unEi3ZaAh(845*Un;gx$%wG(cnc=Y#1qgvw zF4p;BPOJsko74w{sbC~uxuLYQ<%{3)L~?x(cNtKWf9=R^d`b9UW7F)nMERBY{d&G$E{tf&a&{{fiP-(T9W0Aml)QUex~Gw^ zvVDVm$|c}L18Kfo?I;hs{PJLSoQMQF&Un|_mJQ;l(%fx3ejfQBwK#i8({S3bOU3d~ zBY1_q+b$Z8fF9SZaQxj%{7X$Ad8{Ko?c{BbKJMoxmgZfLJ4+{2tM znj7zRT11e_Z2G&Cqt~-KSJuly0VgCt8>0Xa&Fe+}K1edexO!?XG!$tUkw6|6XU%;66Ws#+QHI?ulekNF1@M0N`$Xqy@FSm&V;H;dqxS0=OE>ZsU`=oJ7rO?HSqjw;Tjsdj^p}}8xoms*jkZAyO3Glo z9N_lGeb+xJ_+fktNIe`F8<1t>ejtlXXywq=BWhjRtF2$SAWBEt&$d6op0^S(Ahv+C zjhnn1NA2wZ^^IU!*;Ak#lEVh2F4E}G3ZI!mtxo!OZ$)1;tYt59hCtoYRWOlDDAz4+ zRWXnZpEizWlNfV1uzqLhk)4kQ;;Uk7^0#)kIGzBwORa)|TIbmO04S0;FeTR>OmjYx1)QB0zvCRZWtkS%B`zPtEkU~5nqIxL3Ps76+K(MklMnN-I*|2H z)Q--&H@%}$rHllQ=pFKBgmMuG1J>`RjLBVboA6akq+fRr&nw`Ft=nu~5h35s=-Cqf z)$Z&IMUwpSV(E7nzkxLwoCJCO^?ZhSPrt2hf-7J*h_D)R?pbeidn5JmJHe|HsssrY*#g$h6*FhoA{1?+DMKbe;<(N7xS z>(892RY_lII{-O4c)#Li_L$-aozpp$fn2_d3mI3AKSzfD{S1{7PD(TrQ1l8mxfy2F zr{hRUQUE7&V#t~xGHXw`9oe=h_}=U1Hg}3+(cyEh>v=e9lK}%Kl!7(#^|ZFTl9pFj z4H4bdULf^~_P|S(P#4OY08|)2Z6CmFqkA^a(QtcV&SOqYiG77#RD!tIwbc@9Dv4Q^LI6Wi-JQf6yzOeR9=9 z2G<|XZnIE3A^ITpcg%wKadGF3H*BE%w~kVwT^z8 z5Utg77@8&K^yU6u6AD9p#`%Xy&;t)2!$Hal17*MEH9y-0uD?p;q)XLHG+1PF7>pJ- z*`V=_OT~D2+GYuBQK$o6i2I;T0*FW)e)NnYLJCx3a9{7gqI|*$^c|Ny%5Fyu>SrdEvO_+);>Sw;69re8&3G*d~%b4 z@5jWhvgnG$6=8(obkJfz>YznaRoNnan=8?c(6Po!1}bTXw8exZiE3t{3_>lk<0*p7 ztckxL_tdZ4*@sXwWw;ovu1L3x>{UJMS2Qld>FXea>UB8yG|wh{^+CJlY6r zjJmQHOjPCv&i1=+93%&0M~~c=y4*Uso?mVl1lSh|*N+Jy*xwE)iJ@(L=A3-@ck9%l zlN_LbEQZwW;nq?ONPn`sR%ud3e>^ORqV;=?E-HVIDA?i+Y^Plw_O`tI(|zW)T2c^b zK<3rH!6&U>+}&F|D{gS6Z4MUr;KoIWF(0f)4={4SqZQmlEMGM;h+AEeGCmH4jkC+? z#4g%~cK(}GgR#l;&!x@If`>SUah_u|B=fuj`Gszc76K0@ucQFyShlp+BBZHaW zDro28w|yiZ$4T<5BDww-Is`kq2%mqAq1UkDw11!0|76z{$HdY6XgkHWu_fh!u zSrul=4>MHZIB^>Rg55zO81Jf?I+1UR$rMADceW*PKEYHY=!0DpJ*vhG-R^N3W|T%& zEbIg8q3H69^f~jJNDyXTi_<0G)*iO$0=5X{lQBm`mUClOfB_`+> z2ULYYIMvRU8Ze}c_^a$edhL!DpTqD398n@_N~*n{AcE5!ta`j(3=C__`z09WQG)ZU zYSNY7IMaboDGG&pE^%|B=3l*LuMIDKyhLG(d2vdyeSW9S*+06h7Z6ma7x6*}JVX*u z{iw`Xy>9^|GsoXIh;-`}MBs_TS`Kqy@91!*^($#E#R+c8%s7Fg0PQXyTO$_>^vBYw z-phON`~Tln(YJ}v_VhLG+6ywO5xy=Ms&f#!_+dCmjnstmfv=^lTWR(}Ta0;{YXf0%lqOua#MuHxLkrM=! z4qx2S1{}+`v&h=ciTgRMD44`F5>N<`Jib$i-Y5L;mU`=hbFZq z3s++v=qbyP|Aw@g>XXbvND}hkOJ8mGb9fDw_CQxulwB$~QA2oY@E+gLfUEueB$kQ2 z)R^7NCup8hDgipie_u`k9_LlGj8&o=}72`u_v zXoc}EXC$EW!$NCzmmuWX5=x)DS}7L;=;3Wc%R1G^*VS`g_*wlgx!9s0XsuiTbHoF- z(ne_(0I%vX%sov5y)~@*)5HRY2a!Dk?yO;M-1ZR6k9*~a!f5s(R4XJHYK?YmYNyq9 zeSnpT^Vhx%1^g88M5OO{WRprB1Qv`0Tv@>e5`Im@S)m;vzx&FfKx)jt4mZ8q`zTc# zkLrVI04fLG9%M5^b{d;ASYTtzn>3o;Th}S|WiT`fI#9Qaz2~j~LI&Boum4)%-La}q z5>lqJL|#?S$*P4)U)8|Giq)Xt2w)-h-i=44I^ZX1}jhBRB#VV9a|#m6ibwp)R3vI|+j-@d+#OcR-_{6L@EE zjirKIv@#IV!j!*ZaEHU?Mpt`5TX+mxdtyB1@(k2PyK z@D@h^zNo9uMYr}AhtXV)NNul&q~$A!H4LO^WqM7nRE6-!hh+cbpWuHN`*LCzxOupM z&fKwkX24_!S9H;rZ3gvv^fMShl3SalB%bXSRz=ZTzxDJ8@a&IEp zT@(dkATSw2RFXx4Lw}}V6v;~^nBxU;pS*laxfG`>%)Qfm`vDnYL&lfg0!Mp{x$rrk zhY}(p*Y##Gi?S&sSOU&3`ZAqpXRw;dfP7lQ(uss#_ca(2MDt}K=*-l(Rs(V-WUA2R zl~;H=VXJ)Y=msI_5P|-|bD6X@vT6Hz`iV0w_5UcUFTQ@NsA0`8`FO80D1HU;Dj?^@ zC6{Vk-=E;sFn8qfl}$U-+yWRAsN6E}`&EP9*W%~(c&`!1?IJ%5^EALGlTFP0~_V5?pP1n$unxfRy98ZtA6+r z&Z#ida-3#(1+FQ88|(6x%KGvwx%G7A8vQ17r3M{8ll%cw_~&JP*?Amz>Q{cRZAFds z(%q<)>w@@w`A}}xL}a&Z0Rt)OgDt{FQ@u^-XJ$EnE^<)Cm|VwkL>cSmLv!Rp0mw_QEa@KKK^bQ` zf6GwK<<6HQ;UH#UONYUjJ@e763D_+4u$Qv;ClcG5p;gx2Mg=Cr03osSV%Q*VKH(Q+ z7eSqVsij|ORo?qMojayHWfSN=j3Fq%Z+ND92YwG^?+oLrIohi4YHrbF`{bEV;KfvD z;V51+@W{_hyMFqDd_n#nZO_@ z+8<4loX6}O^)9s5!h~~kC~Y7i5Mtmr)=$6`VGt_uWUeXH#~ESse!453V&B5iGikP{ z12ItD#~-GDiBYO6K&*(^i0D<0Wct+ynZ36znu)g9Y?>mN!9FT%MK994U*csp_pWFL zXbqLSD!iy6Rwbvi>Yk#>PvtCmeUD~$5ma^f65xT)+*`r_vpfzW3D8wVs4}Y)685gU@=I7(<_*ofhu7Xb0EMl#g$Q`-2_Yw!ceVzGu#=-#5mC1Q z3*)XuBEmk)fmkV#ywNF1K(KS|Vn`v{(jA*%VI_Ba6zfQN8tMz+2d~d{o2e0FXs8a! zqo_h74!(i?V0KLSD1lg_S7Jl=^*-wpUHHXMzG@kLYO*Y0hwqPr%8*=q5g|(m=3pJ< zwxLL$Zf_$0zh8B9wNI<-Q9hoR0T%KS-b3w2Jx*r#tQ3jm7wolFdr15%-1ZzKPm%Lv z#tvtUQZEn0+b=U*}+F)iju&a5vn*m5`VgZeFK<_KH%0!^9G-pb4I_|i09 zo00c=B1_w3;&FX#`q8siC+8wg&J=E%Skl~`%B&7jD{>gU_HB6A?aS6)Ki~Zt(9?*?-(T_t;g4k(=M%C~l3Tk>k9>ht3NRf8SL;C|ot@~-xFMAbJazxG*aQdl4UX1*A&}GqDq;14=_&iE zKYp;-VwN@g6!@t`D3Jpr=Gfe(4BUuKdM2P_G+dvArXz@m=#gBup_9>r+9ywQJGu7< z8AMrf^?8@5JNOkZzTnA0mDk%xV}EG_nSp#do}7Z>W`A-C(p=2UDZE~tdrvj1<0rC6 z#mtL|O$ocyv!HPy)%h;yeP!-)i7{z*)syzohmc?R*h-5uFFCi!?f8mpU6=ACO1=*j zZ)_-mw7H6t5bhGow3a+&+}iM!`v1${V*ceSHbYUD2J3=+-`cd4f?Bgp1wNmX>{w!6 z$H|92-B|#)@-f4N*bqxlcta3}2<&$A-l}9PxlI6$N4aD#%E3c22QoaPQL?z3O23wt zx9z3ZO|q3-Z<#gxux4()EHHlWnp5KZ9Th*_{b}k&LV5czo#bH*Ty^dNb57ola`W0E zWd(fj2O0|?LbDE79!yI?q#=dO>U)SWEQ45#>)!kOil_Mu@+CvK6(kpJ*7G?M7_~qM z14?L88H36714L>^CU$NQ4!>dJMVWN%7+1kD*(P1GqWY>0`!@cnRvAo-Sah-@mzNVR zUz0!VtFijwv&FAlnZ3F#?NDf6fY&r0ehKiDTu` z3c>RB^kDT$@GDQAh@iQY2@~1a-iKzqhE>|JC%u827S_y>th1YCe0&0O?*kL91<=^g z^i+?q4kH}{4+5>eI4~5{p4)?{baSWT3eH(FlJ=1(?e=)3x~jm`*_nF^ck0 zvcHs)g2#Y5t{m>y7V3ZqKjs2fOA;$BMos@6k+&khbr)ttE2IF?__KpW@vEp&1)`7;_mea7QQ3Q%rT5FYsQvs4i*h=&UaAYkT} zC)}9J7g~Qm2zo)VoUv;Qz9`J(RN;!JKJtV&9_#-_LCcJ2&jN%gt1BOV2_9!l92s=8 zz$&6;%5?PqU&*`8S{7qTi{LLY&mTnygEVwMm2!MwG3x6P0E4kO&h1VyXjzj

    c_W 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

    v=tqPO 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

    P(Hbt$RTCQE6L3-ep5^``bloAE<@){&)q|E_ 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/m+UyBF;N*c%(F%>?b*MqE-M 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 UXOu{pr~m8!1GOok&>Ec$0BA;FJOBUy diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub deleted file mode 100644 index 13e491cf7875bbf3d9bdc04c660c7870462372d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 229261 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� 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 diff --git a/Barotrauma/BarotraumaShared/Submarines/Herja.sub b/Barotrauma/BarotraumaShared/Submarines/Herja.sub deleted file mode 100644 index 246bbcdcdceb26a276cba5688cf829f953126cb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245733 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≠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

    UlVT@G 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

      -tJKYeP`3<0}NM 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
      iw%-L;O5i`vfr{{bSy%&)P@QXBTzHys? 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%0DX zp5`94W*qj*;tDdsTp%YM@DcAa8Mw`orhHW))n~>R?X^v*az|^q_4Y z3g`;m<;4UWuZN56pCEqEyy3#F8GC`Z4^C$a5X92D_6*dE*@P7mS9T(!sM#{jf(S+F zpQrmcy4Ev0OehA>sJTe{qy*dlf)|}n2(UXn+-hwh-Ldn<1D{jSokZmUhdT_sF563lFC7>P%AByxQAj$@dt#FTT(n+WKvT#ar&2jV&Jg8ZIeFDRE=kO5{ zG_imgcLMJR-v(=#DG%jkf5^-Fz~?L)y3}t0Qw!jHsKH8c`(tH3 zhLsPymr1isP&9ixF%2=IHG^`UgEfue%5bfzGJ1b@j>@ykDt#?PiaeA!QW)r*7f|EV zg9n=g)iH_*#DcqjZ|v}inwFRCDltl?WftQGM579z4Cv+5w3&R(xZLu`VCuGTCo0Fs zCQf_BFuEb4v}KP=fL*7E0F={#Pz|Yt1@F4H0Y|MxO6GeTbqdTBYx))LkA8?085u%3 zlkHf5a47lFNZP~DL=91I;@@{G(_AT2j=LG0Q3dCcUzEb#l`{cDZP0fg%oR1Qms4nt z*uXOPx0&C396PN^$h%hNvi|na+~(K@+#A6c&?9@{ZsB_IhKU~qFanR@V|o69ty#am z2?1mlptlwn1rIBjWB!)GNk2N0Hz}$+EN%3#V4$Ou4?59NaV2UX9~9TUrbc1(T5;fe zLkcXv>IWBQAk4DBbW(fUz7`6Y6kmqujNPDetKER*5kTw0MJl+#ZDaSeFRA@V+YD5) zBrYw@MbH{nVDq-Vpg>(ws{=m^>Hh98(3gbzPE0USRYJP?vm+aL8y$AQ@h>Z+s#`+0 zI(HLq6W&OI3*Jd~*eh}Acj3RN=VFbs0fWN^DXAeM=JTm~KL}Z!S5XoOw&9$H2*M~f zakge}akax?G)~y1{6IZm=<-~R126m*rAd5wB{5G}bKq;?=XTUDn>}sBu$6DSc{!O^DT^M!Z1l!>C1^(MVw&T?5Ss%Y2gpHTp zro{5Q7kOYdtoTYx@;UxSCjw+9lj@e@m{ zkut{uBEj)~^HmBlQUSW<9%zi%2;@*?GfFKFE1Rnns28Z_@!ip$bBH~sxw~UmTx)fv52FBd z@Q*0#RhKP{L3;f(lLCh`z=8}<7z%=k`rd43Pyt7_fw z_pA<*hR4Z{@A_QzH%|))0yepF8@{m-jDR^Z#cLYl=~HPdmFp=uMN;%Y_f>qvroP&b zEP$NRSQ7Dn6(!(5I@-4wU9XR5%Gf*ExHW6G&u*d+RA@IlWGcZL7~?T z2_qY7ZQ7<2aW6CM>bd8`!tE7s&;SFY2Mtid#Ll@A@vOAlzQ4u=94^zdtVi)Ra6Cb0 zJSPO)t(@6DK4I>ySQ3q$x1Gg?P=iJOeZHGzV(t4pY{rd#9=^%SuDW7a5UV#kh(Yi7 zF2m>s^iUJs<_Uy*vla&SzUx${y-L7Q)Ulb9=x5H0RWqdW{oZ*1mg|@J zt&~68ah3stn88z63=L|J&CLR=HeBcxggs~k;$l)w%NojG0$HM)fH65C;NZA8&?&BA zSa+_Y2&?=n7U35CH@lC|0gZEbG7oBeMWa*V#lgbxW9;Fx)|D zGI(~9jM{+c6p^$MU{TABis#~1;OTc~3(fB3Y#ps)N9QOm5ljy4vPQAlK_&vgrQ6YmsFK zucYH0D`J*zG0blPB2()N>cT*Gn`UG@GQ}YcRM{wwr&Po9n%GpYMIF*?z}X`o(?xfm zy`=zr`dbG#G=U-+f0*AHp9eTRXER%~-x3I)neKNe1o=7Y#RsNPoTr(-nm zFi0u|X9!?$Uj`_D#yF23ztmj%E5B4Cn&YkwMr3y=!16DS7A=7hHo2FHs`-N$r4`-i zTL4MM)W^_gurx5o0@%|l4#GMvK;@uS!Y$*JdQcZEWhglYuN|M6awNZv^^+ox)5}ev zVWhHWKd(Ro9p-dcGDNH~>?(rD34@he$?T+=reoe{U@*m?05?F$zbME}=Q)6beI1{k zwCjO~n&!T!pvDb0#&d_ZzO+z-+z>1Nl>-S13Ow2Au4VPc>RbN;9#@?Bz-B!nn>?UB z(fywD=NKJ&&%l_edrsC769WreE+2ACUf|W|b$Dr1W7DRdhZ?)?C(33xrC4HHn71{a5wxcA)JGVqh`%L!C<49 z{kB~AT?qF18T!gE_BXq@dO=;#&O!vP%kkG=%-8l?pO-G2!$MeppF>ZVn+JmO_tK~H zlRR~Hay3m(247re7`pnyiMmbE6rLC0ICrwr3Wz$2Cnl1<6cj>_&LONSpOpx-9S-;@ zthNp+yl3(Nbj69JS?(!3E$sJj&wmhP))P}xKZ5`jaPl@J#V7+KOIB7#G7Bs#RicTj zfK;9<*S?f5yF|lf9M6^LzPjJUtm|=s{Qi4|2AOF?OA9zSvV2gW;>lMuI8ZvLpxHE! z=0{hc6rJt@F--B?gZcdFN7%QHohinoQ8xMohYrAX9#myLX?^5tY}rT_u8fZl;2{@d zC8_fwa9v}nKuPJESi)E7rG>XxuN7w?TD>?gs_?UekF(>&O8b@0q%Q)ZMZ=^P|3FUQ zT;QMcF<{xCt7}F$nY4tU@MKkb z!O)kvv4)P2HQTPg-Acm0tDO#vx4lgD-PIiy-@6ai>e%}>lN%L5R{iy=?Z?W zZi`W>i?_*5z+iv-RO$yPpw$O#9ff!o)QaNFji{ zEz>Olr?_LKC*SQpp@9;dwYK5^puC9%``<^t>&^<vH`YHJWI1lJnEk`po&D;z z8pY~Zr6=H2T%k9GB9Wd0gvFoi0D17EhwM~bsEo;7zaxc zr<*AJ?q*o41&pcbB?kjoawd6OblQSc01iHeWmL$AO~vwbX~!mcFDh(jlb2spapl{6 zn;+8YUEELn6VRRJ53UITi5&flAT?fe9)q{8)hSwJ*S{G>o1k3eGS;{iCzSXnuHby1 zIKr76@;_)&zerW%m)AAiQ+sBgZ0&u>TH}IxXN}j~z2PewD_^rX~z}BbIqO z8{H3eCFHV$+)662*+zKyqj{9vN1APJYYpT^H2Dt1;bmdFVlkiO_C#iisNbpx@#MER zxJQ_Yt5`{<+Fj($O-3EC0lWOtzLl!Z8EX#tl?z{r_ipA0K;fMHpiv>|WZELt5aK{K zghX1>j{#1lXYIQm8lMhNy^e7E{X>rIC(kKo*Aou&j3j|x3 zph6PI&8u?1AxWwU1+&4yQA#Y6wF957hv)1lr~c@ns6T`27WX!Dq8Sg$ciV@^2@MW7 zJ~a_k_2(60{3__RuqAT=wFED`REVv8$*C`3BVX1gISKzI3ZsSr3W4B#17#HiXk z(AH9_w&~9q9nKq&sJQ)9_C}GgLNesr0k9urvX{;b(lgi(*USY)a;nUAH21VEt0xX< z@jI*JyG-8W;h37bmPy>@vh=`w^bWojxZln%V$_lb?kYJJ>$<4vE@V zsVi)z3xGHc%dnlDk3FiH5Kx28w)=!b=6E~G`L&{18G^>ZDdZys97 z@8|CZ>tAXADKN+Z8j5L;bIgAbms&+5a-Ie3?8*KD1O!*F!aKm=ive~}x)~sKJdnel z>!V*>>+estrOqqEtnd8BdXJoZ^|6>QI1K`b;g&Q@iC*gn$lIVFz}K2=8Kth3Z;kAwVr}&>KKZQxAs;;aTIJL#_E{^?7Xkv3 zk-SK#SZgAoKzNW_iQEV9O{Ibegz4gT-+I`%w?|Y_`@zZ!J zKvB-G2HUd?JdN%XFpRy3d58PX9hVOt^lLP}^$#}AtK1fX^LoN{D(d-LwbzRW=vOC{ z@08F8B!8l+_<^UxI7$m0yg@^Nn5JVx_-g!q$m{(IXMq{MT_8)SU2s2wA{wg;1|KyA z@MR)pErH7W5OgtFipf%kTYaEkVSr5AR)K4=um8%`f*W3=%nNu z*+K6tt0wxi63#(MwT;tvX<`qLz#%7DKOqE6o9Oq@Ctnh_2P;<}R9lOx~3r>ti~<+}Uy+l7lR^THLtuu|dssE^FzhmQD6 zrxNW0BL`@dfFZTXtF_|_9+Nf9Ptys?;2)gZxN3@(@|l+}lIr(7Mh^c$4;vX-E_~zJ zDL;6^A_)L}E;#a@?o&}x{_5BqLr(~D`#t#hF#K0cw2ER!2EHH(z!HGVy*W@==IkVX z-C?3oPbs#tZ)eN0M))Mi+>~*BQR}IbM;1q~l*W||wuRp)ykOva`V1ocm2}2r18X5i z-$Y_hfkrGmW&p=-tvHTXeVPVu!<1j3Pu~C)mx67`-#Tnvx04|6D+r=y1izb_ZzmVa zY$phM_Y(nmWoe%eQZ)$gkTeIV?}VhLOP`mZYZjvcUE=y}enGg-?xxk*2eqi;?G42( zv;f5BGbPC(+7e`>D^r7fUUVQoC$%G-OOv7PNrO%iCI+BXMF zt6**erDvEoF-g2=W~E&DItRDeIbZY#0wZJvKBsz7qx`+bYu$}j z#&u>H8ejWf8=!2mYK>odxhYb=-bN+J_0)5C8#8p7`WkiZGvn)qr)&00ab5AECN*3^xZJ*8h!7f^jBc2I zDPJM}WtBy}_^D$ehL-_4v0e7B^40mPFyz$V&z4wAQNf~RJhqmpa0-;m#js*DxY8=N zKa9e?IcQ!Vdk6h&E85gW11G&zH3B0{On%<3JxaTXOoOi<+~Bp+#3{)Qc0K^{zQ|}N zjW`PD;>Js;xP(bGj=?)CtVkDctX&b)!&<(@WP*_s`K5~jIK8g}P$=`A<=aPLAVYPW z2fGe20ja@u@4WaV~$Kq5UvF z0U8zt8u=P}gA^%2nwk2fBktmWs2;k<$5(Ml*kh1CwHbF3O#lOE2Rd3KH3jt1>L57a zfB7M&YEZ(*?z?b8y>42IS#l%!Dr7&CnM-T-SCI|HPlT$YM4ZzBr?+|iQbt6O@-6)> z;aUvB@aDsL29HmngqV#3^rzWU02FHQy^^aUFunoiRa_9up*4EzU_K5fQ%v9I z1hn&1u*oYFqR-ExwGHp+Wnm8Q^~K>uW@lFUw!DR7x=5bVLrn_6X4l1SPuYQn(WYSASB(9?(bSD)_zlg zi`2@elIwnYX}`3-xn{MoUa>mb144b~F(1Q`2tH2mbX_oyPr&cTKU9Bv^&UEClo>dO zr(acm51%Ef)`jM;5Tuu*pn?Q;g@&7Ix^WDGn{a_HuW(t8LsFtVh~qR+MlKuc#(`7~&Qa)jIK=Pr)=6=qWy z@G+O{>kFBXG*?k(jdk>1AIlsXfDjeZ`}@veag!?6^TqN5fH2>4B>_+|LRn8Q5CM#+ z%6w!n46ni@;~wL8BL>+g=nk8|?Cz&NI}^>0fp@F{kT$W1D#BZR9Q-U&|C6{UNG)r}SLjOA{A77@Uv49$?XaD39L{ka}& z9Gh-C)VmJr^ZpVtPo1y7>u$4Gn9>|w@b@Xu!W`ZRB1d&C+UoyqP=uO%Vre1s_r z&x~LyEY?f5I*SY55v)(_{@sbDFt*$8_zEf}o!sSo2d1%*S)mz&rSZk=%=a$0Pom9V z!CND_q-wXm3&IlTc>cK{v;EC-#p|CdT(-dYX1gRR9uHcQmK_ysAKtgXM$(N&8#jDT z7I*+5yH_*LA5~D0Op7=H=rO+Q5E{0f$xV6Puj$+(F!8p}M(hI*--b{I)y4^=YhW97 z!Qy_Q7Ei<2ffvb=i3P%WwbAc*xd@v%B*;2bZ!jM2l+@XI9Ak+{tQJLLuvT_N;K=tzV00RYi`KLVuhg0yziO7|P zxvbe#`cqmbwH-&aYY6C>&~I8Volzb=kQOPZ(N#{eO|Mmvlw#^TFx!_UM_VdH_%4nL zo((&s0@5mRA&Z4EJ$tP(e?8nUGOlS1RY!gqpAt8}%krLtw>|v;L#j-KclM#&RI??d z@#LT;Odlx7%QV};XRa|b^>!~4X|_$d*b0vLdlN94|6Q33f^}rXe!K9b-w&t?eifd0=7#`#QKe1GeD$m|8uVbmJ6Ryn z^h=6y))zQl7+~c3+6m%r`t1nAZNf{ynp9&WuLl(<81TYi3*gZ4i52AOOZ`ma<^rG% z>^el!pUD=clL#@{Zo%B0ov+^-ckjfXw4^eVwZdHhUrkCt=8bV`8S(`DdpAM=+5@Bz z@u&%~U^#SM%~#UqIFk7sXJmUq<9xEDc`i}{csXE0$+%rqph=RU^KF_$WEU{&UziHk z1LQC@+u@ur%H+=wsq5?YO6qS)L*$0D9oDh8COhvEpZBVpF!ZSwuP_rBm>S|&J0+QU zYk;X;r^!VdZ(i1$^-UX8vZLY)9^DBV!{}EEEszX` z?|gX1u%D7-MRNEy9}*zK2>RZm-}^;_s`gSe1?1R<9wZ`9LYdOl;achI*zf>uc%}LF znz>{gcfZsWKK_JSjci_Iy*!#)VOalyle|toZNKjiIxCEyy>-BeDGk2^=+yth@@sj) z?XO|V#wp9q({3_Qy-iqqK9p`Ac3jv+s&t`!M^KmY6$b=>OOt+2S zyT2T8Am;Sb9S`_H-rO1i1Iz83F@g5zl^lWF^(Ku(ktd9qG+uZ4GAOqq9DKQ*N#(+i zJx!@80gm)jOIibm--=f?>D{P?|1|+^fQ#(1+KF&=B9YE`D}==>wuVI%0Da&{X(f14 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

      j~V%RI6 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�+Ba~#B0JT= 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�>U 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 diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub deleted file mode 100644 index 0696aa7ab1a6defd102005da0421b4e73ea273eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 227231 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

      EX%rR#h-`8O65M$^3)vfO1qZjs~o^v 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ⅅ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
    1. =A}kMTPy`O9=CB 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ⅆH0-L=*f!!!7h% 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 I0UhN30G?;$DF6Tf diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub deleted file mode 100644 index 0fed8bcee19a2debb87857d1700fce7ad7ff3572..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 281036 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

      F_B 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

      aB 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 z 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%0DTjuy{+{jB)$(V@ckR;DVI#h~SW4 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%0DJrC6VgC_(IYer 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 p{o_+ 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

    4w!JpM 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 X4m*cTl=SiV0ZEx*diy;K73}{2_XpL6 diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub deleted file mode 100644 index 86acde003f37cf909edb1ae23faf5d6a7fcbc950..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 210282 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 BkSPEF diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub deleted file mode 100644 index db4d8efad28d0d87706c014a72c2a34d907aa015..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270838 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

public Editor RemoveKeyValueTags(string key) { - if (KeyValueTagsToRemove == null) - KeyValueTagsToRemove = new HashSet(); + if (keyValueTagsToRemove == null) + keyValueTagsToRemove = new HashSet(); - KeyValueTagsToRemove.Add(key); + keyValueTagsToRemove.Add(key); return this; } @@ -207,7 +201,7 @@ namespace Steamworks.Ugc if ( Language != null ) SteamUGC.Internal.SetItemUpdateLanguage( handle, Language ); if ( ContentFolder != null ) SteamUGC.Internal.SetItemContent( handle, ContentFolder.FullName ); if ( PreviewFile != null ) SteamUGC.Internal.SetItemPreview( handle, PreviewFile ); - if ( Visibility.HasValue ) SteamUGC.Internal.SetItemVisibility( handle, Visibility.Value ); + if ( Visibility.HasValue ) SteamUGC.Internal.SetItemVisibility( handle, (RemoteStoragePublishedFileVisibility)Visibility.Value ); if ( Tags != null && Tags.Count > 0 ) { using ( var a = SteamParamStringArray.From( Tags.ToArray() ) ) @@ -217,15 +211,15 @@ namespace Steamworks.Ugc } } - if ( KeyValueTagsToRemove != null) + if ( keyValueTagsToRemove != null) { - foreach ( var key in KeyValueTagsToRemove ) + foreach ( var key in keyValueTagsToRemove ) SteamUGC.Internal.RemoveItemKeyValueTags( handle, key ); } - if ( KeyValueTags != null ) + if ( keyValueTags != null ) { - foreach ( var keyWithValues in KeyValueTags ) + foreach ( var keyWithValues in keyValueTags ) { var key = keyWithValues.Key; foreach ( var value in keyWithValues.Value ) diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs index 894f641f2..742cfbf83 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcItem.cs @@ -9,6 +9,14 @@ using QueryType = Steamworks.Ugc.Query; namespace Steamworks.Ugc { + public enum Visibility : int + { + Public = 0, + FriendsOnly = 1, + Private = 2, + Unlisted = 3, + } + public struct Item { internal SteamUGCDetails_t details; @@ -74,20 +82,20 @@ namespace Steamworks.Ugc ///
public DateTime Updated => Epoch.ToDateTime( details.TimeUpdated ); - /// - /// True if this is publically visible - /// - public bool IsPublic => details.Visibility == RemoteStoragePublishedFileVisibility.Public; + public DateTime LatestUpdateTime + { + get + { + var created = Created; + var updated = Updated; + return created > updated ? created : updated; + } + } /// - /// True if this item is only visible by friends of the creator + /// The item's visibility, i.e. public, friends-only, unlisted or private /// - public bool IsFriendsOnly => details.Visibility == RemoteStoragePublishedFileVisibility.FriendsOnly; - - /// - /// True if this is only visible to the creator - /// - public bool IsPrivate => details.Visibility == RemoteStoragePublishedFileVisibility.Private; + public Visibility Visibility => (Visibility)details.Visibility; /// /// True if this item has been banned @@ -122,22 +130,11 @@ namespace Steamworks.Ugc ulong size = 0; uint ts = 0; - if ( !SteamUGC.Internal.GetItemInstallInfo( Id, ref size, out var strVal, ref ts ) ) - return null; - - return strVal; + if (SteamUGC.Internal.GetItemInstallInfo(Id, ref size, out var strVal, ref ts)) { return strVal; } + return null; } } - /// - /// Start downloading this item. - /// If this returns false the item isn't getting downloaded. - /// - public bool Download( Action onInstalled = null, bool highPriority = false ) - { - return SteamUGC.Download( Id, onInstalled, highPriority ); - } - /// /// If we're downloading, how big the total download is /// @@ -146,7 +143,7 @@ namespace Steamworks.Ugc get { if ( !NeedsUpdate ) - return SizeBytes; + return InstalledSize; ulong downloaded = 0; ulong total = 0; @@ -165,7 +162,7 @@ namespace Steamworks.Ugc get { if ( !NeedsUpdate ) - return SizeBytes; + return InstalledSize; ulong downloaded = 0; ulong total = 0; @@ -179,7 +176,7 @@ namespace Steamworks.Ugc /// /// If we're installed, how big is the install /// - public long SizeBytes + public long InstalledSize { get { @@ -195,6 +192,13 @@ namespace Steamworks.Ugc } } + /// + /// File size as returned by Steamworks, + /// no download/install required + /// + public long SizeOfFileInBytes + => details.FileSize; + /// /// If we're downloading our current progress as a delta betwen 0-1 /// @@ -204,17 +208,19 @@ namespace Steamworks.Ugc { //changed from NeedsUpdate as it's false when validating and redownloading ugc //possibly similar properties should also be changed - if ( !IsDownloading ) return 1; - ulong downloaded = 0; ulong total = 0; - if ( SteamUGC.Internal.GetItemDownloadInfo( Id, ref downloaded, ref total ) && total > 0 ) + if (SteamUGC.Internal.GetItemDownloadInfo(Id, ref downloaded, ref total) && total > 0) + { return (float)((double)downloaded / (double)total); + } - if ( NeedsUpdate || !IsInstalled || IsDownloading ) + if (NeedsUpdate || !IsInstalled || IsDownloading) + { return 0; + } - return 1; + return IsDownloadPending || IsDownloading ? 0 : 1; } } diff --git a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs index f59d22ee3..b8bc42740 100644 --- a/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/UgcQuery.cs @@ -15,7 +15,6 @@ namespace Steamworks.Ugc AppId consumerApp; AppId creatorApp; string searchText; - bool returnLongDescription; public Query( UgcType type ) : this() { @@ -106,18 +105,6 @@ namespace Steamworks.Ugc } #endregion - public Query WithLongDescription() - { - returnLongDescription = true; - return this; - } - - public Query WithSummaryDescription() - { - returnLongDescription = false; - return this; - } - public async Task GetPageAsync( int page ) { if ( page <= 0 ) throw new System.Exception( "page should be > 0" ); @@ -255,8 +242,6 @@ namespace Steamworks.Ugc { SteamUGC.Internal.SetSearchText( handle, searchText ); } - - SteamUGC.Internal.SetReturnLongDescription( handle, returnLongDescription ); } #endregion diff --git a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs index 03eb9153c..968e9d0ea 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs @@ -2,6 +2,8 @@ using System; using System.Runtime.InteropServices; using System.Text; using System.Collections.Generic; +using System.Threading; +using System.Collections.Concurrent; namespace Steamworks { @@ -9,33 +11,40 @@ namespace Steamworks { public const int MemoryBufferSize = 1024 * 32; - private static IntPtr[] MemoryPool = new IntPtr[] - { - Marshal.AllocHGlobal( MemoryBufferSize ), - Marshal.AllocHGlobal( MemoryBufferSize ), - Marshal.AllocHGlobal( MemoryBufferSize ), - Marshal.AllocHGlobal( MemoryBufferSize ) - }; + internal struct Memory : IDisposable + { + private const int MaxBagSize = 4; + private static readonly ConcurrentBag BufferBag = new ConcurrentBag(); - private static int MemoryPoolIndex; + public IntPtr Ptr { get; private set; } - public static unsafe IntPtr TakeMemory() - { - lock ( MemoryPool ) + public static implicit operator IntPtr(in Memory m) => m.Ptr; + + internal unsafe Memory(int sz) { - MemoryPoolIndex++; - - if ( MemoryPoolIndex >= MemoryPool.Length ) - MemoryPoolIndex = 0; - - var take = MemoryPool[MemoryPoolIndex]; - - ((byte*)take)[0] = 0; - - return take; + Ptr = BufferBag.TryTake(out IntPtr ptr) ? ptr : Marshal.AllocHGlobal(sz); + ((byte*)Ptr)[0] = 0; } - } + public void Dispose() + { + if (Ptr == IntPtr.Zero) { return; } + if (BufferBag.Count < MaxBagSize) + { + BufferBag.Add(Ptr); + } + else + { + Marshal.FreeHGlobal(Ptr); + } + Ptr = IntPtr.Zero; + } + } + + public static Memory TakeMemory() + { + return new Memory(MemoryBufferSize); + } private static byte[][] BufferPool = new byte[4][]; private static int BufferPoolIndex; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs index 6c08c2979..f49bdecfd 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsResource.cs @@ -2,34 +2,34 @@ // /* // Microsoft Public License (Ms-PL) // MonoGame - Copyright © 2009 The MonoGame Team -// +// // All rights reserved. -// +// // This license governs use of the accompanying software. If you use the software, you accept this license. If you do not // accept the license, do not use the software. -// +// // 1. Definitions -// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under // U.S. copyright law. -// +// // A "contribution" is the original software, or any additions or changes to the software. // A "contributor" is any person that distributes its contribution under this license. // "Licensed patents" are a contributor's patent claims that read directly on its contribution. -// +// // 2. Grant of Rights -// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, // each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. -// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, // each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. -// +// // 3. Conditions and Limitations // (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. -// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, // your patent license from such contributor to the software ends automatically. -// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution // notices that are present in the software. -// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including -// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including +// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object // code form, you may only do so under a license that complies with this license. // (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees // or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent @@ -56,7 +56,7 @@ namespace Microsoft.Xna.Framework.Graphics internal GraphicsResource() { - + } ~GraphicsResource() @@ -66,7 +66,7 @@ namespace Microsoft.Xna.Framework.Graphics } /// - /// Called before the device is reset. Allows graphics resources to + /// Called before the device is reset. Allows graphics resources to /// invalidate their state so they can be recreated after the device reset. /// Warning: This may be called after a call to Dispose() up until /// the resource is garbage collected. @@ -117,7 +117,7 @@ namespace Microsoft.Xna.Framework.Graphics } public event EventHandler Disposing; - + public GraphicsDevice GraphicsDevice { get @@ -146,7 +146,7 @@ namespace Microsoft.Xna.Framework.Graphics graphicsDevice.AddResourceReference(_selfReference); } } - + public bool IsDisposed { get @@ -154,9 +154,9 @@ namespace Microsoft.Xna.Framework.Graphics return disposed; } } - + public string Name { get; set; } - + public Object Tag { get; set; } public override string ToString() diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index d6560df72..441214b6e 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -237,9 +237,17 @@ namespace Microsoft.Xna.Framework.Graphics void CheckValid(Texture2D texture) { if (texture == null) + { throw new ArgumentNullException("texture"); + } + if (texture.IsDisposed) + { + throw new InvalidOperationException($"Texture is disposed"); + } if (!_beginCalled) + { throw new InvalidOperationException("Draw was called, but Begin has not yet been called. Begin must be called successfully before you can call Draw."); + } } void CheckValid(SpriteFont spriteFont, string text) @@ -315,7 +323,7 @@ namespace Microsoft.Xna.Framework.Graphics } } - public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth) + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth, int? count = null) { CheckValid(texture); @@ -338,7 +346,7 @@ namespace Microsoft.Xna.Framework.Graphics break; } - int iters = vertices.Length / 4; + int iters = count ?? (vertices.Length / 4); for (int i=0;i 4, + Keys.B => 5, + Keys.C => 6, + Keys.D => 7, + Keys.E => 8, + Keys.F => 9, + Keys.G => 10, + Keys.H => 11, + Keys.I => 12, + Keys.J => 13, + Keys.K => 14, + Keys.L => 15, + Keys.M => 16, + Keys.N => 17, + Keys.O => 18, + Keys.P => 19, + Keys.Q => 20, + Keys.R => 21, + Keys.S => 22, + Keys.T => 23, + Keys.U => 24, + Keys.V => 25, + Keys.W => 26, + Keys.X => 27, + Keys.Y => 28, + Keys.Z => 29, + _ => -1 + }; + if (scancode < 0) { return qwertyKey; } + return KeyboardUtil.ToXna(Sdl.Keyboard.GetKeyFromScancode(scancode)); + } } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 0da7ec4c5..5ee24f7f0 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -837,6 +837,10 @@ internal static class Sdl [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate Keymod d_sdl_getmodstate(); public static d_sdl_getmodstate GetModState = FuncLoader.LoadFunction(NativeLibrary, "SDL_GetModState"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int d_sdl_getkeyfromscancode(int scancode); + public static d_sdl_getkeyfromscancode GetKeyFromScancode = FuncLoader.LoadFunction(NativeLibrary, "SDL_GetKeyFromScancode"); } public static class Joystick diff --git a/Libraries/XNATypes/Point.cs b/Libraries/XNATypes/Point.cs index 671017b63..314e769ce 100644 --- a/Libraries/XNATypes/Point.cs +++ b/Libraries/XNATypes/Point.cs @@ -153,6 +153,7 @@ namespace Microsoft.Xna.Framework return !a.Equals(b); } + public static implicit operator Point((int X, int Y) tuple) => new Point(tuple.X, tuple.Y); #endregion #region Public methods diff --git a/Libraries/XNATypes/Vector2.cs b/Libraries/XNATypes/Vector2.cs index cc7c02161..74b600963 100644 --- a/Libraries/XNATypes/Vector2.cs +++ b/Libraries/XNATypes/Vector2.cs @@ -247,6 +247,7 @@ namespace Microsoft.Xna.Framework return value1.X != value2.X || value1.Y != value2.Y; } + public static implicit operator Vector2((float X, float Y) tuple) => new Vector2(tuple.X, tuple.Y); #endregion #region Public Methods diff --git a/Libraries/XNATypes/Vector3.cs b/Libraries/XNATypes/Vector3.cs index 5137ef7d2..5d35750ff 100644 --- a/Libraries/XNATypes/Vector3.cs +++ b/Libraries/XNATypes/Vector3.cs @@ -1368,6 +1368,7 @@ namespace Microsoft.Xna.Framework return value1; } + public static implicit operator Vector3((float X, float Y, float Z) tuple) => new Vector3(tuple.X, tuple.Y, tuple.Z); #endregion } } diff --git a/Libraries/XNATypes/Vector4.cs b/Libraries/XNATypes/Vector4.cs index 9fcc2f72a..f05c39b02 100644 --- a/Libraries/XNATypes/Vector4.cs +++ b/Libraries/XNATypes/Vector4.cs @@ -1292,6 +1292,7 @@ namespace Microsoft.Xna.Framework return value1; } + public static implicit operator Vector4((float X, float Y, float Z, float W) tuple) => new Vector4(tuple.X, tuple.Y, tuple.Z, tuple.W); #endregion } } From 6d410cc1b79c8ddf886d1d0919a39524629243d6 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Thu, 17 Mar 2022 01:25:04 +0900 Subject: [PATCH 2/9] Unstable 0.17.1.0 --- .../Characters/AI/Wreck/WreckAI.cs | 2 +- .../Characters/CharacterNetworking.cs | 750 +++++++++--------- .../Characters/Health/AfflictionHusk.cs | 37 - .../Characters/Health/CharacterHealth.cs | 8 +- .../ClientSource/Characters/Limb.cs | 2 +- .../ClientSource/DebugConsole.cs | 144 ++-- .../ClientSource/GUI/ChatBox.cs | 2 +- .../ClientSource/GUI/CrewManagement.cs | 6 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 19 +- .../ClientSource/GUI/GUIColorPicker.cs | 11 +- .../ClientSource/GUI/GUIDropDown.cs | 7 +- .../ClientSource/GUI/GUILayoutGroup.cs | 4 + .../ClientSource/GUI/GUINumberInput.cs | 9 +- .../ClientSource/GUI/GUIScrollBar.cs | 1 + .../ClientSource/GUI/MedicalClinicUI.cs | 6 +- .../ClientSource/GUI/Store.cs | 59 +- .../ClientSource/GUI/SubmarineSelection.cs | 4 +- .../ClientSource/GUI/TabMenu.cs | 347 +++++++- .../ClientSource/GUI/UpgradeStore.cs | 65 +- .../ClientSource/GameSession/CargoManager.cs | 2 +- .../ClientSource/GameSession/CrewManager.cs | 148 ++-- .../{GameModes => }/Data/CampaignMetadata.cs | 0 .../ClientSource/GameSession/Data/Wallet.cs | 26 + .../GameSession/GameModes/CampaignMode.cs | 4 +- .../GameModes/MultiPlayerCampaign.cs | 61 +- .../GameModes/SinglePlayerCampaign.cs | 18 +- .../GameSession/GameModes/TestGameMode.cs | 2 +- .../ClientSource/GameSession/GameSession.cs | 2 +- .../ClientSource/GameSession/MedicalClinic.cs | 6 + .../ClientSource/GameSession/RoundSummary.cs | 21 +- .../ClientSource/Items/Components/Door.cs | 4 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Items/Components/GeneticMaterial.cs | 2 +- .../ClientSource/Items/Components/Growable.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 14 +- .../Items/Components/Holdable/IdCard.cs | 2 +- .../Items/Components/ItemComponent.cs | 10 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LevelResource.cs | 2 +- .../Items/Components/LightComponent.cs | 7 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Components/Machines/Deconstructor.cs | 4 +- .../Items/Components/Machines/Engine.cs | 6 +- .../Items/Components/Machines/Fabricator.cs | 4 +- .../Items/Components/Machines/Pump.cs | 6 +- .../Items/Components/Machines/Reactor.cs | 10 +- .../Items/Components/Machines/Sonar.cs | 12 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/Power/PowerContainer.cs | 6 +- .../Items/Components/Projectile.cs | 2 +- .../Items/Components/Repairable.cs | 4 +- .../ClientSource/Items/Components/Rope.cs | 2 +- .../ClientSource/Items/Components/Scanner.cs | 2 +- .../Items/Components/Signal/ButtonTerminal.cs | 9 +- .../Components/Signal/ConnectionPanel.cs | 4 +- .../Components/Signal/CustomInterface.cs | 11 +- .../Components/Signal/MemoryComponent.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 22 +- .../Items/Components/Signal/WifiComponent.cs | 2 +- .../Items/Components/Signal/Wire.cs | 20 +- .../Items/Components/TriggerComponent.cs | 2 +- .../ClientSource/Items/Components/Turret.cs | 6 +- .../ClientSource/Items/DockingPort.cs | 2 +- .../ClientSource/Items/Inventory.cs | 20 +- .../ClientSource/Items/Item.cs | 173 ++-- .../ClientSource/Items/ItemEventData.cs | 35 + .../BarotraumaClient/ClientSource/Map/Hull.cs | 212 +++-- .../ClientSource/Map/Levels/Level.cs | 48 +- .../Levels/LevelObjects/LevelObjectManager.cs | 2 +- .../ClientSource/Map/Levels/WaterRenderer.cs | 3 +- .../ClientSource/Map/Lights/LightManager.cs | 6 +- .../ClientSource/Map/Map/Map.cs | 17 +- .../ClientSource/Map/Structure.cs | 11 +- .../ClientSource/Map/Submarine.cs | 79 +- .../ClientSource/Networking/ChatMessage.cs | 5 +- .../Networking/ChildServerRelay.cs | 23 +- .../ClientSource/Networking/EntitySpawner.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 31 +- .../ClientEntityEventManager.cs | 26 +- .../NetEntityEvent/NetEntityEvent.cs | 11 +- .../Primitives/Peers/LidgrenClientPeer.cs | 2 +- .../Primitives/Peers/SteamP2POwnerPeer.cs | 2 +- .../ClientSource/Networking/RespawnManager.cs | 2 +- .../ClientSource/Physics/PhysicsBody.cs | 2 +- .../SinglePlayerCampaignSetupUI.cs | 11 + .../ClientSource/Screens/CampaignUI.cs | 27 +- .../CharacterEditor/CharacterEditorScreen.cs | 14 +- .../Screens/CharacterEditor/Wizard.cs | 11 +- .../ClientSource/Screens/NetLobbyScreen.cs | 5 + .../Screens/SpriteEditorScreen.cs | 81 +- .../ClientSource/Screens/SubEditorScreen.cs | 51 +- .../ClientSource/Screens/TestScreen.cs | 29 +- .../Serialization/SerializableEntityEditor.cs | 19 +- .../DeformAnimations/SpriteDeformation.cs | 18 +- .../ClientSource/Steam/ItemList.cs | 6 +- .../ClientSource/Steam/Lobby.cs | 3 +- .../ClientSource/Steam/PublishTab.cs | 88 +- .../ClientSource/Steam/UiUtil.cs | 2 +- .../ClientSource/Steam/Workshop.cs | 9 +- .../ClientSource/Steam/WorkshopMenu.cs | 40 +- .../ClientSource/Utils/ToolBox.cs | 32 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/Character.cs | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 6 +- .../Characters/CharacterNetworking.cs | 730 +++++++++-------- .../ServerSource/DebugConsole.cs | 133 +++- .../BarotraumaServer/ServerSource/GameMain.cs | 15 - .../ServerSource/GameSession/CargoManager.cs | 13 +- .../ServerSource/GameSession/Data/Wallet.cs | 43 + .../GameModes/CharacterCampaignData.cs | 45 +- .../GameModes/MultiPlayerCampaign.cs | 226 ++++-- .../ServerSource/GameSession/MedicalClinic.cs | 2 +- .../Items/Components/DockingPort.cs | 2 +- .../ServerSource/Items/Components/Door.cs | 19 +- .../Items/Components/GeneticMaterial.cs | 2 +- .../ServerSource/Items/Components/Growable.cs | 15 +- .../Items/Components/Holdable/Holdable.cs | 6 +- .../Components/Holdable/LevelResource.cs | 2 +- .../Items/Components/ItemComponent.cs | 6 +- .../Items/Components/ItemLabel.cs | 2 +- .../Items/Components/LightComponent.cs | 2 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Components/Machines/Deconstructor.cs | 4 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 40 +- .../Components/Machines/OutpostTerminal.cs | 4 +- .../Items/Components/Machines/Pump.cs | 4 +- .../Items/Components/Machines/Reactor.cs | 4 +- .../Items/Components/Machines/Steering.cs | 18 +- .../Items/Components/Power/PowerContainer.cs | 4 +- .../Items/Components/Projectile.cs | 19 +- .../Items/Components/Repairable.cs | 6 +- .../ServerSource/Items/Components/Rope.cs | 2 +- .../ServerSource/Items/Components/Scanner.cs | 2 +- .../Items/Components/Signal/ButtonTerminal.cs | 6 +- .../Components/Signal/ConnectionPanel.cs | 8 +- .../Components/Signal/CustomInterface.cs | 13 +- .../Components/Signal/MemoryComponent.cs | 2 +- .../Items/Components/Signal/Terminal.cs | 26 +- .../Items/Components/Signal/WifiComponent.cs | 2 +- .../Items/Components/Signal/Wire.cs | 22 +- .../Items/Components/TriggerComponent.cs | 2 +- .../ServerSource/Items/Inventory.cs | 16 +- .../ServerSource/Items/Item.cs | 226 +++--- .../ServerSource/Levels/Level.cs | 58 ++ .../Map/Creatures/BallastFloraBehavior.cs | 55 +- .../BarotraumaServer/ServerSource/Map/Hull.cs | 320 +++----- .../ServerSource/Map/Structure.cs | 2 +- .../ServerSource/Map/Submarine.cs | 12 +- .../ServerSource/Networking/ChatMessage.cs | 10 +- .../Networking/ChildServerRelay.cs | 11 +- .../ServerSource/Networking/EntitySpawner.cs | 59 +- .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 72 +- .../ServerEntityEventManager.cs | 27 +- .../Primitives/Peers/Server/ServerPeer.cs | 2 +- .../ServerSource/Networking/RespawnManager.cs | 10 +- .../ServerSource/Networking/Voting.cs | 2 +- .../ServerSource/Physics/PhysicsBody.cs | 2 +- .../BarotraumaServer/ServerSource/Program.cs | 55 +- .../ServerSource/Steam/SteamManager.cs | 2 +- .../ServerSource/Traitors/Traitor.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/EnemyAIController.cs | 16 +- .../Characters/AI/HumanAIController.cs | 40 +- .../Objectives/AIObjectiveChargeBatteries.cs | 1 + .../AI/Objectives/AIObjectiveCleanupItems.cs | 4 +- .../AI/Objectives/AIObjectiveCombat.cs | 2 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 1 - .../AI/Objectives/AIObjectiveGoTo.cs | 19 +- .../AI/Objectives/AIObjectiveLoadItems.cs | 1 + .../AI/Objectives/AIObjectiveManager.cs | 42 +- .../AI/Objectives/AIObjectiveOperateItem.cs | 5 + .../AI/Objectives/AIObjectivePumpWater.cs | 1 + .../AI/Objectives/AIObjectiveRepairItem.cs | 6 +- .../AI/Objectives/AIObjectiveRepairItems.cs | 1 + .../AI/Objectives/AIObjectiveRescue.cs | 21 +- .../SharedSource/Characters/AI/Order.cs | 60 +- .../Characters/AI/ShipCommandManager.cs | 7 +- .../Characters/AI/Wreck/WreckAI.cs | 2 +- .../Animation/FishAnimController.cs | 29 +- .../Animation/HumanoidAnimController.cs | 6 +- .../Characters/Animation/Ragdoll.cs | 13 +- .../SharedSource/Characters/Attack.cs | 8 + .../SharedSource/Characters/Character.cs | 122 ++- .../Characters/CharacterEventData.cs | 172 ++++ .../SharedSource/Characters/CharacterInfo.cs | 26 +- .../Health/Afflictions/AfflictionHusk.cs | 66 +- .../Characters/Health/CharacterHealth.cs | 81 +- .../SharedSource/Characters/HumanPrefab.cs | 2 +- .../SharedSource/Characters/Jobs/Job.cs | 2 +- .../SharedSource/Characters/Limb.cs | 17 +- .../Characters/Params/CharacterParams.cs | 7 +- .../Params/Ragdoll/RagdollParams.cs | 4 +- .../AbilityConditionNoCrewDied.cs | 22 +- .../ContentFile/ContentFile.cs | 8 +- .../ContentFile/GenericPrefabFile.cs | 1 - .../ContentPackage/ContentPackage.cs | 8 +- .../ContentPackageManager.cs | 4 + .../ContentManagement/ContentPath.cs | 18 +- .../SharedSource/DebugConsole.cs | 69 +- .../SharedSource/Events/ArtifactEvent.cs | 2 +- .../Events/EventActions/CheckMoneyAction.cs | 26 +- .../Events/EventActions/ConversationAction.cs | 6 +- .../Events/EventActions/MoneyAction.cs | 44 +- .../EventActions/NPCChangeTeamAction.cs | 2 +- .../Events/EventActions/TriggerAction.cs | 4 +- .../SharedSource/Events/EventManager.cs | 30 +- .../SharedSource/Events/EventSet.cs | 2 +- .../SharedSource/Events/Missions/Mission.cs | 63 +- .../GameAnalytics/GameAnalyticsManager.cs | 2 +- .../GameSession/AutoItemPlacer.cs | 2 +- .../SharedSource/GameSession/CargoManager.cs | 57 +- .../SharedSource/GameSession/CrewManager.cs | 10 + .../SharedSource/GameSession/Data/Factions.cs | 7 +- .../GameSession/Data/Reputation.cs | 2 +- .../SharedSource/GameSession/Data/Wallet.cs | 193 +++++ .../GameSession/GameModes/CampaignMode.cs | 48 +- .../GameModes/CharacterCampaignData.cs | 29 +- .../GameModes/MultiPlayerCampaign.cs | 13 +- .../SharedSource/GameSession/GameSession.cs | 57 +- .../SharedSource/GameSession/MedicalClinic.cs | 27 +- .../GameSession/UpgradeManager.cs | 15 +- .../SharedSource/Items/CharacterInventory.cs | 6 +- .../Items/Components/ElectricalDischarger.cs | 2 +- .../Components/EntitySpawnerComponent.cs | 1 - .../SharedSource/Items/Components/Growable.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 34 +- .../Items/Components/Holdable/IdCard.cs | 1 - .../Items/Components/Holdable/MeleeWeapon.cs | 9 +- .../Items/Components/Holdable/Pickable.cs | 6 +- .../Items/Components/Holdable/Throwable.cs | 6 +- .../Items/Components/ItemComponent.cs | 25 + .../Items/Components/Machines/Controller.cs | 16 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 9 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Machines/Reactor.cs | 25 +- .../Items/Components/Machines/Sonar.cs | 4 +- .../Items/Components/Machines/Steering.cs | 8 +- .../Items/Components/Power/PowerContainer.cs | 28 +- .../Items/Components/Power/PowerTransfer.cs | 45 +- .../Items/Components/Power/Powered.cs | 26 +- .../Items/Components/Projectile.cs | 29 +- .../Items/Components/Repairable.cs | 6 +- .../Components/Signal/ArithmeticComponent.cs | 8 +- .../Items/Components/Signal/ButtonTerminal.cs | 19 +- .../Components/Signal/ConnectionPanel.cs | 2 +- .../Components/Signal/CustomInterface.cs | 10 + .../Items/Components/Signal/MotionSensor.cs | 1 - .../Items/Components/Signal/NotComponent.cs | 6 +- .../Items/Components/Signal/RelayComponent.cs | 4 +- .../Items/Components/Signal/SmokeDetector.cs | 2 +- .../Components/Signal/StringComponent.cs | 5 +- .../Signal/TrigonometricFunctionComponent.cs | 5 +- .../Items/Components/Signal/Wire.cs | 14 +- .../SharedSource/Items/Components/Turret.cs | 31 +- .../SharedSource/Items/Components/Wearable.cs | 8 +- .../SharedSource/Items/Inventory.cs | 66 +- .../SharedSource/Items/Item.cs | 123 +-- .../SharedSource/Items/ItemEventData.cs | 114 +++ .../SharedSource/Items/ItemInventory.cs | 11 +- .../SharedSource/Items/ItemPrefab.cs | 4 - .../Map/Creatures/BallastFloraBehavior.cs | 32 +- .../Map/Creatures/BallastFloraEventData.cs | 74 ++ .../State/BallastFloraStateMachine.cs | 4 +- .../SharedSource/Map/Entity.cs | 1 + .../SharedSource/Map/Explosion.cs | 4 +- .../SharedSource/Map/FireSource.cs | 4 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 9 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 97 ++- .../SharedSource/Map/HullEventData.cs | 70 ++ .../SharedSource/Map/IDamageable.cs | 37 +- .../SharedSource/Map/Levels/Level.cs | 39 +- .../Levels/LevelObjects/LevelObjectManager.cs | 19 +- .../SharedSource/Map/Map/Location.cs | 4 +- .../SharedSource/Map/StructurePrefab.cs | 41 +- .../SharedSource/Map/Submarine.cs | 2 +- .../SharedSource/Networking/ChatMessage.cs | 8 +- .../Networking/ChildServerRelay.cs | 57 +- .../SharedSource/Networking/EntitySpawner.cs | 83 +- .../Networking/INetSerializable.cs | 25 +- .../Networking/INetSerializableStruct.cs | 211 +++-- .../NetEntityEvent/NetEntityEvent.cs | 68 +- .../NetEntityEvent/NetEntityEventManager.cs | 3 +- .../SharedSource/Networking/NetworkMember.cs | 10 +- .../Networking/Primitives/Message/Message.cs | 4 +- .../SharedSource/PerformanceCounter.cs | 94 ++- .../SharedSource/Screens/GameScreen.cs | 9 +- .../Serialization/SerializableProperty.cs | 5 +- .../Serialization/XMLExtensions.cs | 163 ++-- .../StatusEffects/StatusEffect.cs | 15 +- .../SharedSource/SteamAchievementManager.cs | 10 +- .../SharedSource/Text/RichString.cs | 2 +- .../SharedSource/Utils/NamedEvent.cs | 61 ++ .../SharedSource/Utils/Option/Option.cs | 12 + .../SharedSource/Utils/RichTextData.cs | 6 +- Barotrauma/BarotraumaShared/changelog.txt | 84 ++ 302 files changed, 5878 insertions(+), 3317 deletions(-) delete mode 100644 Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs rename Barotrauma/BarotraumaClient/ClientSource/GameSession/{GameModes => }/Data/CampaignMetadata.cs (100%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs create mode 100644 Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs index 6904554bd..cf8c7c628 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -42,7 +42,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { IsAlive = msg.ReadBoolean(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index a3dba23b3..22d741e27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -112,395 +112,395 @@ namespace Barotrauma } } - public virtual void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientWriteInput(IWriteMessage msg) { - if (extraData != null) + msg.Write((byte)ClientNetObject.CHARACTER_INPUT); + + if (memInput.Count > 60) { - switch ((NetEntityEvent.Type)extraData[0]) + memInput.RemoveRange(60, memInput.Count - 60); + } + + msg.Write(LastNetworkUpdateID); + byte inputCount = Math.Min((byte)memInput.Count, (byte)60); + msg.Write(inputCount); + for (int i = 0; i < inputCount; i++) + { + msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal); + msg.Write(memInput[i].intAim); + if (memInput[i].states.HasFlag(InputNetFlags.Select) || + memInput[i].states.HasFlag(InputNetFlags.Deselect) || + memInput[i].states.HasFlag(InputNetFlags.Use) || + memInput[i].states.HasFlag(InputNetFlags.Health) || + memInput[i].states.HasFlag(InputNetFlags.Grab)) { - case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 4); - Inventory.ClientWrite(msg, extraData); - break; - case NetEntityEvent.Type.Treatment: - msg.WriteRangedInteger(1, 0, 4); - msg.Write(AnimController.Anim == AnimController.Animation.CPR); - break; - case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 4); - break; - case NetEntityEvent.Type.UpdateTalents: - msg.WriteRangedInteger(3, 0, 4); - msg.Write((ushort)characterTalents.Count); - foreach (var unlockedTalent in characterTalents) - { - msg.Write(unlockedTalent.Prefab.UintIdentifier); - } - break; + msg.Write(memInput[i].interact); } } + } + + public virtual void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) + { + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}"); } + + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) + { + case InventoryStateEventData inventoryStateEventData: + Inventory.ClientEventWrite(msg, inventoryStateEventData); + break; + case TreatmentEventData _: + msg.Write(AnimController.Anim == AnimController.Animation.CPR); + break; + case StatusEventData _: + //do nothing + break; + case UpdateTalentsEventData _: + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.Prefab.UintIdentifier); + } + break; + default: + throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}"); + } + } + + public void ClientReadPosition(IReadMessage msg, float sendingTime) + { + bool facingRight = AnimController.Dir > 0.0f; + + lastRecvPositionUpdateTime = (float)Lidgren.Network.NetTime.Now; + + AnimController.Frozen = false; + Enabled = true; + + UInt16 networkUpdateID = 0; + if (msg.ReadBoolean()) + { + networkUpdateID = msg.ReadUInt16(); + } else { - msg.Write((byte)ClientNetObject.CHARACTER_INPUT); + bool aimInput = msg.ReadBoolean(); + keys[(int)InputType.Aim].Held = aimInput; + keys[(int)InputType.Aim].SetState(false, aimInput); - if (memInput.Count > 60) + bool shootInput = msg.ReadBoolean(); + keys[(int)InputType.Shoot].Held = shootInput; + keys[(int)InputType.Shoot].SetState(false, shootInput); + + bool useInput = msg.ReadBoolean(); + keys[(int)InputType.Use].Held = useInput; + keys[(int)InputType.Use].SetState(false, useInput); + + if (AnimController is HumanoidAnimController) { - memInput.RemoveRange(60, memInput.Count - 60); + bool crouching = msg.ReadBoolean(); + keys[(int)InputType.Crouch].Held = crouching; + keys[(int)InputType.Crouch].SetState(false, crouching); } - msg.Write(LastNetworkUpdateID); - byte inputCount = Math.Min((byte)memInput.Count, (byte)60); - msg.Write(inputCount); - for (int i = 0; i < inputCount; i++) - { - msg.WriteRangedInteger((int)memInput[i].states, 0, (int)InputNetFlags.MaxVal); - msg.Write(memInput[i].intAim); - if (memInput[i].states.HasFlag(InputNetFlags.Select) || - memInput[i].states.HasFlag(InputNetFlags.Deselect) || - memInput[i].states.HasFlag(InputNetFlags.Use) || - memInput[i].states.HasFlag(InputNetFlags.Health) || - memInput[i].states.HasFlag(InputNetFlags.Grab)) - { - msg.Write(memInput[i].interact); - } - } + bool attackInput = msg.ReadBoolean(); + keys[(int)InputType.Attack].Held = attackInput; + keys[(int)InputType.Attack].SetState(false, attackInput); + + double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; + cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; + TransformCursorPos(); + + bool ragdollInput = msg.ReadBoolean(); + keys[(int)InputType.Ragdoll].Held = ragdollInput; + keys[(int)InputType.Ragdoll].SetState(false, ragdollInput); + + facingRight = msg.ReadBoolean(); } - msg.WritePadBits(); - } - public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) - { - switch (type) + bool entitySelected = msg.ReadBoolean(); + Character selectedCharacter = null; + Item selectedItem = null; + + AnimController.Animation animation = AnimController.Animation.None; + if (entitySelected) { - case ServerNetObject.ENTITY_POSITION: - bool facingRight = AnimController.Dir > 0.0f; - - lastRecvPositionUpdateTime = (float)Lidgren.Network.NetTime.Now; - - AnimController.Frozen = false; - Enabled = true; - - UInt16 networkUpdateID = 0; - if (msg.ReadBoolean()) + ushort characterID = msg.ReadUInt16(); + ushort itemID = msg.ReadUInt16(); + selectedCharacter = FindEntityByID(characterID) as Character; + selectedItem = FindEntityByID(itemID) as Item; + if (characterID != NullEntityID) + { + bool doingCpr = msg.ReadBoolean(); + if (doingCpr && SelectedCharacter != null) { - networkUpdateID = msg.ReadUInt16(); + animation = AnimController.Animation.CPR; } - else - { - bool aimInput = msg.ReadBoolean(); - keys[(int)InputType.Aim].Held = aimInput; - keys[(int)InputType.Aim].SetState(false, aimInput); - - bool shootInput = msg.ReadBoolean(); - keys[(int)InputType.Shoot].Held = shootInput; - keys[(int)InputType.Shoot].SetState(false, shootInput); - - bool useInput = msg.ReadBoolean(); - keys[(int)InputType.Use].Held = useInput; - keys[(int)InputType.Use].SetState(false, useInput); - - if (AnimController is HumanoidAnimController) - { - bool crouching = msg.ReadBoolean(); - keys[(int)InputType.Crouch].Held = crouching; - keys[(int)InputType.Crouch].SetState(false, crouching); - } - - bool attackInput = msg.ReadBoolean(); - keys[(int)InputType.Attack].Held = attackInput; - keys[(int)InputType.Attack].SetState(false, attackInput); - - double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; - cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; - TransformCursorPos(); - - bool ragdollInput = msg.ReadBoolean(); - keys[(int)InputType.Ragdoll].Held = ragdollInput; - keys[(int)InputType.Ragdoll].SetState(false, ragdollInput); - - facingRight = msg.ReadBoolean(); - } - - bool entitySelected = msg.ReadBoolean(); - Character selectedCharacter = null; - Item selectedItem = null; - - AnimController.Animation animation = AnimController.Animation.None; - if (entitySelected) - { - ushort characterID = msg.ReadUInt16(); - ushort itemID = msg.ReadUInt16(); - selectedCharacter = FindEntityByID(characterID) as Character; - selectedItem = FindEntityByID(itemID) as Item; - if (characterID != NullEntityID) - { - bool doingCpr = msg.ReadBoolean(); - if (doingCpr && SelectedCharacter != null) - { - animation = AnimController.Animation.CPR; - } - } - } - - Vector2 pos = new Vector2( - msg.ReadSingle(), - msg.ReadSingle()); - float MaxVel = NetConfig.MaxPhysicsBodyVelocity; - Vector2 linearVelocity = new Vector2( - msg.ReadRangedSingle(-MaxVel, MaxVel, 12), - msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); - linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12); - - bool fixedRotation = msg.ReadBoolean(); - float? rotation = null; - float? angularVelocity = null; - if (!fixedRotation) - { - rotation = msg.ReadSingle(); - float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; - angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8); - angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8); - } - - bool readStatus = msg.ReadBoolean(); - if (readStatus) - { - ReadStatus(msg); - AIController?.ClientRead(msg); - } - - msg.ReadPadBits(); - - int index = 0; - if (GameMain.Client.Character == this && CanMove) - { - var posInfo = new CharacterStateInfo( - pos, rotation, - networkUpdateID, - facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, animation); - - while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) - index++; - memState.Insert(index, posInfo); - } - else - { - var posInfo = new CharacterStateInfo( - pos, rotation, - linearVelocity, angularVelocity, - sendingTime, facingRight ? Direction.Right : Direction.Left, - selectedCharacter, selectedItem, animation); - - while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) - index++; - memState.Insert(index, posInfo); - } - - break; - case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 13); - switch (eventType) - { - case 0: //NetEntityEvent.Type.InventoryState - if (Inventory == null) - { - string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; - DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); - - //read anyway to prevent messing up reading the rest of the message - _ = msg.ReadUInt16(); - byte inventoryItemCount = msg.ReadByte(); - for (int i = 0; i < inventoryItemCount; i++) - { - msg.ReadUInt16(); - } - } - else - { - Inventory.ClientRead(type, msg, sendingTime); - } - break; - case 1: //NetEntityEvent.Type.Control - bool myCharacter = msg.ReadBoolean(); - byte ownerID = msg.ReadByte(); - ResetNetState(); - if (myCharacter) - { - if (controlled != null) - { - LastNetworkUpdateID = controlled.LastNetworkUpdateID; - } - - if (!IsDead) { Controlled = this; } - IsRemotePlayer = false; - GameMain.Client.HasSpawned = true; - GameMain.Client.Character = this; - GameMain.LightManager.LosEnabled = true; - GameMain.LightManager.LosAlpha = 1f; - GameMain.Client.WaitForNextRoundRespawn = null; - } - else - { - if (controlled == this) - { - Controlled = null; - IsRemotePlayer = ownerID > 0; - } - } - break; - case 2: //NetEntityEvent.Type.Status - ReadStatus(msg); - break; - case 3: //NetEntityEvent.Type.UpdateSkills - int skillCount = msg.ReadByte(); - for (int i = 0; i < skillCount; i++) - { - Identifier skillIdentifier = msg.ReadIdentifier(); - float skillLevel = msg.ReadSingle(); - info?.SetSkillLevel(skillIdentifier, skillLevel); - } - break; - case 4: // NetEntityEvent.Type.SetAttackTarget - case 5: //NetEntityEvent.Type.ExecuteAttack - int attackLimbIndex = msg.ReadByte(); - UInt16 targetEntityID = msg.ReadUInt16(); - int targetLimbIndex = msg.ReadByte(); - Vector2 targetSimPos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); - //255 = entity already removed, no need to do anything - if (attackLimbIndex == 255 || Removed) { break; } - if (attackLimbIndex >= AnimController.Limbs.Length) - { - DebugConsole.ThrowError($"Received invalid {(eventType == 4 ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); - break; - } - Limb attackLimb = AnimController.Limbs[attackLimbIndex]; - Limb targetLimb = null; - IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable; - if (targetEntity == null && eventType == 4) - { - DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); - break; - } - if (targetEntity is Character targetCharacter) - { - if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) - { - DebugConsole.ThrowError($"Received invalid {(eventType == 4 ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); - break; - } - targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; - } - if (attackLimb?.attack != null && Controlled != this) - { - if (eventType == 4) - { - SetAttackTarget(attackLimb, targetEntity, targetSimPos); - PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); - } - else - { - attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); - } - } - break; - case 6: //NetEntityEvent.Type.AssignCampaignInteraction - byte campaignInteractionType = msg.ReadByte(); - bool requireConsciousness = msg.ReadBoolean(); - (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); - RequireConsciousnessForCustomInteract = requireConsciousness; - break; - case 7: //NetEntityEvent.Type.ObjectiveManagerState - // 1 = order, 2 = objective - int msgType = msg.ReadRangedInteger(0, 2); - if (msgType == 0) { break; } - bool validData = msg.ReadBoolean(); - if (!validData) { break; } - if (msgType == 1) - { - UInt32 orderPrefabUintIdentifier = msg.ReadUInt32(); - var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); - Identifier option = Identifier.Empty; - if (orderPrefab.HasOptions) - { - int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); - if (optionIndex > -1) - { - option = orderPrefab.AllOptions[optionIndex]; - } - } - GameMain.GameSession?.CrewManager?.SetOrderHighlight(this, orderPrefab.Identifier, option); - } - else if (msgType == 2) - { - Identifier identifier = msg.ReadIdentifier(); - Identifier option = msg.ReadIdentifier(); - ushort objectiveTargetEntityId = msg.ReadUInt16(); - var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); - GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); - } - break; - case 8: //NetEntityEvent.Type.TeamChange - byte newTeamId = msg.ReadByte(); - ChangeTeam((CharacterTeamType)newTeamId); - break; - case 9: //NetEntityEvent.Type.AddToCrew - GameMain.GameSession.CrewManager.AddCharacter(this); - CharacterTeamType teamID = (CharacterTeamType)msg.ReadByte(); - ushort itemCount = msg.ReadUInt16(); - for (int i = 0; i < itemCount; i++) - { - ushort itemID = msg.ReadUInt16(); - if (!(Entity.FindEntityByID(itemID) is Item item)) { continue; } - item.AllowStealing = true; - var wifiComponent = item.GetComponent(); - if (wifiComponent != null) - { - wifiComponent.TeamID = teamID; - } - var idCard = item.GetComponent(); - if (idCard != null) - { - idCard.TeamID = teamID; - idCard.SubmarineSpecificID = 0; - } - } - break; - case 10: //NetEntityEvent.Type.UpdateExperience - int experienceAmount = msg.ReadInt32(); - info?.SetExperience(experienceAmount); - break; - case 11: //NetEntityEvent.Type.UpdateTalents: - ushort talentCount = msg.ReadUInt16(); - for (int i = 0; i < talentCount; i++) - { - bool addedThisRound = msg.ReadBoolean(); - UInt32 talentIdentifier = msg.ReadUInt32(); - GiveTalent(talentIdentifier, addedThisRound); - } - break; - case 12: //NetEntityEvent.Type.UpdateMoney: - int moneyAmount = msg.ReadInt32(); - SetMoney(moneyAmount); - break; - case 13: //NetEntityEvent.Type.UpdatePermanentStats: - byte savedStatValueCount = msg.ReadByte(); - StatTypes statType = (StatTypes)msg.ReadByte(); - info?.ClearSavedStatValues(statType); - for (int i = 0; i < savedStatValueCount; i++) - { - string statIdentifier = msg.ReadString(); - float statValue = msg.ReadSingle(); - bool removeOnDeath = msg.ReadBoolean(); - info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); - } - break; - - } - msg.ReadPadBits(); - break; + } } + + Vector2 pos = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + float MaxVel = NetConfig.MaxPhysicsBodyVelocity; + Vector2 linearVelocity = new Vector2( + msg.ReadRangedSingle(-MaxVel, MaxVel, 12), + msg.ReadRangedSingle(-MaxVel, MaxVel, 12)); + linearVelocity = NetConfig.Quantize(linearVelocity, -MaxVel, MaxVel, 12); + + bool fixedRotation = msg.ReadBoolean(); + float? rotation = null; + float? angularVelocity = null; + if (!fixedRotation) + { + rotation = msg.ReadSingle(); + float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; + angularVelocity = msg.ReadRangedSingle(-MaxAngularVel, MaxAngularVel, 8); + angularVelocity = NetConfig.Quantize(angularVelocity.Value, -MaxAngularVel, MaxAngularVel, 8); + } + + bool readStatus = msg.ReadBoolean(); + if (readStatus) + { + ReadStatus(msg); + AIController?.ClientRead(msg); + } + + msg.ReadPadBits(); + + int index = 0; + if (GameMain.Client.Character == this && CanMove) + { + var posInfo = new CharacterStateInfo( + pos, rotation, + networkUpdateID, + facingRight ? Direction.Right : Direction.Left, + selectedCharacter, selectedItem, animation); + + while (index < memState.Count && NetIdUtils.IdMoreRecent(posInfo.ID, memState[index].ID)) + index++; + memState.Insert(index, posInfo); + } + else + { + var posInfo = new CharacterStateInfo( + pos, rotation, + linearVelocity, angularVelocity, + sendingTime, facingRight ? Direction.Right : Direction.Left, + selectedCharacter, selectedItem, animation); + + while (index < memState.Count && posInfo.Timestamp > memState[index].Timestamp) + index++; + memState.Insert(index, posInfo); + } + } + + public virtual void ClientEventRead(IReadMessage msg, float sendingTime) + { + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) + { + case EventType.InventoryState: + if (Inventory == null) + { + string errorMsg = "Received an inventory update message for an entity with no inventory ([name], removed: " + Removed + ")"; + DebugConsole.ThrowError(errorMsg.Replace("[name]", Name)); + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ClientRead:NoInventory" + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg.Replace("[name]", SpeciesName.Value)); + + //read anyway to prevent messing up reading the rest of the message + _ = msg.ReadUInt16(); + byte inventoryItemCount = msg.ReadByte(); + for (int i = 0; i < inventoryItemCount; i++) + { + msg.ReadUInt16(); + } + } + else + { + Inventory.ClientEventRead(msg, sendingTime); + } + break; + case EventType.Control: + bool myCharacter = msg.ReadBoolean(); + byte ownerID = msg.ReadByte(); + ResetNetState(); + if (myCharacter) + { + if (controlled != null) + { + LastNetworkUpdateID = controlled.LastNetworkUpdateID; + } + + if (!IsDead) { Controlled = this; } + IsRemotePlayer = false; + GameMain.Client.HasSpawned = true; + GameMain.Client.Character = this; + GameMain.LightManager.LosEnabled = true; + GameMain.LightManager.LosAlpha = 1f; + GameMain.Client.WaitForNextRoundRespawn = null; + } + else + { + if (controlled == this) + { + Controlled = null; + IsRemotePlayer = ownerID > 0; + } + } + break; + case EventType.Status: + ReadStatus(msg); + break; + case EventType.UpdateSkills: + int skillCount = msg.ReadByte(); + for (int i = 0; i < skillCount; i++) + { + Identifier skillIdentifier = msg.ReadIdentifier(); + float skillLevel = msg.ReadSingle(); + info?.SetSkillLevel(skillIdentifier, skillLevel); + } + break; + case EventType.SetAttackTarget: + case EventType.ExecuteAttack: + int attackLimbIndex = msg.ReadByte(); + UInt16 targetEntityID = msg.ReadUInt16(); + int targetLimbIndex = msg.ReadByte(); + float targetX = msg.ReadSingle(); + float targetY = msg.ReadSingle(); + Vector2 targetSimPos = new Vector2(targetX, targetY); + //255 = entity already removed, no need to do anything + if (attackLimbIndex == 255 || Removed) { break; } + if (attackLimbIndex >= AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + break; + } + Limb attackLimb = AnimController.Limbs[attackLimbIndex]; + Limb targetLimb = null; + IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable; + if (targetEntity == null && eventType == EventType.SetAttackTarget) + { + DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); + break; + } + if (targetEntity is Character targetCharacter) + { + if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + break; + } + targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; + } + if (attackLimb?.attack != null && Controlled != this) + { + if (eventType == EventType.SetAttackTarget) + { + SetAttackTarget(attackLimb, targetEntity, targetSimPos); + PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); + } + else + { + attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); + } + } + break; + case EventType.AssignCampaignInteraction: + byte campaignInteractionType = msg.ReadByte(); + bool requireConsciousness = msg.ReadBoolean(); + (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); + RequireConsciousnessForCustomInteract = requireConsciousness; + break; + case EventType.ObjectiveManagerState: + // 1 = order, 2 = objective + AIObjectiveManager.ObjectiveType msgType + = (AIObjectiveManager.ObjectiveType)msg.ReadRangedInteger( + (int)AIObjectiveManager.ObjectiveType.MinValue, + (int)AIObjectiveManager.ObjectiveType.MaxValue); + if (msgType == 0) { break; } + bool validData = msg.ReadBoolean(); + if (!validData) { break; } + if (msgType == AIObjectiveManager.ObjectiveType.Order) + { + UInt32 orderPrefabUintIdentifier = msg.ReadUInt32(); + var orderPrefab = OrderPrefab.Prefabs.Find(p => p.UintIdentifier == orderPrefabUintIdentifier); + Identifier option = Identifier.Empty; + if (orderPrefab.HasOptions) + { + int optionIndex = msg.ReadRangedInteger(-1, orderPrefab.AllOptions.Length); + if (optionIndex > -1) + { + option = orderPrefab.AllOptions[optionIndex]; + } + } + GameMain.GameSession?.CrewManager?.SetOrderHighlight(this, orderPrefab.Identifier, option); + } + else if (msgType == AIObjectiveManager.ObjectiveType.Objective) + { + Identifier identifier = msg.ReadIdentifier(); + Identifier option = msg.ReadIdentifier(); + ushort objectiveTargetEntityId = msg.ReadUInt16(); + var objectiveTargetEntity = FindEntityByID(objectiveTargetEntityId); + GameMain.GameSession?.CrewManager?.CreateObjectiveIcon(this, identifier, option, objectiveTargetEntity); + } + break; + case EventType.TeamChange: + byte newTeamId = msg.ReadByte(); + ChangeTeam((CharacterTeamType)newTeamId); + break; + case EventType.AddToCrew: + GameMain.GameSession.CrewManager.AddCharacter(this); + CharacterTeamType teamID = (CharacterTeamType)msg.ReadByte(); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + ushort itemID = msg.ReadUInt16(); + if (!(Entity.FindEntityByID(itemID) is Item item)) { continue; } + item.AllowStealing = true; + var wifiComponent = item.GetComponent(); + if (wifiComponent != null) + { + wifiComponent.TeamID = teamID; + } + var idCard = item.GetComponent(); + if (idCard != null) + { + idCard.TeamID = teamID; + idCard.SubmarineSpecificID = 0; + } + } + break; + case EventType.UpdateExperience: + int experienceAmount = msg.ReadInt32(); + info?.SetExperience(experienceAmount); + break; + case EventType.UpdateTalents: + ushort talentCount = msg.ReadUInt16(); + for (int i = 0; i < talentCount; i++) + { + bool addedThisRound = msg.ReadBoolean(); + UInt32 talentIdentifier = msg.ReadUInt32(); + GiveTalent(talentIdentifier, addedThisRound); + } + break; + case EventType.UpdateMoney: + int moneyAmount = msg.ReadInt32(); + SetMoney(moneyAmount); + break; + case EventType.UpdatePermanentStats: + byte savedStatValueCount = msg.ReadByte(); + StatTypes statType = (StatTypes)msg.ReadByte(); + info?.ClearSavedStatValues(statType); + for (int i = 0; i < savedStatValueCount; i++) + { + string statIdentifier = msg.ReadString(); + float statValue = msg.ReadSingle(); + bool removeOnDeath = msg.ReadBoolean(); + info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true); + } + break; + + } + msg.ReadPadBits(); } public static Character ReadSpawnData(IReadMessage inc) @@ -542,6 +542,8 @@ namespace Barotrauma { bool hasOwner = inc.ReadBoolean(); int ownerId = hasOwner ? inc.ReadByte() : -1; + int balance = hasOwner ? inc.ReadInt32() : -1; + int rewardDistribution = hasOwner ? inc.ReadRangedInteger(0, 100) : -1; byte teamID = inc.ReadByte(); bool hasAi = inc.ReadBoolean(); Identifier infoSpeciesName = inc.ReadIdentifier(); @@ -558,6 +560,8 @@ namespace Barotrauma } character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); + character.Wallet.Balance = balance; + character.Wallet.RewardDistribution = rewardDistribution; if (character.CampaignInteractionType != CampaignMode.InteractionType.None) { (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(character, character.CampaignInteractionType); @@ -597,7 +601,7 @@ namespace Barotrauma : Identifier.Empty) .WithManualPriority(orderPriority) .WithOrderGiver(orderGiver); - character.SetOrder(order, speak: false, force: true); + character.SetOrder(order, isNewOrder: true, speak: false, force: true); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs deleted file mode 100644 index f89b83bdf..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Barotrauma -{ - partial class AfflictionHusk : Affliction - { - private InfectionState? prevDisplayedMessage; - partial void UpdateMessages() - { - if (character != Character.Controlled) { return; } - if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; } - if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } - - switch (State) - { - case InfectionState.Dormant: - if (Strength < DormantThreshold * 0.5f) - { - return; - } - GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); - break; - case InfectionState.Transition: - GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); - break; - case InfectionState.Active: - if (character.Params.UseHuskAppendage) - { - GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red); - } - break; - case InfectionState.Final: - default: - break; - } - prevDisplayedMessage = State; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index c84fe8eab..b61c76669 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -300,7 +300,7 @@ namespace Barotrauma if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.TreatmentEventData()); } return true; @@ -400,7 +400,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Status }); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.StatusEventData()); } else { @@ -573,10 +573,10 @@ namespace Barotrauma inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, limb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); - if (blood != null && !inWater) + if (blood != null) { blood.Size *= bloodParticleSize; - if (!string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) + if (!inWater && !string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) { blood.OnCollision += (Vector2 pos, Hull hull) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 4d924f605..41d2d470e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -523,7 +523,7 @@ namespace Barotrauma private string GetSpritePath(ContentPath texturePath) { if (!character.IsHumanoid) { return texturePath.Value; } - return GetSpritePath(texturePath, character?.Info); + return GetSpritePath(texturePath, character.Info); } partial void LoadParamsProjSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 22c5061a7..4f4d110e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -559,7 +559,7 @@ namespace Barotrauma GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName, difficulty, levelGenerationParams); - }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().ToArray() })); + }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().OrderBy(s => s).ToArray() })); commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { @@ -628,7 +628,7 @@ namespace Barotrauma DebugConsoleMapping.Instance.Remove(key); NewMessage("Keybind unbound.", GUIStyle.Green); return; - }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); + }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().OrderBy(k => k).ToArray() })); commands.Add(new Command("savebinds", "savebinds: Writes current keybinds into the config file.", (string[] args) => { @@ -710,6 +710,7 @@ namespace Barotrauma commands.Add(new Command("traitorlist", "", (string[] args) => { })); AssignRelayToServer("traitorlist", true); AssignRelayToServer("money", true); + AssignRelayToServer("showmoney", true); AssignRelayToServer("setskill", true); AssignRelayToServer("readycheck", true); @@ -1734,7 +1735,7 @@ namespace Barotrauma return new string[][] { - propertyList.Distinct().Select(i => i.Value).ToArray(), + propertyList.Distinct().Select(i => i.Value).OrderBy(n => n).ToArray(), Array.Empty() }; })); @@ -1763,17 +1764,26 @@ namespace Barotrauma foreach (var missionPrefab in MissionPrefab.Prefabs) { Identifier missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); - Identifier nameIdentifier = $"missionname.{missionId}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) + addIfMissing($"missionname.{missionId}".ToIdentifier(), language); + addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language); + } + + foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)))) + { + foreach (var property in itemComponentType.GetProperties()) { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } - Identifier descriptionIdentifier = $"missiondescription.{missionId}".ToIdentifier(); - if (!tags[language].Contains(descriptionIdentifier)) - { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } - missingTags[descriptionIdentifier].Add(language); + if (!property.IsDefined(typeof(InGameEditable), false)) { continue; } + + string propertyTag = $"{property.DeclaringType.Name}.{property.Name}"; + + addIfMissingAll(language, + propertyTag.ToIdentifier(), + property.Name.ToIdentifier(), + $"sp.{propertyTag}.name".ToIdentifier()); + + addIfMissingAll(language, + $"sp.{propertyTag}.description".ToIdentifier(), + $"{property.Name.ToIdentifier()}.description".ToIdentifier()); } } @@ -1781,18 +1791,8 @@ namespace Barotrauma { if (sub.Type != SubmarineType.Player || !sub.IsVanillaSubmarine()) { continue; } - Identifier nameIdentifier = $"submarine.name.{sub.Name}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } - Identifier descriptionIdentifier = ("submarine.description." + sub.Name).ToIdentifier(); - if (!tags[language].Contains(descriptionIdentifier)) - { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } - missingTags[descriptionIdentifier].Add(language); - } + addIfMissing($"submarine.name.{sub.Name}".ToIdentifier(), language); + addIfMissing(("submarine.description." + sub.Name).ToIdentifier(), language); } foreach (AfflictionPrefab affliction in AfflictionPrefab.List) @@ -1806,42 +1806,21 @@ namespace Barotrauma } Identifier afflictionId = affliction.TranslationIdentifier; - Identifier nameIdentifier = $"afflictionname.{afflictionId}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } - - Identifier descriptionIdentifier = $"afflictiondescription.{afflictionId}".ToIdentifier(); - if (!tags[language].Contains(descriptionIdentifier)) - { - if (!missingTags.ContainsKey(descriptionIdentifier)) { missingTags[descriptionIdentifier] = new HashSet(); } - missingTags[descriptionIdentifier].Add(language); - } + addIfMissing($"afflictionname.{afflictionId}".ToIdentifier(), language); + addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language); } foreach (var talentTree in TalentTree.JobTalentTrees) { foreach (var talentSubTree in talentTree.TalentSubTrees) { - Identifier nameIdentifier = $"talenttree.{talentSubTree.Identifier}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } + addIfMissing($"talenttree.{talentSubTree.Identifier}".ToIdentifier(), language); } } foreach (var talent in TalentPrefab.TalentPrefabs) { - Identifier nameIdentifier = $"talentname.{talent.Identifier}".ToIdentifier(); - if (!tags[language].Contains(nameIdentifier)) - { - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); - } + addIfMissing($"talentname.{talent.Identifier}".ToIdentifier(), language); } //check missing entity names @@ -1849,17 +1828,25 @@ namespace Barotrauma { Identifier nameIdentifier = ("entityname." + me.Identifier).ToIdentifier(); if (tags[language].Contains(nameIdentifier)) { continue; } + if (me.HideInMenus) { continue; } + + ContentXElement configElement = null; + if (me is ItemPrefab itemPrefab) { - nameIdentifier = itemPrefab.ConfigElement?.GetAttributeIdentifier("nameidentifier", nameIdentifier) ?? nameIdentifier; - if (nameIdentifier != null) - { - if (tags[language].Contains("entityname." + nameIdentifier)) { continue; } - } + configElement = itemPrefab.ConfigElement; + } + else if (me is StructurePrefab structurePrefab) + { + configElement = structurePrefab.ConfigElement; + } + if (configElement != null) + { + var overrideIdentifier = configElement.GetAttributeIdentifier("nameidentifier", null); + if (overrideIdentifier != null && tags[language].Contains("entityname." + overrideIdentifier)) { continue; } } - if (!missingTags.ContainsKey(nameIdentifier)) { missingTags[nameIdentifier] = new HashSet(); } - missingTags[nameIdentifier].Add(language); + addIfMissing(nameIdentifier, language); } } @@ -1868,11 +1855,7 @@ namespace Barotrauma foreach (LanguageIdentifier language in TextManager.AvailableLanguages) { if (language == TextManager.DefaultLanguage) { continue; } - if (!tags[language].Contains(englishTag)) - { - if (!missingTags.ContainsKey(englishTag)) { missingTags[englishTag] = new HashSet(); } - missingTags[englishTag].Add(language); - } + addIfMissing(englishTag, language); } } @@ -1920,6 +1903,24 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); SwapLanguage(TextManager.DefaultLanguage); + + void addIfMissing(Identifier tag, LanguageIdentifier language) + { + if (!tags[language].Contains(tag)) + { + if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet(); } + missingTags[tag].Add(language); + } + } + void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags) + { + if (!potentialTags.Any(t => tags[language].Contains(t))) + { + var tag = potentialTags.First(); + if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet(); } + missingTags[tag].Add(language); + } + } })); commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) => @@ -1944,7 +1945,10 @@ namespace Barotrauma } var content1 = getContent(doc1.Root); + var language1 = doc1.Root.GetAttributeIdentifier("language", string.Empty); + var content2 = getContent(doc2.Root); + var language2 = doc2.Root.GetAttributeIdentifier("language", string.Empty); foreach (KeyValuePair kvp in content1) { @@ -1952,12 +1956,9 @@ namespace Barotrauma { ThrowError($"File 2 doesn't contain the text tag \"{kvp.Key}\""); } - else + else if (language1 == language2 && content2[kvp.Key] != kvp.Value) { - if (content2[kvp.Key] != kvp.Value) - { - ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}"); - } + ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}"); } } foreach (KeyValuePair kvp in content2) @@ -2319,7 +2320,14 @@ namespace Barotrauma } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true; File.WriteAllLines(filePath, lines); - ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); + try + { + ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); + } + catch (Exception e) + { + ThrowError($"Failed to open the file \"{filePath}\".", e); + } System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 35ae05d0a..9c175f289 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -671,7 +671,7 @@ namespace Barotrauma if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio)) { radio.Channel = channel; - GameMain.Client?.CreateEntityEvent(radio.Item, new object[] { NetEntityEvent.Type.ChangeProperty, radio.SerializableProperties["channel".ToIdentifier()] }); + GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()])); if (setText) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 32cfeee49..04eabc733 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -172,7 +172,7 @@ namespace Barotrauma { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => FormatCurrency(campaign.Money) + TextGetter = () => FormatCurrency(campaign.Wallet.Balance) }; var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -630,7 +630,7 @@ namespace Barotrauma total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); totalBlock.Text = FormatCurrency(total); - bool enoughMoney = campaign != null ? total <= campaign.Money : true; + bool enoughMoney = campaign == null || campaign.Wallet.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); } @@ -652,7 +652,7 @@ namespace Barotrauma int total = nonDuplicateHires.Aggregate(0, (total, info) => total + info.Salary); - if (total > campaign.Money) { return false; } + if (!campaign.Wallet.CanAfford(total)) { return false; } bool atLeastOneHired = false; foreach (CharacterInfo ci in nonDuplicateHires) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index c83bca84c..cba0182cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -299,14 +299,16 @@ namespace Barotrauma return; } - if (GameMain.ShowFPS || GameMain.DebugDraw) + if (GameMain.ShowFPS || GameMain.DebugDraw || GameMain.ShowPerf) { - DrawString(spriteBatch, new Vector2(10, 10), + float y = 10.0f; + DrawString(spriteBatch, new Vector2(10, y), "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0) { - DrawString(spriteBatch, new Vector2(10, 25), + y += GameSettings.CurrentConfig.Graphics.TextScale * 15.0f; + DrawString(spriteBatch, new Vector2(10, y), $"Physics: {GameMain.CurrentUpdateRate}", (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont); } @@ -336,8 +338,15 @@ namespace Barotrauma DrawString(spriteBatch, new Vector2(300, y), key + ": " + elapsedMillisecs.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); - y += 15; + foreach (string childKey in GameMain.PerformanceCounter.GetSavedPartialIdentifiers(key)) + { + elapsedMillisecs = GameMain.PerformanceCounter.GetPartialAverageElapsedMillisecs(key, childKey); + DrawString(spriteBatch, new Vector2(315, y), + childKey + ": " + elapsedMillisecs.ToString("0.00"), + Color.Lerp(Color.LightGreen, GUIStyle.Red, elapsedMillisecs / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont); + y += 15; + } } if (Powered.Grids != null) @@ -1453,7 +1462,7 @@ namespace Barotrauma 3 => radii.Start, _ => throw new InvalidOperationException() }; - int getDirectionIndex(int vertexIndex) + static int getDirectionIndex(int vertexIndex) => (vertexIndex % 4) switch { 0 => (vertexIndex / 4) + 0, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs index 5fd93f7da..4cc0ee1e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs @@ -5,7 +5,7 @@ using Microsoft.Xna.Framework; namespace Barotrauma { - public class GUIColorPicker : GUIComponent + public class GUIColorPicker : GUIComponent, IDisposable { public delegate bool OnColorSelectedHandler(GUIColorPicker component, Color color); public OnColorSelectedHandler? OnColorSelected; @@ -34,11 +34,6 @@ namespace Barotrauma public GUIColorPicker(RectTransform rectT, string? style = null) : base(style, rectT) { } - ~GUIColorPicker() - { - DisposeTextures(); - } - private void Init() { int tWidth = Rect.Width; @@ -170,10 +165,12 @@ namespace Barotrauma } } - public void DisposeTextures() + public void Dispose() { mainTexture?.Dispose(); + mainTexture = null; hueTexture?.Dispose(); + hueTexture = null; } public void RefreshHue() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index b4dc7513d..5ec73c90f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -161,7 +161,7 @@ namespace Barotrauma } } - public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false) : base(style, rectT) + public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT) { text ??= new RawLString(""); @@ -170,9 +170,10 @@ namespace Barotrauma this.selectMultiple = selectMultiple; - button = new GUIButton(new RectTransform(Vector2.One, rectT), text, Alignment.CenterLeft, style: "GUIDropDown") + button = new GUIButton(new RectTransform(Vector2.One, rectT), text, textAlignment, style: "GUIDropDown") { - OnClicked = OnClicked + OnClicked = OnClicked, + TextBlock = { OverflowClip = true } }; GUIStyle.Apply(button, "", this); button.TextBlock.SetTextPos(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index d21951cc1..33837ece7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -96,11 +96,15 @@ namespace Barotrauma switch (child.ScaleBasis) { case ScaleBasis.BothHeight: + child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); + break; case ScaleBasis.Smallest when Rect.Height <= Rect.Width: case ScaleBasis.Largest when Rect.Height > Rect.Width: child.MinSize = new Point((int)((child.Rect.Height * child.RelativeSize.X) / child.RelativeSize.Y), child.MinSize.Y); break; case ScaleBasis.BothWidth: + child.MinSize = new Point(child.MinSize.X, child.Rect.Width); + break; case ScaleBasis.Smallest when Rect.Width <= Rect.Height: case ScaleBasis.Largest when Rect.Width > Rect.Height: child.MinSize = new Point(child.MinSize.X, (int)((child.Rect.Width * child.RelativeSize.Y) / child.RelativeSize.X)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index a410c8db5..b7265c76f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -12,9 +12,12 @@ namespace Barotrauma Int, Float } + public delegate void OnValueEnteredHandler(GUINumberInput numberInput); + public OnValueEnteredHandler OnValueEntered; + public delegate void OnValueChangedHandler(GUINumberInput numberInput); public OnValueChangedHandler OnValueChanged; - + public GUITextBox TextBox { get; private set; } public GUIButton PlusButton { get; private set; } @@ -209,6 +212,8 @@ namespace Barotrauma { ClampFloatValue(); } + + OnValueEntered?.Invoke(this); }; TextBox.OnEnterPressed += (textBox, text) => { @@ -220,6 +225,8 @@ namespace Barotrauma { ClampFloatValue(); } + + OnValueEntered?.Invoke(this); return true; }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs index ff245d132..1a17d1124 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScrollBar.cs @@ -312,6 +312,7 @@ namespace Barotrauma MoveButton(new Vector2( Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Bar.Rect.Width * barScale, Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Bar.Rect.Height * barScale)); + OnReleased?.Invoke(this, BarScroll); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index fb87b3639..6c530a3a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -271,7 +271,7 @@ namespace Barotrauma healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; - if (medicalClinic.GetMoney() > totalCost) + if (medicalClinic.GetWallet().CanAfford(totalCost)) { healList.PriceBlock.TextColor = GUIStyle.TextColorNormal; if (medicalClinic.PendingHeals.Any()) @@ -467,7 +467,7 @@ namespace Barotrauma GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { - TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetMoney()), + TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetWallet().Balance), AutoScaleVertical = true, TextScale = 1.1f }; @@ -577,7 +577,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")) { - Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetTotalCost() < medicalClinic.GetMoney(), + Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetWallet().CanAfford(medicalClinic.GetTotalCost()), OnClicked = (button, _) => { button.Enabled = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 47df0dea8..87c7ebe15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -3,6 +3,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; @@ -67,8 +68,7 @@ namespace Barotrauma private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; - private int PlayerMoney => campaignUI.Campaign.Money; - + private Wallet PlayerWallet => campaignUI.Campaign.Wallet; private bool IsBuying => activeTab switch { StoreTab.Buy => true, @@ -715,24 +715,30 @@ namespace Barotrauma private LocalizedString GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); - private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); + private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerWallet.Balance); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { var elementHeight = (int)(GUI.yScale * 80); - var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); - frame.UserData = "deals"; + var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null) + { + UserData = "deals" + }; var dealsGroup = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter); - var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - dealsHeader.UserData = "header"; + var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + UserData = "header" + }; var iconWidth = (0.9f * dealsHeader.Rect.Height) / dealsHeader.Rect.Width; var dealsIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 0.9f), dealsHeader.RectTransform), "StoreDealIcon", scaleToFit: true); var text = TextManager.Get(parentList == storeBuyList ? "campaignstore.dailyspecials" : "campaignstore.requestedgoods"); var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUIStyle.LargeFont); storeSpecialColor = dealsIcon.Color; dealsText.TextColor = storeSpecialColor; - var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine"); - divider.UserData = "divider"; + var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine") + { + UserData = "divider" + }; frame.CanBeFocused = dealsGroup.CanBeFocused = dealsHeader.CanBeFocused = dealsIcon.CanBeFocused = dealsText.CanBeFocused = divider.CanBeFocused = false; return dealsGroup; } @@ -1801,7 +1807,7 @@ namespace Barotrauma private void SetOwnedText(GUIComponent itemComponent, GUITextBlock ownedLabel = null) { ownedLabel ??= itemComponent?.FindChild("owned", recursive: true) as GUITextBlock; - if (itemComponent == null && ownedLabel == null) { return; } + if (itemComponent == null && ownedLabel == null) { return; } PurchasedItem purchasedItem = itemComponent?.UserData as PurchasedItem; ItemQuantity itemQuantity = null; LocalizedString ownedLabelText = string.Empty; @@ -1970,7 +1976,7 @@ namespace Barotrauma DebugConsole.ShowError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return false; } - } + } private bool BuyItems() { @@ -1990,7 +1996,7 @@ namespace Barotrauma } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); - if (itemsToPurchase.None() || totalPrice > PlayerMoney) { return false; } + if (itemsToPurchase.None() || !PlayerWallet.CanAfford(totalPrice)) { return false; } CargoManager.PurchaseItems(itemsToPurchase, true); GameMain.Client?.SendCampaignState(); @@ -2047,7 +2053,7 @@ namespace Barotrauma if (IsBuying) { shoppingCrateTotal.Text = GetCurrencyFormatted(buyTotal); - shoppingCrateTotal.TextColor = buyTotal > PlayerMoney ? Color.Red : Color.White; + shoppingCrateTotal.TextColor = !PlayerWallet.CanAfford(buyTotal) ? Color.Red : Color.White; } else { @@ -2093,7 +2099,7 @@ namespace Barotrauma ActiveShoppingCrateList.Content.RectTransform.Children.Any() && activeTab switch { - StoreTab.Buy => buyTotal <= PlayerMoney, + StoreTab.Buy => PlayerWallet.CanAfford(buyTotal), StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, _ => false @@ -2109,9 +2115,12 @@ namespace Barotrauma private float ownedItemsUpdateTimer = 0.0f, sellableItemsFromSubUpdateTimer = 0.0f; private const float timerUpdateInterval = 1.5f; + private readonly Stopwatch updateStopwatch = new Stopwatch(); public void Update(float deltaTime) { + updateStopwatch.Restart(); + if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); @@ -2124,10 +2133,10 @@ namespace Barotrauma { var prevOwnedItems = new Dictionary(OwnedItems); UpdateOwnedItems(); - var refresh = (prevOwnedItems.Count != OwnedItems.Count) || - (prevOwnedItems.Select(kvp => kvp.Value.Total).Sum() != OwnedItems.Select(kvp => kvp.Value.Total).Sum()) || - (OwnedItems.Any(kvp => kvp.Value.Total > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || - prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out ItemQuantity itemQuantity) || kvp.Value.Total != itemQuantity.Total)); + bool refresh = OwnedItems.Count != prevOwnedItems.Count || + OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) || + OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) || + prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key)); if (refresh) { needsItemsToSellRefresh = true; @@ -2138,8 +2147,13 @@ namespace Barotrauma sellableItemsFromSubUpdateTimer += deltaTime; if (sellableItemsFromSubUpdateTimer >= timerUpdateInterval) { - needsItemsToSellFromSubRefresh = true; - needsRefresh = true; + var prevSubItems = new List(itemsToSellFromSub); + RefreshItemsToSellFromSub(); + needsRefresh = needsRefresh || + itemsToSellFromSub.Count != prevSubItems.Count || + itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || + itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); } } @@ -2148,7 +2162,10 @@ namespace Barotrauma if (needsRefresh || HavePermissionsChanged()) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) { RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); } if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) { RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) { RefreshSellingFromSub(updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) { RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + + updateStopwatch.Stop(); + GameMain.PerformanceCounter.AddPartialElapsedTicks("GameSessionUpdate", "StoreUpdate", updateStopwatch.ElapsedTicks); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 2d8d5a598..4272b8dcc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -581,7 +581,7 @@ namespace Barotrauma private void ShowTransferPrompt() { - if (GameMain.GameSession.Campaign.Money < deliveryFee && deliveryFee > 0) + if (!GameMain.GameSession.Campaign.Wallet.CanAfford(deliveryFee) && deliveryFee > 0) { new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", ("[currencyname]", currencyLongText), @@ -629,7 +629,7 @@ namespace Barotrauma private void ShowBuyPrompt(bool purchaseOnly) { - if (GameMain.GameSession.Campaign.Money < selectedSubmarine.Price) + if (!GameMain.GameSession.Campaign.Wallet.CanAfford(selectedSubmarine.Price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", ("[currencyname]", currencyLongText), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index aa22ddcd0..c31514929 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -37,6 +37,12 @@ namespace Barotrauma private GUIFrame pendingChangesFrame = null; public static Color OwnCharacterBGColor = Color.Gold * 0.7f; + private bool isTransferMenuOpen; + private bool isSending; + private GUIComponent transferMenu; + private GUIButton transferMenuButton; + private float transferMenuOpenState; + private bool transferMenuStateCompleted; private class LinkedGUI { @@ -133,8 +139,43 @@ namespace Barotrauma SelectInfoFrameTab(SelectedTab); } - public void Update() + public void Update(float deltaTime) { + float menuOpenSpeed = deltaTime * 10f; + if (isTransferMenuOpen) + { + if (transferMenuStateCompleted) + { + transferMenuOpenState = transferMenuOpenState < 0.25f ? Math.Min(0.25f, transferMenuOpenState + (menuOpenSpeed / 2f)) : 0.25f; + } + else + { + if (transferMenuOpenState > 0.15f) + { + transferMenuStateCompleted = false; + transferMenuOpenState = Math.Max(0.15f, transferMenuOpenState - menuOpenSpeed); + } + else + { + transferMenuStateCompleted = true; + } + } + } + else + { + transferMenuStateCompleted = false; + if (transferMenuOpenState < 1f) + { + transferMenuOpenState = Math.Min(1f, transferMenuOpenState + menuOpenSpeed); + } + } + + if (transferMenu != null && transferMenuButton != null) + { + int pos = (int)(transferMenuOpenState * -transferMenu.Rect.Height); + transferMenu.RectTransform.AbsoluteOffset = new Point(0, pos); + transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height); + } GameSession.UpdateTalentNotificationIndicator(talentPointNotification); if (Character.Controlled is { } controlled && talentResetButton != null && talentApplyButton != null) { @@ -243,10 +284,7 @@ namespace Barotrauma var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation"); var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame"); - new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), "", textAlignment: Alignment.Right) - { - TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) - }; + GUITextBlock balanceText = new GUITextBlock(new RectTransform(Vector2.One, balanceFrame.RectTransform), string.Empty, textAlignment: Alignment.Right); 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)) @@ -258,6 +296,18 @@ namespace Barotrauma { NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); } + + SetBalanceText(balanceText, campaignMode.Bank.Balance); + campaignMode.OnMoneyChanged.RegisterOverwriteExisting(nameof(CreateInfoFrame).ToIdentifier(), e => + { + if (e.Wallet != campaignMode.Bank) { return; } + SetBalanceText(balanceText, e.Wallet.Balance); + }); + + static void SetBalanceText(GUITextBlock text, int balance) + { + text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance)); + } } else { @@ -329,7 +379,8 @@ namespace Barotrauma private void CreateCrewListFrame(GUIFrame crewFrame) { - crew = GameMain.GameSession.CrewManager.GetCharacters(); + // FIXME remove TestScreen stuff + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new []{ TestScreen.dummyCharacter }; teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -743,7 +794,7 @@ namespace Barotrauma Client client = userData as Client; GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter"); - if (existingPreview != null) infoFrameHolder.RemoveChild(existingPreview); + if (existingPreview != null) { infoFrameHolder.RemoveChild(existingPreview); } GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) }) { @@ -760,17 +811,291 @@ namespace Barotrauma { GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); GameMain.Client.SelectCrewCharacter(character, preview); + CreateWalletFrame(background, character); } } else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); - if (GameMain.NetworkMember != null) GameMain.Client.SelectCrewClient(client, preview); + if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewClient(client, preview); } + CreateWalletFrame(background, client.Character); } return true; } + private void CreateWalletFrame(GUIComponent parent, Character character) + { + if (character is null) { throw new ArgumentNullException(nameof(character), "Tried to create a wallet frame for a null character");} + isTransferMenuOpen = false; + transferMenuOpenState = 1f; + ImmutableArray salaryCrew = Mission.GetSalaryEligibleCrew().Where(c => c != character).ToImmutableArray(); + + Wallet targetWallet = character.Wallet; + + GUIFrame walletFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), parent.RectTransform, anchor: Anchor.TopLeft) + { + RelativeOffset = new Vector2(0, 1.02f) + }); + + GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(walletFrame.RectTransform, 0.9f), walletFrame.RectTransform, anchor: Anchor.Center)); + + GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), walletLayout.RectTransform), isHorizontal: true); + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "StoreTradingIcon", scaleToFit: true); + 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); + GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), UpgradeStore.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), $"{Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage}%", textAlignment: Alignment.BottomRight); + 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) + { + Range = Vector2.UnitY, + BarScrollValue = targetWallet.RewardDistribution / 100f, + Step = 0.01f, + BarSize = 0.1f, + OnMoved = (bar, scroll) => + { + rewardBlock.Text = $"{Mission.GetRewardShare((int)(scroll * 100f), salaryCrew, Option.None()).Percentage}%"; + return true; + }, + OnReleased = (bar, scroll) => + { + int newRewardDistribution = (int)(scroll * 100); + if (newRewardDistribution == targetWallet.RewardDistribution) { return false; } + SetRewardDistribution(character, newRewardDistribution); + return true; + } + }; +// @formatter:off + GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter)) + { + CanBeFocused = false + }; + transferMenu = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform)); + + GUILayoutGroup transferMenuLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), transferMenu.RectTransform, Anchor.BottomLeft), childAnchor: Anchor.Center); + GUILayoutGroup paddedTransferMenuLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(transferMenuLayout.RectTransform, 0.85f), transferMenuLayout.RectTransform)); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedTransferMenuLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform)); + GUITextBlock leftName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), character.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); + GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue }; + GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform), childAnchor: Anchor.TopRight); + GUITextBlock rightName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); + GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red }; + GUILayoutGroup centerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), mainLayout.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { IgnoreLayoutGroups = true }; + new GUIFrame(new RectTransform(new Vector2(0f, 1f), centerLayout.RectTransform, Anchor.Center), style: "VerticalLine") { IgnoreLayoutGroups = true }; + GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(0.6f), 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) + { + MinValueInt = 0 + }; + + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); + GUILayoutGroup centerButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), buttonLayout.RectTransform), isHorizontal: true); + GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; + GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; +// @formatter:on + transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical") + { + OnClicked = (button, o) => + { + isTransferMenuOpen = !isTransferMenuOpen; + if (!isTransferMenuOpen) + { + transferAmountInput.IntValue = 0; + } + ToggleTransferMenuIcon(button, open: isTransferMenuOpen); + return true; + } + }; + ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen); + ToggleCenterButton(centerButton, isSending); + + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + { + if (!(Character.Controlled is { } myCharacter)) + { + salarySlider.Enabled = false; + transferAmountInput.Enabled = false; + centerButton.Enabled = false; + confirmButton.Enabled = false; + return; + } + + bool hasPermissions = campaign.AllowedToManageCampaign(); + salarySlider.Enabled = hasPermissions; + Wallet otherWallet; + + switch (hasPermissions) + { + 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; + } + + if (!hasPermissions) + { + centerButton.Enabled = centerButton.CanBeFocused = false; + salarySlider.Enabled = salarySlider.CanBeFocused = false; + } + + leftBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + + UpdateAllInputs(); + + centerButton.OnClicked = (btn, o) => + { + isSending = !isSending; + ToggleCenterButton(btn, isSending); + UpdateAllInputs(); + return true; + }; + + transferAmountInput.OnValueChanged = input => + { + UpdateInputs(); + }; + + transferAmountInput.OnValueEntered = input => + { + UpdateAllInputs(); + }; + + campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(CreateWalletFrame).ToIdentifier(), e => + { + if (e.Wallet == targetWallet) + { + moneyBlock.Text = UpgradeStore.FormatCurrency(e.Info.Balance); + salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; + } + 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; + }; + + void UpdateAllInputs() + { + UpdateInputs(); + UpdateMaxInput(); + } + + void UpdateInputs() + { + confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; + if (transferAmountInput.IntValue == 0) + { + rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + rightBalance.TextColor = GUIStyle.TextColorNormal; + leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance); + leftBalance.TextColor = GUIStyle.TextColorNormal; + } + else if (isSending) + { + rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Blue; + leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Red; + } + else + { + rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); + rightBalance.TextColor = GUIStyle.Red; + leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); + leftBalance.TextColor = GUIStyle.Blue; + } + } + + void UpdateMaxInput() + { + transferAmountInput.MaxValueInt = isSending ? targetWallet.Balance : otherWallet.Balance; + } + } + + static void ToggleTransferMenuIcon(GUIButton btn, bool open) + { + foreach (GUIComponent child in btn.Children) + { + child.SpriteEffects = open ? SpriteEffects.None : SpriteEffects.FlipVertically; + } + } + + static void ToggleCenterButton(GUIButton btn, bool isSending) + { + foreach (GUIComponent child in btn.Children) + { + child.SpriteEffects = isSending ? SpriteEffects.None : SpriteEffects.FlipHorizontally; + } + } + + static void SendTransaction(Option to, Option from, int amount) + { + INetSerializableStruct transfer = new NetWalletTransfer + { + Sender = from.Select(option => option.ID), + Receiver = to.Select(option => option.ID), + Amount = amount + }; + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.MONEY); + transfer.Write(msg); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + static void SetRewardDistribution(Character character, int newValue) + { + INetSerializableStruct transfer = new NetWalletSalaryUpdate + { + Target = character.ID, + NewRewardDistribution = newValue + }; + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION); + transfer.Write(msg); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + static int GetRewardDistributionPercentage(int distribution, ImmutableArray crew) + { + return Mission.GetRewardShare(distribution, crew, Option.None()).Percentage; + } + } + private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) { GUIComponent paddedFrame; @@ -1209,8 +1534,6 @@ namespace Barotrauma } } private Color unselectedColor = new Color(240, 255, 255, 225); - private Color selectedColor = new Color(220, 255, 220, 225); - private Color ownedColor = new Color(140, 180, 140, 225); private Color unselectableColor = new Color(100, 100, 100, 225); private Color pressedColor = new Color(60, 60, 60, 225); @@ -1427,7 +1750,7 @@ namespace Barotrauma GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - foreach (TalentPrefab talent in talentOption.Talents) + foreach (TalentPrefab talent in talentOption.Talents.OrderBy(t => t.Identifier)) { GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) { @@ -1652,7 +1975,7 @@ namespace Barotrauma controlledCharacter.GiveTalent(talent); if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(controlledCharacter, new object[] { NetEntityEvent.Type.UpdateTalents }); + GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData()); } } selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index a5be0901b..f4ec7f408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -41,7 +41,7 @@ namespace Barotrauma private readonly CampaignUI campaignUI; private CampaignMode? Campaign => campaignUI.Campaign; - private int AvailableMoney => Campaign?.Money ?? 0; + private Wallet PlayerWallet => Campaign?.Wallet ?? Wallet.Invalid; private UpgradeTab selectedUpgradeTab = UpgradeTab.Upgrade; private GUIMessageBox? currectConfirmation; @@ -61,7 +61,7 @@ namespace Barotrauma private Vector2[][] subHullVertices = new Vector2[0][]; private List submarineWalls = new List(); - public MapEntity? HoveredItem; + public MapEntity? HoveredEntity; private bool highlightWalls; private UpgradeCategory? currentUpgradeCategory; @@ -105,6 +105,7 @@ namespace Barotrauma Campaign.UpgradeManager.OnUpgradesChanged += RefreshAll; Campaign.CargoManager.OnPurchasedItemsChanged += RefreshAll; Campaign.CargoManager.OnSoldItemsChanged += RefreshAll; + Campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(UpgradeStore).ToIdentifier(), e => { RefreshAll(); } ); } public void RefreshAll() @@ -184,7 +185,7 @@ namespace Barotrauma } // reset the order first - foreach (UpgradeCategory category in UpgradeCategory.Categories) + foreach (UpgradeCategory category in UpgradeCategory.Categories.OrderBy(c => c.Name)) { GUIComponent component = categoryList.Content.FindChild(c => c.UserData is CategoryData categoryData && categoryData.Category == category); component?.SetAsLastChild(); @@ -286,7 +287,7 @@ namespace Barotrauma GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(AvailableMoney, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(AvailableMoney, format: true) }; + new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(PlayerWallet.Balance, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(PlayerWallet.Balance, format: true) }; new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => @@ -343,15 +344,15 @@ namespace Barotrauma private void DrawItemSwapPreview(SpriteBatch spriteBatch, GUICustomComponent component) { var selectedItem = customizeTabOpen ? - activeItemSwapSlideDown?.UserData as Item ?? HoveredItem as Item : - HoveredItem as Item; + activeItemSwapSlideDown?.UserData as Item ?? HoveredEntity as Item : + HoveredEntity as Item; if (selectedItem?.Prefab.SwappableItem == null) { return; } Sprite schematicsSprite = selectedItem.Prefab.SwappableItem.SchematicSprite; if (schematicsSprite == null) { return; } float schematicsScale = Math.Min(component.Rect.Width / 2 / schematicsSprite.size.X, component.Rect.Height / schematicsSprite.size.Y); Vector2 center = new Vector2(component.Rect.Center.X, component.Rect.Center.Y); - schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2), + schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2), scale: schematicsScale); var swappableItemList = selectedUpgradeCategoryLayout?.FindChild("prefablist", true) as GUIListBox; @@ -426,14 +427,14 @@ namespace Barotrauma return false; } - if (AvailableMoney >= hullRepairCost) + if (PlayerWallet.CanAfford(hullRepairCost)) { LocalizedString body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (AvailableMoney >= hullRepairCost) + if (PlayerWallet.Balance >= hullRepairCost) { - Campaign.Money -= hullRepairCost; + PlayerWallet.TryDeduct(hullRepairCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; button.Enabled = false; @@ -461,14 +462,14 @@ namespace Barotrauma CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) => { - if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (PlayerWallet.Balance >= itemRepairCost && !Campaign.PurchasedItemRepairs) { LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (AvailableMoney >= itemRepairCost && !Campaign.PurchasedItemRepairs) + if (PlayerWallet.Balance >= itemRepairCost && !Campaign.PurchasedItemRepairs) { - Campaign.Money -= itemRepairCost; + PlayerWallet.TryDeduct(itemRepairCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; button.Enabled = false; @@ -507,14 +508,14 @@ namespace Barotrauma return false; } - if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (PlayerWallet.CanAfford(shuttleRetrieveCost) && !Campaign.PurchasedLostShuttles) { LocalizedString body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString()); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () => { - if (AvailableMoney >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) + if (PlayerWallet.Balance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles) { - Campaign.Money -= shuttleRetrieveCost; + PlayerWallet.TryDeduct(shuttleRetrieveCost); GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; button.Enabled = false; @@ -572,13 +573,13 @@ namespace Barotrauma new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; new GUITextBlock(rectT(1, 0, textLayout), 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 = AvailableMoney >= price && !isDisabled, OnClicked = onPressed }; + new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { ClickSound = GUISoundType.HireRepairClick, Enabled = PlayerWallet.Balance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); buyButtonLayout.Recalculate(); if (disableElement) { - frameChild.Enabled = AvailableMoney >= price && !isDisabled; + frameChild.Enabled = PlayerWallet.Balance >= price && !isDisabled; } if (!HasPermission) @@ -610,9 +611,9 @@ namespace Barotrauma Dictionary> upgrades = new Dictionary>(); - foreach (UpgradeCategory category in UpgradeCategory.Categories) + foreach (UpgradeCategory category in UpgradeCategory.Categories.OrderBy(c => c.Name)) { - foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs) + foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs.OrderBy(p => p.Name)) { if (prefab.UpgradeCategories.Contains(category)) { @@ -963,12 +964,12 @@ namespace Barotrauma buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } - if (Campaign.Money >= price) + if (PlayerWallet.CanAfford(price)) { buyButton.Enabled = true; buyButton.OnClicked += (button, o) => { - LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", + LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody", ("[itemtoinstall]", replacement.Name), ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => @@ -1183,7 +1184,7 @@ namespace Barotrauma buyButton.OnClicked += (button, o) => { - LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", + LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody", ("[upgradename]", prefab.Name), ("[amount]", prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString())); currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () => @@ -1334,16 +1335,16 @@ namespace Barotrauma { if (GUI.MouseOn == frame) { - if (HoveredItem != item) { CreateItemTooltip(item); } - HoveredItem = item; + if (HoveredEntity != item) { CreateItemTooltip(item); } + HoveredEntity = item; if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null) { if (customizeTabOpen) { if (selectedUpgradeCategoryLayout != null) { - var linkedItems = HoveredItem is Item ? Campaign.UpgradeManager.GetLinkedItemsToSwap((Item)HoveredItem) : new List(); - if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem || linkedItems.Contains((Item)c.UserData), recursive: true) is GUIButton itemElement) + var linkedItems = HoveredEntity is Item hoveredItem ? Campaign.UpgradeManager.GetLinkedItemsToSwap(hoveredItem) : new List(); + if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData is Item item && (item == HoveredEntity || linkedItems.Contains(item)), recursive: true) is GUIButton itemElement) { if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); @@ -1370,8 +1371,8 @@ namespace Barotrauma // use pnpoly algorithm to detect if our mouse is within any of the hull polygons if (subHullVertices.Any(hullVertex => ToolBox.PointIntersectsWithPolygon(PlayerInput.MousePosition, hullVertex))) { - if (HoveredItem != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); } - HoveredItem = firstStructure; + if (HoveredEntity != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); } + HoveredEntity = firstStructure; isMouseOnStructure = true; GUI.MouseCursor = CursorState.Hand; @@ -1382,7 +1383,7 @@ namespace Barotrauma } } - if (!isMouseOnStructure) { HoveredItem = null; } + if (!isMouseOnStructure) { HoveredEntity = null; } } // flip the tooltip if it is outside of the screen @@ -1582,12 +1583,12 @@ namespace Barotrauma priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); } } - + GUIButton button = buttonParent.GetChild(); if (button != null) { button.Enabled = currentLevel < prefab.MaxLevel; - if (WaitForServerUpdate || !campaign.AllowedToManageCampaign() || price > campaign.Money) + if (WaitForServerUpdate || !campaign.AllowedToManageCampaign() || !campaign.Wallet.CanAfford(price)) { button.Enabled = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index f25e1db11..81e700dc4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -184,7 +184,7 @@ namespace Barotrauma } // Exchange money Location.StoreCurrentBalance -= itemValue; - campaign.Money += itemValue; + campaign.Wallet.TryDeduct(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 86d82528c..d00290a64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -236,7 +236,7 @@ namespace Barotrauma if (crewManager != null) { - Order order = new Order(orderPrefab, Identifier.Empty, CharacterInfo.HighestManualOrderPriority, Order.OrderType.Current, null, null, orderGiver: Character.Controlled); + Order order = orderPrefab.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: Character.Controlled); crewManager.SetCharacterOrder(null, order); if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } } @@ -290,16 +290,6 @@ namespace Barotrauma return crewArea.Rect; } - public IEnumerable GetCharacters() - { - return characters; - } - - public IEnumerable GetCharacterInfos() - { - return characterInfos; - } - /// /// Remove the character from the crew (and crew menus). /// @@ -708,7 +698,8 @@ namespace Barotrauma List availableSpeakers = Character.CharacterList.FindAll(c => c.AIController is HumanAIController && !c.IsDead && - c.SpeechImpediment <= 100.0f); + c.SpeechImpediment <= 100.0f && + c.CharacterHealth.GetAllAfflictions(a => a is AfflictionHusk huskInfection && huskInfection.Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }).None()); pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers)); } @@ -837,7 +828,7 @@ namespace Barotrauma var orderGiver = order?.OrderGiver; if (IsSinglePlayer) { - character.SetOrder(order, speak: orderGiver != character); + character.SetOrder(order, isNewOrder, speak: orderGiver != character); string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, givingOrderToSelf: character == orderGiver, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder); orderGiver?.Speak(message); } @@ -2437,26 +2428,26 @@ namespace Barotrauma optionNodes.Add(new OptionNode(node, Keys.D0 + hotkey % 10)); } + /// + /// The order giver doesn't need to be set for the Order instances as it will be set when the node button is clicked. + /// private void CreateShortcutNodes() { - bool HasAppropriateJobId(Character c, Identifier jobId) => c.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(jobId); - bool HasAppropriateJob(Character c, string jobId) => HasAppropriateJobId(c, jobId.ToIdentifier()); - - var sub = GetTargetSubmarine(); - if (sub == null) { return; } + if (!(GetTargetSubmarine() is { } sub)) { return; } shortcutNodes.Clear(); - if (CanFitMoreNodes() && sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) + var subItems = sub.GetItems(false); + if (CanFitMoreNodes() && subItems.Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { float reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor - // ---> Create shortcut node for "Operate Reactor" order's "Power Up" option + // --> Create shortcut node for "Operate Reactor" order's "Power Up" option if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var orderPrefab = OrderPrefab.Prefabs["operatereactor"]; - var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor, Character.Controlled); + var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor); if (IsNonDuplicateOrder(order)) { - shortcutNodes.Add(CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNode(order); } } } @@ -2464,11 +2455,11 @@ namespace Barotrauma // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) && - sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + subItems.Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { - var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering, Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering); + AddOrderNode(order); } // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order @@ -2476,8 +2467,7 @@ namespace Barotrauma ActiveOrders.Any(o => o.Order.Identifier == "reportintruders") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fightintruders"])) { - var order = new Order(OrderPrefab.Prefabs["fightintruders"], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("fightintruders"); } // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order @@ -2485,8 +2475,7 @@ namespace Barotrauma IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fixleaks"]) && ActiveOrders.Any(o => o.Order.Identifier == "reportbreach")) { - var order = new Order(OrderPrefab.Prefabs["fixleaks"], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("fixleaks"); } // --> Create shortcut nodes for the Repair orders if (CanFitMoreNodes() && ActiveOrders.Any(o => o.Order.Identifier == "reportbrokendevices")) @@ -2494,33 +2483,27 @@ namespace Barotrauma var reportBrokenDevices = OrderPrefab.Prefabs["reportbrokendevices"]; // TODO: Doesn't work for player issued reports, because they don't have a target. bool useSpecificRepairOrder = false; - string tag = "repairelectrical"; - if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + if (CanFitMoreNodes() && ShouldDelegateOrder("repairelectrical") && ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "electrical"))) { - if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairelectrical"])) { - var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("repairelectrical"); } useSpecificRepairOrder = true; } - tag = "repairmechanical"; - if (CanFitMoreNodes() && ShouldDelegateOrder(tag) && + if (CanFitMoreNodes() && ShouldDelegateOrder("repairmechanical") && ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.requiredSkills.Any(s => s.Identifier == "mechanical"))) { - if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs[tag])) + if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairmechanical"])) { - var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("repairmechanical"); } useSpecificRepairOrder = true; } - tag = "repairsystems"; - if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder(tag) && OrderPrefab.Prefabs[tag] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder)) + if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder("repairsystems") && OrderPrefab.Prefabs["repairsystems"] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder)) { - var order = new Order(OrderPrefab.Prefabs[tag], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("repairsystems"); } } // If fire is reported @@ -2528,8 +2511,7 @@ namespace Barotrauma if (CanFitMoreNodes() && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["extinguishfires"]) && ActiveOrders.Any(o => o.Order.Identifier == "reportfire")) { - var order = new Order(OrderPrefab.Prefabs["extinguishfires"], null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + AddOrderNodeWithIdentifier("extinguishfires"); } if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) { @@ -2541,8 +2523,8 @@ namespace Barotrauma { if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true, interactableFor: characterContext ?? Character.Controlled).Any()) { - var order = new Order(orderPrefab, null, orderGiver: Character.Controlled); - shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + var order = orderPrefab.CreateInstance(OrderPrefab.OrderTargetType.Entity); + AddOrderNode(order); } if (!CanFitMoreNodes()) { break; } } @@ -2550,9 +2532,8 @@ namespace Barotrauma } if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed) { - var order = new Order(OrderPrefab.Dismissal, null, orderGiver: Character.Controlled); - shortcutNodes.Add( - CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1)); + var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity); + AddOrderNode(order); } shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o)); if (shortcutNodes.Count < 1) { return; } @@ -2593,18 +2574,30 @@ namespace Barotrauma characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) : characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option)); } + void AddOrderNodeWithIdentifier(string identifier) + { + var order = OrderPrefab.Prefabs[identifier].CreateInstance(OrderPrefab.OrderTargetType.Entity); + AddOrderNode(order); + } + void AddOrderNode(Order order) + { + var node = order.Option.IsEmpty ? + CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1) : + CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1); + shortcutNodes.Add(node); + } } private void CreateOrderNodes(OrderCategory orderCategory) { - var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).ToArray(); + var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).OrderBy(o => o.Identifier).ToArray(); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(orderPrefabs.Length), GetFirstNodeAngle(orderPrefabs.Length)); for (int i = 0; i < orderPrefabs.Length; i++) { - order = new Order(orderPrefabs[i], null, orderGiver: Character.Controlled); + order = orderPrefabs[i].CreateInstance(OrderPrefab.OrderTargetType.Entity); disableNode = !CanCharacterBeHeard() || (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); @@ -2617,11 +2610,13 @@ namespace Barotrauma /// /// Create order nodes based on the item context /// + /// + /// The order giver doesn't need to be set for the Order instances as it will be set when the node button is clicked. + /// private void CreateContextualOrderNodes() { if (contextualOrders.None()) { - string orderIdentifier; // Check if targeting an item or a hull if (itemContext != null && itemContext.IsPlayerTeamInteractable) { @@ -2636,66 +2631,62 @@ namespace Barotrauma { if (p.TargetItemsMatchItem(itemContext, option)) { - contextualOrders.Add(new Order(p, itemContext, targetComponent, Character.Controlled).WithOption(option)); + contextualOrders.Add(new Order(p, option, itemContext, targetComponent)); } } } else if (p.TargetItemsMatchItem(itemContext) || p.TryGetTargetItemComponent(itemContext, out targetComponent)) { contextualOrders.Add(p.HasOptions ? - new Order(p, null, orderGiver: Character.Controlled) : - new Order(p, itemContext, targetComponent, Character.Controlled)); + p.CreateInstance(OrderPrefab.OrderTargetType.Entity) : + new Order(p, itemContext, targetComponent)); } } // If targeting a periscope connected to a turret, show the 'operateweapons' order - orderIdentifier = "operateweapons"; - var operateWeaponsPrefab = OrderPrefab.Prefabs[orderIdentifier]; - if (contextualOrders.None(o => o.Identifier == orderIdentifier) && itemContext.Components.Any(c => c is Controller)) + var operateWeaponsPrefab = OrderPrefab.Prefabs["operateweapons"]; + if (contextualOrders.None(o => o.Identifier == "operateweapons") && itemContext.Components.Any(c => c is Controller)) { var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)); if (turret != null) { - contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); + contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret)); } } // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order - orderIdentifier = "repairsystems"; - if (contextualOrders.None(order => order.Identifier == orderIdentifier) && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) + if (contextualOrders.None(order => order.Identifier == "repairsystems") && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold)) { if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null)); } else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null)); } else { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairsystems"], itemContext, targetItem: null)); } } // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) - orderIdentifier = "pumpwater"; - if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals(orderIdentifier)) is Order pumpOrder && + if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals("pumpwater")) is Order pumpOrder && itemContext.Components.FirstOrDefault(c => c.GetType() == pumpOrder.ItemComponentType) is Pump pump && pump.IsAutoControlled) { contextualOrders.Remove(pumpOrder); } - orderIdentifier = "cleanupitems"; - if (contextualOrders.None(info => info.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(info => info.Identifier.Equals("cleanupitems"))) { if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], itemContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["cleanupitems"], itemContext, targetItem: null)); } } AddIgnoreOrder(itemContext); } else if (hullContext != null) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null, Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null)); if (wallContext != null) { AddIgnoreOrder(wallContext); @@ -2729,26 +2720,23 @@ namespace Barotrauma } } } - orderIdentifier = "wait"; - if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals("wait"))) { Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], new OrderTarget(position, hull), Character.Controlled)); + contextualOrders.Add(new Order(OrderPrefab.Prefabs["wait"], new OrderTarget(position, hull))); } if (contextualOrders.None(order => order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) { - orderIdentifier = "follow"; - if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(order => order.Identifier.Equals("follow"))) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); + contextualOrders.Add(OrderPrefab.Prefabs["follow"].CreateInstance(OrderPrefab.OrderTargetType.Entity)); } } // Show 'dismiss' order only when there are crew members with active orders - orderIdentifier = "dismissed"; - if (contextualOrders.None(order => order.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) + if (contextualOrders.None(order => order.IsDismissal) && characters.Any(c => !c.IsDismissed)) { - contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], null, orderGiver: Character.Controlled)); + contextualOrders.Add(OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity)); } } contextualOrders.RemoveAll(o => !IsOrderAvailable(o)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs similarity index 100% rename from Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Data/CampaignMetadata.cs rename to Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/CampaignMetadata.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs new file mode 100644 index 000000000..8c8832a05 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/Data/Wallet.cs @@ -0,0 +1,26 @@ +namespace Barotrauma +{ + internal partial class Wallet + { + public bool IsOwnWallet => + GameMain.GameSession?.Campaign switch + { + null => false, + SinglePlayerCampaign spCampaign => this == spCampaign.Bank, + MultiPlayerCampaign mpCampaign => this == mpCampaign.PersonalWallet, + _ => false + }; + + partial void SettingsChanged(Option balanceChanged, Option rewardChanged) + { + CampaignMode campaign = GameMain.GameSession?.Campaign; + WalletChangedData data = new WalletChangedData + { + BalanceChanged = balanceChanged, + RewardDistributionChanged = rewardChanged, + }; + + campaign?.OnMoneyChanged.Invoke(new WalletChangedEvent(this, data, CreateWalletInfo())); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 9f8368a3b..7186c3bd9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -63,6 +63,8 @@ namespace Barotrauma } } + public virtual Wallet Wallet => GetWallet(); + public override void ShowStartMessage() { foreach (Mission mission in Missions.ToList()) @@ -310,7 +312,7 @@ namespace Barotrauma if (ShowCampaignUI || ForceMapUI) { campaignUIContainer?.AddToGUIUpdateList(); - if (CampaignUI?.UpgradeStore?.HoveredItem != null) + if (CampaignUI?.UpgradeStore?.HoveredEntity != null) { if (CampaignUI.SelectedTab != InteractionType.Upgrade) { return; } CampaignUI?.UpgradeStore?.ItemInfoFrame.AddToGUIUpdateList(order: 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index d628ec7fc..597939e79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -21,7 +21,7 @@ namespace Barotrauma private UInt16 pendingSaveID = 1; public UInt16 PendingSaveID { - get + get { return pendingSaveID; } @@ -34,6 +34,14 @@ namespace Barotrauma } } + public Wallet PersonalWallet => Character.Controlled?.Wallet ?? Wallet.Invalid; + public override Wallet Wallet => GetWallet(); + + public override Wallet GetWallet(Client client = null) + { + return PersonalWallet; + } + public static void StartCampaignSetup(IEnumerable saveFiles) { var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; @@ -195,6 +203,11 @@ namespace Barotrauma yield return CoroutineStatus.Running; } + if (GameMain.Client == null) + { + yield return CoroutineStatus.Failure; + } + if (GameMain.Client.LateCampaignJoin) { GameMain.Client.LateCampaignJoin = false; @@ -618,7 +631,6 @@ namespace Barotrauma bool forceMapUI = msg.ReadBoolean(); - int money = msg.ReadInt32(); bool purchasedHullRepairs = msg.ReadBoolean(); bool purchasedItemRepairs = msg.ReadBoolean(); bool purchasedLostShuttles = msg.ReadBoolean(); @@ -812,12 +824,10 @@ namespace Barotrauma GameMain.NetLobbyScreen.ToggleCampaignMode(true); } - bool shouldRefresh = campaign.Money != money || - campaign.PurchasedHullRepairs != purchasedHullRepairs || + bool shouldRefresh = campaign.PurchasedHullRepairs != purchasedHullRepairs || campaign.PurchasedItemRepairs != purchasedItemRepairs || campaign.PurchasedLostShuttles != purchasedLostShuttles; - campaign.Money = money; campaign.PurchasedHullRepairs = purchasedHullRepairs; campaign.PurchasedItemRepairs = purchasedItemRepairs; campaign.PurchasedLostShuttles = purchasedLostShuttles; @@ -896,6 +906,43 @@ namespace Barotrauma } } + public void ClientReadMoney(IReadMessage inc) + { + NetWalletUpdate update = INetSerializableStruct.Read(inc); + foreach (NetWalletTransaction transaction in update.Transactions) + { + WalletInfo info = transaction.Info; + switch (transaction.CharacterID) + { + case Some { Value: var charID}: + { + Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); + if (targetCharacter is null) { break; } + Wallet wallet = targetCharacter.Wallet; + + wallet.Balance = info.Balance; + wallet.RewardDistribution = info.RewardDistribution; + TryInvokeEvent(wallet, transaction.ChangedData, info); + break; + } + case None _: + { + Bank.Balance = info.Balance; + TryInvokeEvent(Bank, transaction.ChangedData, info); + break; + } + } + } + + void TryInvokeEvent(Wallet wallet, WalletChangedData data, WalletInfo info) + { + if (data.BalanceChanged.IsSome() || data.RewardDistributionChanged.IsSome()) + { + OnMoneyChanged.Invoke(new WalletChangedEvent(wallet, data, info)); + } + } + } + public override void Save(XElement element) { //do nothing, the clients get the save files from the server @@ -908,10 +955,10 @@ namespace Barotrauma string gamesessionDocPath = Path.Combine(SaveUtil.TempPath, "gamesession.xml"); XDocument doc = XMLExtensions.TryLoadXml(gamesessionDocPath); - if (doc == null) + if (doc == null) { DebugConsole.ThrowError($"Failed to load the state of a multiplayer campaign. Could not open the file \"{gamesessionDocPath}\"."); - return; + return; } Load(doc.Root.Element("MultiPlayerCampaign")); GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index fb1181c12..7f8e0c43f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -108,6 +109,9 @@ namespace Barotrauma case "pets": petsElement = subElement; break; + case Wallet.LowerCaseSaveElementName: + Bank = new Wallet(subElement); + break; case "stats": LoadStats(subElement); break; @@ -121,7 +125,15 @@ namespace Barotrauma InitUI(); - Money = element.GetAttributeInt("money", 0); + int oldMoney = element.GetAttributeInt("money", 0); + if (oldMoney > 0) + { + Bank = new Wallet + { + Balance = oldMoney + }; + } + PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); @@ -729,7 +741,6 @@ namespace Barotrauma public override void Save(XElement element) { XElement modeElement = new XElement("SinglePlayerCampaign", - new XAttribute("money", Money), new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), @@ -756,13 +767,14 @@ namespace Barotrauma petsElement = new XElement("pets"); PetBehavior.SavePets(petsElement); - modeElement.Add(petsElement); + modeElement.Add(petsElement); CrewManager.Save(modeElement); CampaignMetadata.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); UpgradeManager?.Save(modeElement); + modeElement.Add(Bank.Save()); element.Add(modeElement); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 2cdcec576..725d24de9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -24,7 +24,7 @@ namespace Barotrauma public TestGameMode(GameModePreset preset) : base(preset) { - foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs.OrderBy(p => p.Identifier)) { for (int i = 0; i < jobPrefab.InitialCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 14daa8a4a..0f794fa40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -190,7 +190,7 @@ namespace Barotrauma } else { - tabMenu.Update(); + tabMenu.Update(deltaTime); if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs index 26f8f67b8..0811b26ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -181,6 +181,11 @@ namespace Barotrauma } } + private void OnMoneyChanged(WalletChangedEvent e) + { + if (e.Wallet.IsOwnWallet) { OnUpdate?.Invoke(); } + } + // if you have more than 5000 ping there are probably more important things to worry about but hey just in case private static DateTimeOffset GetTimeout() => DateTimeOffset.Now.AddSeconds(5).AddMilliseconds(GetPing()); @@ -241,6 +246,7 @@ namespace Barotrauma private void HealRequestReceived(IReadMessage inc) { NetHealRequest request = INetSerializableStruct.Read(inc); + if (request.Result == HealRequestResult.Success) { HealAllPending(force: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 1a0f12157..a25c7787f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; @@ -15,7 +16,6 @@ namespace Barotrauma private int jobColumnWidth, characterColumnWidth, statusColumnWidth; - private readonly SubmarineInfo sub; private readonly List selectedMissions; private readonly Location startLocation, endLocation; @@ -30,11 +30,8 @@ namespace Barotrauma public GUIComponent Frame { get; private set; } - - - public RoundSummary(SubmarineInfo sub, GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) + public RoundSummary(GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) { - this.sub = sub; this.gameMode = gameMode; this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; @@ -320,6 +317,16 @@ namespace Barotrauma { LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); + if (Character.Controlled is { } controlled) + { + var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); + if (share > 0) + { + string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); + RichString yourShareString = RichString.Rich(TextManager.GetWithVariables("crewwallet.missionreward.get", ("[money]", $"{shareFormatted}"), ("[share]", $"{percentage}"))); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), yourShareString); + } + } } if (displayedMission != missionsToDisplay.Last()) @@ -407,7 +414,7 @@ namespace Barotrauma CreatePathUnlockElement(locationFrame, null, startLocation); } - foreach (Faction faction in campaignMode.Factions) + foreach (Faction faction in campaignMode.Factions.OrderBy(f => f.Prefab.MenuOrder).ThenBy(f => f.Prefab.Name)) { float initialReputation = faction.Reputation.Value; if (initialFactionReputations.ContainsKey(faction)) @@ -728,7 +735,7 @@ namespace Barotrauma if (Math.Abs(reputationChange) > 0) { string changeText = $"{(reputationChange > 0 ? "+" : "") + reputationChange}"; - string colorStr = XMLExtensions.ColorToString(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); + string colorStr = XMLExtensions.ToStringHex(reputationChange > 0 ? GUIStyle.Green : GUIStyle.Red); var richText = RichString.Rich($"{reputationText} (‖color:{colorStr}‖{changeText}‖color:end‖)"); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), sliderHolder.RectTransform), richText, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 5ed620e91..2b372fd27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -283,9 +283,9 @@ namespace Barotrauma.Items.Components } } - public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public override void ClientEventRead(IReadMessage msg, float sendingTime) { - base.ClientRead(type, msg, sendingTime); + base.ClientEventRead(msg, sendingTime); bool open = msg.ReadBoolean(); bool broken = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index b87d69d5e..1684642d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { CurrPowerConsumption = powerConsumption; charging = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 33dafbe91..590e908f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Tainted = msg.ReadBoolean(); if (Tainted) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index 67ce2148e..d903016ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components private readonly object mutex = new object(); - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Health = msg.ReadRangedSingle(0, MaxHealth, 8); int startOffset = msg.ReadRangedInteger(-1, MaximumVines); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 47a8e6089..77da549e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Diagnostics.Tracing; namespace Barotrauma.Items.Components { @@ -58,18 +59,23 @@ namespace Barotrauma.Items.Components GUI.DrawRectangle(spriteBatch, new Vector2(attachPos.X - 2, -attachPos.Y - 2), Vector2.One * 5, GUIStyle.Red, thickness: 3); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { if (!attachable || body == null) { return; } + + var eventData = ExtractEventData(extraData); - Vector2 attachPos = (Vector2)extraData[2]; + Vector2 attachPos = eventData.AttachPos; msg.Write(attachPos.X); msg.Write(attachPos.Y); } - public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public override void ClientEventRead(IReadMessage msg, float sendingTime) { - base.ClientRead(type, msg, sendingTime); + base.ClientEventRead(msg, sendingTime); bool readAttachData = msg.ReadBoolean(); if (!readAttachData) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index b68703a72..ad2a8cdf2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -96,7 +96,7 @@ namespace Barotrauma.Items.Components ContentXElement getElementFromList(List list, int index) => CharacterInfo.IsValidIndex(index, list) ? list[index] - : characterInfo.GetRandomElement(list); + : null; var disguisedHairElement = getElementFromList(disguisedHairs, disguisedHairIndex); var disguisedBeardElement = getElementFromList(disguisedBeards, disguisedBeardIndex); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 595945e37..b153c5b80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -566,14 +566,14 @@ namespace Barotrauma.Items.Components protected virtual void CreateGUI() { } //Starts a coroutine that will read the correct state of the component from the NetBuffer when correctionTimer reaches zero. - protected void StartDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false) + protected void StartDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync = false) { - if (delayedCorrectionCoroutine != null) CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); + if (delayedCorrectionCoroutine != null) { CoroutineManager.StopCoroutines(delayedCorrectionCoroutine); } - delayedCorrectionCoroutine = CoroutineManager.StartCoroutine(DoDelayedCorrection(type, buffer, sendingTime, waitForMidRoundSync)); + delayedCorrectionCoroutine = CoroutineManager.StartCoroutine(DoDelayedCorrection(buffer, sendingTime, waitForMidRoundSync)); } - private IEnumerable DoDelayedCorrection(ServerNetObject type, IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) + private IEnumerable DoDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync) { while (GameMain.Client != null && (correctionTimer > 0.0f || (waitForMidRoundSync && GameMain.Client.MidRoundSyncing))) @@ -587,7 +587,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - ((IServerSerializable)this).ClientRead(type, buffer, sendingTime); + ((IServerSerializable)this).ClientEventRead(buffer, sendingTime); correctionTimer = 0.0f; delayedCorrectionCoroutine = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 076f6621b..7c7821844 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -282,7 +282,7 @@ namespace Barotrauma.Items.Components textBlock.DrawManually(spriteBatch); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Text = msg.ReadString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs index b16765b69..bcef57295 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LevelResource.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class LevelResource : ItemComponent, IServerSerializable { - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { deattachTimer = msg.ReadSingle(); if (deattachTimer >= DeattachDuration) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 448c56945..52cf7fef1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -56,8 +56,9 @@ namespace Barotrauma.Items.Components } else { - Light.Position = item.DrawPosition; - if (item.Submarine != null) { Light.Position -= item.Submarine.DrawPosition; } + Vector2 pos = item.DrawPosition; + if (item.Submarine != null) { pos -= item.Submarine.DrawPosition; } + Light.Position = pos; } PhysicsBody body = Light.ParentBody; if (body != null) @@ -120,7 +121,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { IsActive = msg.ReadBoolean(); lastReceivedState = IsActive; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 0eb2cbf58..df20deb01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components } #endif - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { State = msg.ReadBoolean(); ushort userID = msg.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 67cc5fe8b..85791f95b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -251,12 +251,12 @@ namespace Barotrauma.Items.Components return true; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(pendingState); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { ushort userID = msg.ReadUInt16(); Character user = userID == Entity.NullEntityID ? null : Entity.FindEntityByID(userID) as Character; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 89280fa88..e9406abde 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -156,17 +156,17 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { //targetForce can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(5 + 16), sendingTime); + StartDelayedCorrection(msg.ExtractBits(5 + 16), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 4a3da093d..7d2a5101f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -705,13 +705,13 @@ namespace Barotrauma.Items.Components requiredTimeBlock.Text = ToolBox.SecondsToReadableTime(timeUntilReady > 0.0f ? timeUntilReady : requiredTime); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; msg.Write(recipeHash); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { FabricatorState newState = (FabricatorState)msg.ReadByte(); float newTimeUntilReady = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index c65e6c41a..e50bbcc8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -216,14 +216,14 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); msg.Write(IsActive); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; @@ -244,7 +244,7 @@ namespace Barotrauma.Items.Components { int msgLength = msg.BitPosition - msgStartPos; msg.BitPosition = msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 6627d8491..cfe675dbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -608,9 +608,10 @@ namespace Barotrauma.Items.Components Vector2 pointerPos = pos - new Vector2(0, 30) * scale; + float scaleMultiplier = 0.95f; if (optimalRangeNormalized.X == optimalRangeNormalized.Y) { - sectorSprite.Draw(spriteBatch, pointerPos, GUIStyle.Red, MathHelper.PiOver2, scale); + sectorSprite.Draw(spriteBatch, pointerPos, GUIStyle.Red, MathHelper.PiOver2, scale * scaleMultiplier); } else { @@ -619,7 +620,6 @@ namespace Barotrauma.Items.Components spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, GameMain.GraphicsWidth, (int)(pointerPos.Y + (meterSprite.size.Y - meterSprite.Origin.Y) * scale) - 3); spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); - float scaleMultiplier = 0.95f; sectorSprite.Draw(spriteBatch, pointerPos, optimalRangeColor, MathHelper.PiOver2 + (allowedSectorRad.X + allowedSectorRad.Y) / 2.0f, scale * scaleMultiplier); sectorSprite.Draw(spriteBatch, pointerPos, offRangeColor, optimalSectorRad.X, scale * scaleMultiplier); sectorSprite.Draw(spriteBatch, pointerPos, warningColor, allowedSectorRad.X, scale * scaleMultiplier); @@ -711,7 +711,7 @@ namespace Barotrauma.Items.Components tempRangeIndicator?.Remove(); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(autoTemp); msg.Write(PowerOn); @@ -721,11 +721,11 @@ namespace Barotrauma.Items.Components correctionTimer = CorrectionDelay; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime); + StartDelayedCorrection(msg.ExtractBits(1 + 1 + 8 + 8 + 8 + 8), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 8b549c0b8..90f0e04b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -766,7 +766,7 @@ namespace Barotrauma.Items.Components if (activePingsCount == 0) { disruptedDirections.Clear(); } foreach (AITarget t in AITarget.List) { - if (t.Entity is Character c && c.Params.HideInSonar) { continue; } + if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); @@ -1264,7 +1264,7 @@ namespace Barotrauma.Items.Components } foreach (AITarget aiTarget in AITarget.List) { - float disruption = aiTarget.Entity is Character c ? c.Params.SonarDisruption : aiTarget.SonarDisruption; + float disruption = aiTarget.Entity is Character c && !c.IsUnconscious ? c.Params.SonarDisruption : aiTarget.SonarDisruption; if (disruption <= 0.0f || aiTarget.InDetectable) { continue; } float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } @@ -1413,7 +1413,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } - if (c.Params.HideInSonar) { continue; } + if (!c.IsUnconscious && c.Params.HideInSonar) { continue; } if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (c.AnimController.SimplePhysicsEnabled) @@ -1742,7 +1742,7 @@ namespace Barotrauma.Items.Components MineralClusters = null; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(currentMode == Mode.Active); if (currentMode == Mode.Active) @@ -1758,7 +1758,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; @@ -1782,7 +1782,7 @@ namespace Barotrauma.Items.Components { int msgLength = msg.BitPosition - msgStartPos; msg.BitPosition = msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 2d2786db4..2fbf7b38d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -822,7 +822,7 @@ namespace Barotrauma.Items.Components Connection dockingConnection = item.Connections?.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { - connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true); + connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, ignoreInactiveRelays: true, allowTraversingBackwards: false); } checkConnectedPortsTimer = CheckConnectedPortsInterval; } @@ -894,7 +894,7 @@ namespace Barotrauma.Items.Components pathFinder = null; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write(AutoPilot); msg.Write(dockingNetworkMessagePending); @@ -921,7 +921,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int msgStartPos = msg.BitPosition; @@ -962,7 +962,7 @@ namespace Barotrauma.Items.Components { int msgLength = (int)(msg.BitPosition - msgStartPos); msg.BitPosition = msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 2a011d6df..cd6883441 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -163,16 +163,16 @@ namespace Barotrauma.Items.Components indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f); } - public void ClientWrite(IWriteMessage msg, object[] extraData) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData) { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(4 + 8), sendingTime); + StartDelayedCorrection(msg.ExtractBits(4 + 8), sendingTime); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index bd467a7b8..8523f56f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components { private readonly List particleEmitters = new List(); - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool launch = msg.ReadBoolean(); if (launch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 1a1497aba..87a2c21e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -427,7 +427,7 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { deteriorationTimer = msg.ReadSingle(); deteriorateAlwaysResetTimer = msg.ReadSingle(); @@ -446,7 +446,7 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)requestStartFixAction, 0, 2); msg.Write(qteSuccess); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 783864f76..2dc8ec191 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -196,7 +196,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { snapped = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs index 865f0915d..765d00bff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool wasScanCompletedPreviously = IsScanCompleted; scanTimer = msg.ReadSingle(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index 2202530c8..89d1a7583 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -76,13 +76,14 @@ namespace Barotrauma.Items.Components UserData = i, OnClicked = (button, userData) => { + int signalIndex = (int)userData; if (GameMain.IsSingleplayer) { - SendSignal((int)userData, Character.Controlled); + SendSignal(signalIndex, Character.Controlled); } else { - item.CreateClientEvent(this, new object[] { userData }); + item.CreateClientEvent(this, new EventData(signalIndex)); } return true; } @@ -104,12 +105,12 @@ namespace Barotrauma.Items.Components Container.Inventory.RectTransform = containerHolder.RectTransform; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { Write(msg, extraData); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, isServerMessage: true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index 057679a25..0d026f53b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { if (GameMain.Client.MidRoundSyncing) { @@ -161,7 +161,7 @@ namespace Barotrauma.Items.Components } int msgLength = (int)(msg.BitPosition - msgStartPos); msg.BitPosition = (int)msgStartPos; - StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime, waitForMidRoundSync: true); + StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime, waitForMidRoundSync: true); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index f22fbcce1..50a7f11d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -137,13 +137,14 @@ namespace Barotrauma.Items.Components }; btn.OnClicked += (_, userdata) => { + CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;; if (GameMain.Client == null) { - ButtonClicked(userdata as CustomInterfaceElement); + ButtonClicked(btnElement); } else { - GameMain.Client.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), userdata as CustomInterfaceElement }); + item.CreateClientEvent(this, new EventData(btnElement)); } return true; }; @@ -301,7 +302,7 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { //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++) @@ -323,12 +324,12 @@ namespace Barotrauma.Items.Components } else { - msg.Write(extraData != null && extraData.Any(d => d as CustomInterfaceElement == customInterfaceElementList[i])); + msg.Write(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); } } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { for (int i = 0; i < customInterfaceElementList.Count; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs index 68e7884e9..2b0aeec29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MemoryComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class MemoryComponent : ItemComponent { - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Value = msg.ReadString(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 7538b6fc4..8c7ef71ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -7,6 +7,16 @@ namespace Barotrauma.Items.Components { partial class Terminal : ItemComponent, IClientSerializable, IServerSerializable { + private readonly struct ClientEventData : IEventData + { + public readonly string Text; + + public ClientEventData(string text) + { + Text = text; + } + } + private GUIListBox historyBox; private GUITextBlock fillerBlock; private GUITextBox inputBox; @@ -42,7 +52,7 @@ namespace Barotrauma.Items.Components } else { - item.CreateClientEvent(this, new object[] { text }); + item.CreateClientEvent(this, new ClientEventData(text)); } textBox.Text = string.Empty; return true; @@ -133,17 +143,15 @@ namespace Barotrauma.Items.Components } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (extraData is null) { return; } - - if (extraData[2] is string str) + if (TryExtractEventData(extraData, out ClientEventData eventData)) { - msg.Write(str); + msg.Write(eventData.Text); } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { SendOutput(msg.ReadString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs index dd8c385eb..64793fa05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/WifiComponent.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components ShapeExtensions.DrawCircle(spriteBatch, pos, range, 32, Color.Cyan * 0.5f, 3); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { Channel = msg.ReadRangedInteger(MinChannel, MaxChannel); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 4e1de585c..fcede0408 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -11,6 +11,16 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable, IClientSerializable { + private readonly struct ClientEventData : IEventData + { + public readonly int NodeCount; + + public ClientEventData(int nodeCount) + { + NodeCount = nodeCount; + } + } + public static Color higlightColor = Color.LightGreen; public static Color editorHighlightColor = Color.Yellow; public static Color editorSelectedColor = Color.Red; @@ -555,7 +565,7 @@ namespace Barotrauma.Items.Components return false; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int eventIndex = msg.ReadRangedInteger(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent)); int nodeCount = msg.ReadRangedInteger(0, MaxNodesPerNetworkEvent); @@ -586,9 +596,13 @@ namespace Barotrauma.Items.Components (item.ParentInventory is CharacterInventory characterInventory && ((characterInventory.Owner as Character)?.HasEquippedItem(item) ?? false)); } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - int nodeCount = (int)extraData[2]; + var eventData = ExtractEventData(extraData); + int nodeCount = eventData.NodeCount; msg.Write((byte)nodeCount); if (nodeCount > 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs index 503c61912..77fe1ba5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/TriggerComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class TriggerComponent : ItemComponent, IServerSerializable { - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { CurrentForceFluctuation = msg.ReadRangedSingle(0.0f, 1.0f, 8); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index ded8496e3..e2df43900 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -44,7 +44,9 @@ namespace Barotrauma.Items.Components private readonly Dictionary widgets = new Dictionary(); private float prevAngle; - + + private float currentBarrelSpin = 0f; + private bool flashLowPower; private bool flashNoAmmo, flashLoaderBroken; private float flashTimer; @@ -716,7 +718,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { UInt16 projectileID = msg.ReadUInt16(); float newTargetRotation = msg.ReadRangedSingle(minRotation, maxRotation, 16); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index 0c31c61a5..c8eb6b435 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool isDocked = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 19ea4b3b9..1c27aaf3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1598,7 +1598,7 @@ namespace Barotrauma int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); if (maxStackSize > 1 || containedItem.Prefab.HideConditionBar) { - containedState = itemContainer.Inventory.slots[0].ItemCount / (float)maxStackSize; + containedState = itemContainer.Inventory.slots[0].Items.Count / (float)maxStackSize; } } } @@ -1702,7 +1702,7 @@ namespace Barotrauma } if (maxStackSize > 1 && inventory != null) { - int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].ItemCount : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count(); + int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].Items.Count : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count(); if (item.IsFullCondition || MathUtils.NearlyEqual(item.Condition, 0.0f) || itemCount > 1) { Vector2 stackCountPos = new Vector2(rect.Right, rect.Bottom); @@ -1795,20 +1795,10 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { UInt16 lastEventID = msg.ReadUInt16(); - byte slotCount = msg.ReadByte(); - receivedItemIDs = new List[slotCount]; - for (int i = 0; i < slotCount; i++) - { - receivedItemIDs[i] = new List(); - int itemCount = msg.ReadRangedInteger(0, MaxStackSize); - for (int j = 0; j < itemCount; j++) - { - receivedItemIDs[i].Add(msg.ReadUInt16()); - } - } + SharedRead(msg, out receivedItemIDs); //delay applying the new state if less than 1 second has passed since this client last sent a state to the server //prevents the inventory from briefly reverting to an old state if items are moved around in quick succession @@ -1895,7 +1885,7 @@ namespace Barotrauma receivedItemIDs = null; } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); syncItemsDelay = 1.0f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 011ec97e9..8cb513864 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -6,6 +6,7 @@ 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; @@ -1262,25 +1263,19 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { - if (type == ServerNetObject.ENTITY_POSITION) - { - ClientReadPosition(type, msg, sendingTime); - return; - } - - NetEntityEvent.Type eventType = - (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); + EventType eventType = + (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); switch (eventType) { - case NetEntityEvent.Type.ComponentState: + case EventType.ComponentState: { int componentIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[componentIndex] is IServerSerializable serverSerializable) { - serverSerializable.ClientRead(type, msg, sendingTime); + serverSerializable.ClientEventRead(msg, sendingTime); } else { @@ -1288,12 +1283,12 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.InventoryState: + case EventType.InventoryState: { int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[containerIndex] is ItemContainer container) { - container.Inventory.ClientRead(type, msg, sendingTime); + container.Inventory.ClientEventRead(msg, sendingTime); } else { @@ -1301,7 +1296,7 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.Status: + case EventType.Status: float prevCondition = condition; condition = msg.ReadSingle(); if (prevCondition > 0.0f && condition <= 0.0f) @@ -1314,10 +1309,10 @@ namespace Barotrauma } SetActiveSprite(); break; - case NetEntityEvent.Type.AssignCampaignInteraction: + case EventType.AssignCampaignInteraction: CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); break; - case NetEntityEvent.Type.ApplyStatusEffect: + case EventType.ApplyStatusEffect: { ActionType actionType = (ActionType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1); byte componentIndex = msg.ReadByte(); @@ -1347,10 +1342,10 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.ChangeProperty: + case EventType.ChangeProperty: ReadPropertyChange(msg, false); break; - case NetEntityEvent.Type.Upgrade: + case EventType.Upgrade: Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); if (UpgradePrefab.Find(identifier) is { } upgradePrefab) @@ -1371,51 +1366,65 @@ namespace Barotrauma AddUpgrade(upgrade, false); } break; - case NetEntityEvent.Type.Invalid: - break; + default: + throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type)) + Exception error(string reason) { - return; + string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}"; + GameAnalyticsManager.AddErrorEventOnce($"Item.ClientWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return new Exception(errorMsg); } + + if (extraData is null) { throw error("event data was null"); } + if (!(extraData is IEventData eventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } - NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0]; - msg.WriteRangedInteger((int)eventType, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - switch (eventType) + EventType eventType = eventData.EventType; + msg.WriteRangedInteger((int)eventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) { - case NetEntityEvent.Type.ComponentState: - int componentIndex = (int)extraData[1]; + case ComponentStateEventData componentStateEventData: + { + var component = componentStateEventData.Component; + if (component is null) { throw error("component was null"); } + if (!(component is IClientSerializable clientSerializable)) { throw error($"component was not {nameof(IClientSerializable)}"); } + int componentIndex = components.IndexOf(component); + if (componentIndex < 0) { throw error("component did not belong to item"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); - (components[componentIndex] as IClientSerializable).ClientWrite(msg, extraData); - break; - case NetEntityEvent.Type.InventoryState: - int containerIndex = (int)extraData[1]; + clientSerializable.ClientEventWrite(msg, extraData); + } + break; + case InventoryStateEventData inventoryStateEventData: + { + var container = inventoryStateEventData.Component; + if (container is null) { throw error("container was null"); } + int containerIndex = components.IndexOf(container); + if (containerIndex < 0) { throw error("container did not belong to item"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); - (components[containerIndex] as ItemContainer).Inventory.ClientWrite(msg, extraData); - break; - case NetEntityEvent.Type.Treatment: - UInt16 characterID = (UInt16)extraData[1]; - Limb targetLimb = (Limb)extraData[2]; + container.Inventory.ClientEventWrite(msg, extraData); + } + break; + case TreatmentEventData treatmentEventData: + Character targetCharacter = treatmentEventData.TargetCharacter; - Character targetCharacter = FindEntityByID(characterID) as Character; - - msg.Write(characterID); - msg.Write(targetCharacter == null ? (byte)255 : (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb)); + msg.Write(targetCharacter.ID); + msg.Write(treatmentEventData.LimbIndex); break; - case NetEntityEvent.Type.ChangeProperty: - WritePropertyChange(msg, extraData, true); + case ChangePropertyEventData changePropertyEventData: + WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: true); editingHUDRefreshTimer = 1.0f; break; - case NetEntityEvent.Type.Combine: - UInt16 combineTargetID = (UInt16)extraData[1]; - msg.Write(combineTargetID); + case CombineEventData combineEventData: + Item combineTarget = combineEventData.CombineTarget; + msg.Write(combineTarget.ID); break; + default: + throw error($"Unsupported event type {eventData.GetType().Name}"); } - msg.WritePadBits(); } partial void UpdateNetPosition(float deltaTime) @@ -1451,7 +1460,7 @@ namespace Barotrauma rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); } - public void ClientReadPosition(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientReadPosition(IReadMessage msg, float sendingTime) { if (body == null) { @@ -1465,7 +1474,7 @@ namespace Barotrauma return; } - var posInfo = body.ClientRead(type, msg, sendingTime, parentDebugName: Name); + var posInfo = body.ClientRead(msg, sendingTime, parentDebugName: Name); msg.ReadPadBits(); if (posInfo != null) { @@ -1510,24 +1519,18 @@ namespace Barotrauma } public void CreateClientEvent(T ic) where T : ItemComponent, IClientSerializable + => CreateClientEvent(ic, null); + + public void CreateClientEvent(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IClientSerializable { - if (GameMain.Client == null) return; + if (GameMain.Client == null) { return; } - int index = components.IndexOf(ic); - if (index == -1) return; + #warning TODO: this should throw an exception + if (!components.Contains(ic)) { return; } - GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ComponentState, index }); - } - - public void CreateClientEvent(T ic, object[] extraData) where T : ItemComponent, IClientSerializable - { - if (GameMain.Client == null) return; - - int index = components.IndexOf(ic); - if (index == -1) return; - - object[] data = new object[] { NetEntityEvent.Type.ComponentState, index }.Concat(extraData).ToArray(); - GameMain.Client.CreateEntityEvent(this, data); + var eventData = new ComponentStateEventData(ic, extraData); + if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } + GameMain.Client.CreateEntityEvent(this, eventData); } public static Item ReadSpawnData(IReadMessage msg, bool spawn = true) @@ -1576,6 +1579,36 @@ namespace Barotrauma bool allowStealing = msg.ReadBoolean(); int quality = msg.ReadRangedInteger(0, Items.Components.Quality.MaxQuality); byte teamID = msg.ReadByte(); + + bool hasIdCard = msg.ReadBoolean(); + string ownerName = "", ownerTags = ""; + int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1; + Color ownerHairColor = Microsoft.Xna.Framework.Color.White, + ownerFacialHairColor = Microsoft.Xna.Framework.Color.White, + ownerSkinColor = Microsoft.Xna.Framework.Color.White; + Identifier ownerJobId = Identifier.Empty; + Vector2 ownerSheetIndex = Vector2.Zero; + if (hasIdCard) + { + ownerName = msg.ReadString(); + ownerTags = msg.ReadString(); + + ownerBeardIndex = msg.ReadByte() - 1; + ownerHairIndex = msg.ReadByte() - 1; + ownerMoustacheIndex = msg.ReadByte() - 1; + ownerFaceAttachmentIndex = msg.ReadByte() - 1; + + ownerHairColor = msg.ReadColorR8G8B8(); + ownerFacialHairColor = msg.ReadColorR8G8B8(); + ownerSkinColor = msg.ReadColorR8G8B8(); + + ownerJobId = msg.ReadIdentifier(); + + int x = msg.ReadByte(); + int y = msg.ReadByte(); + ownerSheetIndex = (x, y); + } + bool tagsChanged = msg.ReadBoolean(); string tags = ""; if (tagsChanged) @@ -1587,6 +1620,7 @@ namespace Barotrauma tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags)); } } + bool isNameTag = msg.ReadBoolean(); string writtenName = ""; if (isNameTag) @@ -1672,6 +1706,17 @@ namespace Barotrauma foreach (IdCard idCard in item.GetComponents()) { idCard.TeamID = (CharacterTeamType)teamID; + idCard.OwnerName = ownerName; + idCard.OwnerTags = ownerTags; + idCard.OwnerBeardIndex = ownerBeardIndex; + idCard.OwnerHairIndex = ownerHairIndex; + idCard.OwnerMoustacheIndex = ownerMoustacheIndex; + idCard.OwnerFaceAttachmentIndex = ownerFaceAttachmentIndex; + idCard.OwnerHairColor = ownerHairColor; + idCard.OwnerFacialHairColor = ownerFacialHairColor; + idCard.OwnerSkinColor = ownerSkinColor; + idCard.OwnerJobId = ownerJobId; + idCard.OwnerSheetIndex = ownerSheetIndex; } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs new file mode 100644 index 000000000..95f314a73 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemEventData.cs @@ -0,0 +1,35 @@ +using System; + +namespace Barotrauma +{ + partial class Item + { + private readonly struct CombineEventData : IEventData + { + public EventType EventType => EventType.Combine; + public readonly Item CombineTarget; + + public CombineEventData(Item combineTarget) + { + CombineTarget = combineTarget; + } + } + + private readonly struct TreatmentEventData : IEventData + { + public EventType EventType => EventType.Treatment; + public readonly Character TargetCharacter; + public readonly Limb TargetLimb; + public byte LimbIndex + => TargetCharacter?.AnimController?.Limbs is { } limbs + ? (byte)Array.IndexOf(limbs, TargetLimb) + : byte.MaxValue; + + public TreatmentEventData(Character targetCharacter, Limb targetLimb) + { + TargetCharacter = targetCharacter; + TargetLimb = targetLimb; + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index b14c94720..665a42380 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -30,7 +30,7 @@ namespace Barotrauma private float serverUpdateDelay; private float remoteWaterVolume, remoteOxygenPercentage; - private List remoteFireSources; + private NetworkFireSource[] remoteFireSources = null; private readonly List remoteBackgroundSections = new List(); private readonly List remoteDecals = new List(); @@ -175,15 +175,16 @@ namespace Barotrauma { if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) { - GameMain.NetworkMember?.CreateEntityEvent(this); + GameMain.NetworkMember?.CreateEntityEvent(this, new StatusEventData()); } foreach (Decal decal in pendingDecalUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); + GameMain.NetworkMember?.CreateEntityEvent(this, new DecalEventData(decal)); } + pendingDecalUpdates.Clear(); foreach (int pendingSectionUpdate in pendingSectionUpdates) { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); + GameMain.NetworkMember?.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } pendingSectionUpdates.Clear(); networkUpdatePending = false; @@ -595,132 +596,101 @@ namespace Barotrauma } } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { - if (extraData == null) - { - msg.WriteRangedInteger(0, 0, 2); - msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed hull event: expected {nameof(Hull)}.{nameof(IEventData)}"); } - msg.Write(FireSources.Count > 0); - if (FireSources.Count > 0) - { - msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); - for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) - { - var fireSource = FireSources[i]; - Vector2 normalizedPos = new Vector2( - (fireSource.Position.X - rect.X) / rect.Width, - (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); - - msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); - msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); - } - } - } - else if (extraData[0] is Decal decal) + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) { - msg.WriteRangedInteger(1, 0, 2); - int decalIndex = decals.IndexOf(decal); - msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex)); - msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); - } - else - { - msg.WriteRangedInteger(2, 0, 2); - int sectorToUpdate = (int)extraData[0]; - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); - msg.Write(BackgroundSections[i].Color.PackedValue); - } + case StatusEventData statusEventData: + SharedStatusWrite(msg); + break; + case BackgroundSectionsEventData backgroundSectionsEventData: + SharedBackgroundSectionsWrite(msg, backgroundSectionsEventData); + break; + case DecalEventData decalEventData: + var decal = decalEventData.Decal; + int decalIndex = decals.IndexOf(decal); + msg.Write((byte)(decalIndex < 0 ? 255 : decalIndex)); + msg.WriteRangedSingle(decal.BaseAlpha, 0.0f, 1.0f, 8); + break; + default: + throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}"); } } - public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { - bool isBallastFloraUpdate = message.ReadBoolean(); - if (isBallastFloraUpdate) + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) { - BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) message.ReadByte(); - if (header == BallastFloraBehavior.NetworkHeader.Spawn) - { - Identifier identifier = message.ReadIdentifier(); - float x = message.ReadSingle(); - float y = message.ReadSingle(); - BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) - { - PowerConsumptionTimer = message.ReadSingle() - }; - } - else - { - BallastFlora?.ClientRead(message, header); - } - return; - } - remoteWaterVolume = message.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; - remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); - - bool hasFireSources = message.ReadBoolean(); - remoteFireSources = new List(); - if (hasFireSources) - { - int fireSourceCount = message.ReadRangedInteger(0, 16); - for (int i = 0; i < fireSourceCount; i++) - { - remoteFireSources.Add(new Vector3( - MathHelper.Clamp(message.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - MathHelper.Clamp(message.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - message.ReadRangedSingle(0.0f, 1.0f, 8))); - } - } - - bool hasExtraData = message.ReadBoolean(); - if (hasExtraData) - { - bool hasSectionUpdate = message.ReadBoolean(); - if (hasSectionUpdate) - { - int sectorToUpdate = message.ReadRangedInteger(0, BackgroundSections.Count - 1); - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - float colorStrength = message.ReadRangedSingle(0.0f, 1.0f, 8); - Color color = new Color(message.ReadUInt32()); - var remoteBackgroundSection = remoteBackgroundSections.Find(s => s.Index == i); - if (remoteBackgroundSection != null) + case EventType.Status: + remoteOxygenPercentage = msg.ReadRangedSingle(0.0f, 100.0f, 8); + + SharedStatusRead( + msg, + out float newWaterVolume, + out NetworkFireSource[] newFireSources); + + remoteWaterVolume = newWaterVolume; + remoteFireSources = newFireSources; + break; + case EventType.BackgroundSections: + SharedBackgroundSectionRead( + msg, + bsnu => { - remoteBackgroundSection.SetColorStrength(colorStrength); - remoteBackgroundSection.SetColor(color); - } - else - { - remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), (ushort)i, colorStrength, color, 0)); - } - } + int i = bsnu.SectionIndex; + Color color = bsnu.Color; + float colorStrength = bsnu.ColorStrength; + + var remoteBackgroundSection = remoteBackgroundSections.Find(s => s.Index == i); + if (remoteBackgroundSection != null) + { + remoteBackgroundSection.SetColorStrength(colorStrength); + remoteBackgroundSection.SetColor(color); + } + else + { + remoteBackgroundSections.Add(new BackgroundSection(new Rectangle(0, 0, 1, 1), (ushort)i, colorStrength, color, 0)); + } + }, out _); paintAmount = BackgroundSections.Sum(s => s.ColorStrength); - } - else - { - int decalCount = message.ReadRangedInteger(0, MaxDecalsPerHull); + break; + case EventType.Decal: + int decalCount = msg.ReadRangedInteger(0, MaxDecalsPerHull); if (decalCount == 0) { decals.Clear(); } remoteDecals.Clear(); for (int i = 0; i < decalCount; i++) { - UInt32 decalId = message.ReadUInt32(); - int spriteIndex = message.ReadByte(); - float normalizedXPos = message.ReadRangedSingle(0.0f, 1.0f, 8); - float normalizedYPos = message.ReadRangedSingle(0.0f, 1.0f, 8); - float decalScale = message.ReadRangedSingle(0.0f, 2.0f, 12); + UInt32 decalId = msg.ReadUInt32(); + int spriteIndex = msg.ReadByte(); + float normalizedXPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); + float normalizedYPos = msg.ReadRangedSingle(0.0f, 1.0f, 8); + float decalScale = msg.ReadRangedSingle(0.0f, 2.0f, 12); remoteDecals.Add(new RemoteDecal(decalId, spriteIndex, new Vector2(normalizedXPos, normalizedYPos), decalScale)); } - } + break; + case EventType.BallastFlora: + BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) msg.ReadByte(); + if (header == BallastFloraBehavior.NetworkHeader.Spawn) + { + Identifier identifier = msg.ReadIdentifier(); + float x = msg.ReadSingle(); + float y = msg.ReadSingle(); + BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) + { + PowerConsumptionTimer = msg.ReadSingle() + }; + } + else + { + BallastFlora?.ClientRead(msg, header); + } + break; + default: + throw new Exception($"Malformed incoming hull event: {eventType} is not a supported event type"); } if (serverUpdateDelay > 0.0f) { return; } @@ -756,17 +726,15 @@ namespace Barotrauma remoteDecals.Clear(); } - if (remoteFireSources == null) { return; } - + if (remoteFireSources is null) { return; } + WaterVolume = remoteWaterVolume; OxygenPercentage = remoteOxygenPercentage; - for (int i = 0; i < remoteFireSources.Count; i++) + for (int i = 0; i < remoteFireSources.Length; i++) { - Vector2 pos = new Vector2( - rect.X + rect.Width * remoteFireSources[i].X, - rect.Y - rect.Height + (rect.Height * remoteFireSources[i].Y)); - float size = remoteFireSources[i].Z * rect.Width; + Vector2 pos = remoteFireSources[i].Position; + float size = remoteFireSources[i].Size; var newFire = i < FireSources.Count ? FireSources[i] : @@ -782,7 +750,7 @@ namespace Barotrauma } } - for (int i = FireSources.Count - 1; i >= remoteFireSources.Count; i--) + for (int i = FireSources.Count - 1; i >= remoteFireSources.Length; i--) { FireSources[i].Remove(); if (i < FireSources.Count) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 87c8a8a64..67a6e6fa2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -121,34 +121,42 @@ namespace Barotrauma { renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { - bool isGlobalUpdate = msg.ReadBoolean(); - if (isGlobalUpdate) + EventType eventType = (EventType)msg.ReadByte(); + + switch (eventType) { - foreach (LevelWall levelWall in ExtraWalls) + case EventType.GlobalDestructibleWall: { - if (levelWall.Body.BodyType == BodyType.Static) { continue; } - - Vector2 bodyPos = new Vector2( - msg.ReadSingle(), - msg.ReadSingle()); - levelWall.MoveState = msg.ReadRangedSingle(0.0f, MathHelper.TwoPi, 16); - DestructibleLevelWall destructibleWall = levelWall as DestructibleLevelWall; - if (Vector2.DistanceSquared(bodyPos, levelWall.Body.Position) > 0.5f && (destructibleWall == null || !destructibleWall.Destroyed)) + foreach (LevelWall levelWall in ExtraWalls) { - levelWall.Body.SetTransformIgnoreContacts(ref bodyPos, levelWall.Body.Rotation); + if (levelWall.Body.BodyType == BodyType.Static) { continue; } + + Vector2 bodyPos = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + levelWall.MoveState = msg.ReadRangedSingle(0.0f, MathHelper.TwoPi, 16); + if (Vector2.DistanceSquared(bodyPos, levelWall.Body.Position) > 0.5f + && !(levelWall is DestructibleLevelWall { Destroyed: true })) + { + levelWall.Body.SetTransformIgnoreContacts(ref bodyPos, levelWall.Body.Rotation); + } } } - } - else - { - int index = msg.ReadUInt16(); - byte damageByte = msg.ReadByte(); - if (index < ExtraWalls.Count && ExtraWalls[index] is DestructibleLevelWall destructibleWall) + break; + case EventType.SingleDestructibleWall: { - destructibleWall.SetDamage(destructibleWall.MaxHealth * damageByte / 255.0f); + int index = msg.ReadUInt16(); + float damageByte = msg.ReadByte(); + if (index < ExtraWalls.Count && ExtraWalls[index] is DestructibleLevelWall destructibleWall) + { + destructibleWall.SetDamage(destructibleWall.MaxHealth * damageByte / 255.0f); + } } + break; + default: + throw new Exception($"Malformed incoming level event: {eventType} is not a supported event type"); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 76bf3c231..bdf919218 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -229,7 +229,7 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { int objIndex = msg.ReadRangedInteger(0, objects.Count); objects[objIndex].ClientRead(msg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index 71df6f48a..ea9eebd9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -106,7 +106,8 @@ namespace Barotrauma WaterEffect.Parameters["xBlurDistance"].SetValue(BlurAmount / 100.0f); } else - { WaterEffect.CurrentTechnique = WaterEffect.Techniques["WaterShader"]; + { + WaterEffect.CurrentTechnique = WaterEffect.Techniques["WaterShader"]; } Vector2 offset = WavePos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 420a52d66..d1764fd10 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -188,8 +188,10 @@ namespace Barotrauma.Lights if (light.ParentBody != null) { light.ParentBody.UpdateDrawPosition(); - light.Position = light.ParentBody.DrawPosition; - if (light.ParentSub != null) { light.Position -= light.ParentSub.DrawPosition; } + + Vector2 pos = light.ParentBody.DrawPosition; + if (light.ParentSub != null) { pos -= light.ParentSub.DrawPosition; } + light.Position = pos; } float range = light.LightSourceParams.TextureRange; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 0f85e7fda..92112a32a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -70,6 +70,8 @@ namespace Barotrauma private (SubmarineInfo pendingSub, float realWorldCrushDepth) pendingSubInfo; + private RichString beaconStationActiveText, beaconStationInactiveText; + /*private (Rectangle targetArea, string tip)? connectionTooltip; private string sanitizedConnectionTooltip; private List connectionTooltipRichTextData; @@ -153,6 +155,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Could not find campaign map sprites for the biome \"{missingBiome.Identifier}\". Using the sprites of the first biome instead..."); } + beaconStationActiveText = RichString.Rich(TextManager.Get("BeaconStationActiveTooltip")); + beaconStationInactiveText = RichString.Rich(TextManager.Get("BeaconStationInactiveTooltip")); + RemoveFogOfWar(StartLocation); GenerateLocationConnectionVisuals(); @@ -619,7 +624,7 @@ namespace Barotrauma if (Vector2.Distance(PlayerInput.MousePosition, typeChangeIconPos) < generationParams.TypeChangeIcon.SourceRect.Width * zoom && (tooltip == null || IsPreferredTooltip(typeChangeIconPos))) { - tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), location.LastTypeChangeMessage); + tooltip = (new Rectangle(typeChangeIconPos.ToPoint(), new Point(30)), RichString.Rich(location.LastTypeChangeMessage)); } } if (location != CurrentLocation && generationParams.MissionIcon != null) @@ -920,7 +925,7 @@ namespace Barotrauma if (connection.LevelData.HasBeaconStation) { var beaconStationIconStyle = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; - DrawIcon(beaconStationIconStyle, (int)(28 * zoom), TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip")); + DrawIcon(beaconStationIconStyle, (int)(28 * zoom), connection.LevelData.IsBeaconActive ? beaconStationActiveText : beaconStationInactiveText); } if (connection.Locked) @@ -942,9 +947,9 @@ namespace Barotrauma DrawIcon( "LockedLocationConnection", (int)(28 * zoom), - TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", + RichString.Rich(TextManager.GetWithVariables(unlockEvent.UnlockPathTooltip ?? "LockedPathTooltip", ("[requiredreputation]", Reputation.GetFormattedReputationText(MathUtils.InverseLerp(unlockReputation.MinReputation, unlockReputation.MaxReputation, unlockEvent.UnlockPathReputation), unlockEvent.UnlockPathReputation, addColorTags: true)), - ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true)))); + ("[currentreputation]", unlockReputation.GetFormattedReputationText(addColorTags: true))))); } else { @@ -955,7 +960,7 @@ namespace Barotrauma if (connection.LevelData.HasHuntingGrounds) { - DrawIcon("HuntingGrounds", (int)(28 * zoom), TextManager.Get("HuntingGroundsTooltip")); + DrawIcon("HuntingGrounds", (int)(28 * zoom), RichString.Rich(TextManager.Get("HuntingGroundsTooltip"))); } if (crushDepthWarningIconStyle != null) @@ -976,7 +981,7 @@ namespace Barotrauma } } - void DrawIcon(string iconStyle, int iconSize, LocalizedString tooltipText) + void DrawIcon(string iconStyle, int iconSize, RichString tooltipText) { Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 339d904b8..24f85d6bd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -526,22 +526,17 @@ namespace Barotrauma return true; } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { byte sectionCount = msg.ReadByte(); bool invalidMessage = false; - if (type != ServerNetObject.ENTITY_EVENT && type != ServerNetObject.ENTITY_EVENT_INITIAL) - { - DebugConsole.NewMessage($"Error while reading a network event for the structure \"{Name} ({ID})\". Invalid event type ({type}).", Color.Red); - return; - } - else if (sectionCount != Sections.Length) + if (sectionCount != Sections.Length) { invalidMessage = true; string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; - DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); } for (int i = 0; i < sectionCount; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 90fb358ed..b6968b44f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -15,7 +15,7 @@ using Barotrauma.Items.Components; namespace Barotrauma { - partial class Submarine : Entity, IServerSerializable + partial class Submarine : Entity, IServerPositionSync { public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) { @@ -547,19 +547,50 @@ namespace Barotrauma } } - int disabledItemLightCount = 0; - foreach (Item item in Item.ItemList) + float entityCountWarningThreshold = 0.75f; + + if (Item.ItemList.Count > SubEditorScreen.MaxItems * entityCountWarningThreshold) { - if (item.ParentInventory == null) { continue; } - disabledItemLightCount += item.GetComponents().Count(); + if (!IsWarningSuppressed(SubEditorScreen.WarningType.ItemCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.itemcountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.ItemCount); + } } - int count = GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; - if (count > 45) + + if ((MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold) { - if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooManyLights)) + if (!IsWarningSuppressed(SubEditorScreen.WarningType.StructureCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.structurecountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.StructureCount); + } + } + + if (Structure.WallList.Count > SubEditorScreen.MaxStructures * entityCountWarningThreshold) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.WallCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.wallcountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.WallCount); + } + } + + if (GetLightCount() > SubEditorScreen.MaxLights * entityCountWarningThreshold) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.LightCount)) + { + errorMsgs.Add(TextManager.Get("subeditor.lightcountwarning").Value); + warnings.Add(SubEditorScreen.WarningType.LightCount); + } + } + + if (GetShadowCastingLightCount() > SubEditorScreen.MaxShadowCastingLights * entityCountWarningThreshold) + { + if (!IsWarningSuppressed(SubEditorScreen.WarningType.ShadowCastingLightCount)) { errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning").Value); - warnings.Add(SubEditorScreen.WarningType.TooManyLights); + warnings.Add(SubEditorScreen.WarningType.ShadowCastingLightCount); } } @@ -627,14 +658,31 @@ namespace Barotrauma } } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public static int GetLightCount() { - if (type != ServerNetObject.ENTITY_POSITION) + int disabledItemLightCount = 0; + foreach (Item item in Item.ItemList) { - DebugConsole.NewMessage($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Invalid event type ({type}).", Color.Red); + if (item.ParentInventory == null) { continue; } + disabledItemLightCount += item.GetComponents().Count(); } + return GameMain.LightManager.Lights.Count() - disabledItemLightCount; + } - var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Info.Name); + public static int GetShadowCastingLightCount() + { + int disabledItemLightCount = 0; + foreach (Item item in Item.ItemList) + { + if (item.ParentInventory == null) { continue; } + disabledItemLightCount += item.GetComponents().Count(); + } + return GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; + } + + public void ClientReadPosition(IReadMessage msg, float sendingTime) + { + var posInfo = PhysicsBody.ClientRead(msg, sendingTime, parentDebugName: Info.Name); msg.ReadPadBits(); if (posInfo != null) @@ -648,5 +696,10 @@ namespace Barotrauma subBody.PositionBuffer.Insert(index, posInfo); } } + + public void ClientEventRead(IReadMessage msg, float sendingTime) + { + throw new Exception($"Error while reading a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to receive events!"); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index 3a61415c6..9e0472592 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Networking } else { - orderMessageInfo.TargetCharacter?.SetOrder(order); + orderMessageInfo.TargetCharacter?.SetOrder(order, orderMessageInfo.IsNewOrder); } } } @@ -125,7 +125,8 @@ namespace Barotrauma.Networking Order order = null; if (orderMessageInfo.TargetPosition != null) { - order = new Order(orderPrefab, orderOption, orderMessageInfo.Priority, Order.OrderType.Current, null, orderMessageInfo.TargetPosition, orderGiver: senderCharacter); + order = new Order(orderPrefab, orderOption, orderMessageInfo.TargetPosition, orderGiver: senderCharacter) + .WithManualPriority(orderMessageInfo.Priority); } else if (orderMessageInfo.WallSectionIndex != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index 7770929fc..f235624b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO.Pipes; +using System.Linq; namespace Barotrauma.Networking { @@ -12,6 +13,9 @@ namespace Barotrauma.Networking public static void Start(ProcessStartInfo processInfo) { + CrashString = null; + CrashReportFilePath = null; + writePipe = new AnonymousPipeServerStream(PipeDirection.Out, System.IO.HandleInheritability.Inheritable); readPipe = new AnonymousPipeServerStream(PipeDirection.In, System.IO.HandleInheritability.Inheritable); @@ -42,8 +46,8 @@ namespace Barotrauma.Networking public static void ClosePipes() { - writePipe?.Close(); - readPipe?.Close(); + writePipe?.Dispose(); writePipe = null; + readPipe?.Dispose(); readPipe = null; shutDown = true; } @@ -54,5 +58,20 @@ namespace Barotrauma.Networking PrivateShutDown(); } + + public static string CrashString { get; private set; } + public static string CrashReportFilePath { get; private set; } + + public static LocalizedString CrashMessage + => string.IsNullOrEmpty(CrashReportFilePath) + ? TextManager.Get("ServerProcessClosed") + : TextManager.GetWithVariable("ServerProcessCrashed", "[reportfilepath]", CrashReportFilePath); + + static partial void HandleCrashString(string str) + { + DebugConsole.ThrowError($"The server has crashed: {str}"); + CrashReportFilePath = str.Split("||").FirstOrDefault() ?? "servercrashreport.log"; + CrashString = str; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs index 5fb9aac87..972c3f083 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/EntitySpawner.cs @@ -5,7 +5,7 @@ namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) + public void ClientEventRead(IReadMessage message, float sendingTime) { bool remove = message.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 23dd76fe6..4083f49cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -666,7 +666,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process?.HasExited ?? true) { Disconnect(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; } } @@ -937,6 +937,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.MEDICAL: campaign?.MedicalClinic?.ClientRead(inc); break; + case ServerPacketHeader.MONEY: + campaign?.ClientReadMoney(inc); + break; case ServerPacketHeader.READY_CHECK: ReadyCheck.ClientRead(inc); break; @@ -1203,7 +1206,7 @@ namespace Barotrauma.Networking if (disconnectReason == DisconnectReason.ServerCrashed && IsServerOwner) { - msg = TextManager.Get("ServerProcessCrashed"); + msg = TextManager.GetWithVariable("ServerProcessCrashed", "[reportfilepath]", ChildServerRelay.CrashReportFilePath); } } @@ -2240,10 +2243,11 @@ namespace Barotrauma.Networking } } + readonly List debugEntityList = new List(); private void ReadIngameUpdate(IReadMessage inc) { - List entities = new List(); - + debugEntityList.Clear(); + float sendingTime = inc.ReadSingle() - 0.0f;//TODO: reimplement inc.SenderConnection.RemoteTimeOffset; ServerNetObject? prevObjHeader = null; @@ -2284,15 +2288,15 @@ namespace Barotrauma.Networking uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); - var entity = Entity.FindEntityByID(id) as IServerSerializable; + var entity = Entity.FindEntityByID(id) as IServerPositionSync; if (msgEndPos > inc.LengthBits) { DebugConsole.ThrowError($"Error while reading a position update for the entity \"({entity?.ToString() ?? "null"})\". Message length exceeds the size of the buffer."); return; } - entities.Add(entity); - if (entity != null && (entity is Item || entity is Character || entity is Submarine)) + debugEntityList.Add(entity); + if (entity != null) { if (entity is Item != isItem) { @@ -2307,7 +2311,7 @@ namespace Barotrauma.Networking } else { - entity.ClientRead(objHeader.Value, inc, sendingTime); + entity.ClientReadPosition(inc, sendingTime); } } @@ -2321,7 +2325,7 @@ namespace Barotrauma.Networking break; case ServerNetObject.ENTITY_EVENT: case ServerNetObject.ENTITY_EVENT_INITIAL: - if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, entities)) + if (!entityEventManager.Read(objHeader.Value, inc, sendingTime, debugEntityList)) { return; } @@ -2340,7 +2344,6 @@ namespace Barotrauma.Networking prevBytePos = inc.BytePosition; } } - catch (Exception ex) { List errorLines = new List @@ -2359,7 +2362,7 @@ namespace Barotrauma.Networking objHeader == ServerNetObject.ENTITY_EVENT || objHeader == ServerNetObject.ENTITY_EVENT_INITIAL || objHeader == ServerNetObject.ENTITY_POSITION || prevObjHeader == ServerNetObject.ENTITY_POSITION) { - foreach (IServerSerializable ent in entities) + foreach (IServerSerializable ent in debugEntityList) { if (ent == null) { @@ -2480,7 +2483,7 @@ namespace Barotrauma.Networking outmsg.Write(GameMain.NetLobbyScreen.CampaignCharacterDiscarded); } - Character.Controlled?.ClientWrite(outmsg); + Character.Controlled?.ClientWriteInput(outmsg); GameMain.GameScreen.Cam?.ClientWrite(outmsg); entityEventManager.Write(outmsg, clientPeer?.ServerConnection); @@ -2711,7 +2714,7 @@ namespace Barotrauma.Networking } } - public override void CreateEntityEvent(INetSerializable entity, object[] extraData) + public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { if (!(entity is IClientSerializable clientSerializable)) { @@ -2770,7 +2773,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process != null) { int checks = 0; - while (ChildServerRelay.Process != null && !ChildServerRelay.Process.HasExited) + while (ChildServerRelay.Process is { HasExited: false }) { if (checks > 10) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 0d67a7e1a..50486ace0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { @@ -41,16 +42,16 @@ namespace Barotrauma.Networking thisClient = client; } - public void CreateEvent(IClientSerializable entity, object[] extraData = null) + public void CreateEvent(IClientSerializable entity, NetEntityEvent.IData extraData = null) { if (GameMain.Client?.Character == null) { return; } if (!ValidateEntity(entity)) { return; } - var newEvent = new ClientEntityEvent(entity, (UInt16)(ID + 1)) - { - CharacterStateID = GameMain.Client.Character.LastNetworkUpdateID - }; + var newEvent = new ClientEntityEvent( + entity, + eventId: (UInt16)(ID + 1), + characterStateId: GameMain.Client.Character.LastNetworkUpdateID); if (extraData != null) { newEvent.SetData(extraData); } for (int i = events.Count - 1; i >= 0; i--) @@ -144,6 +145,7 @@ namespace Barotrauma.Networking entities.Clear(); + msg.ReadPadBits(); UInt16 firstEventID = msg.ReadUInt16(); int eventCount = msg.ReadByte(); @@ -172,7 +174,6 @@ namespace Barotrauma.Networking DebugConsole.NewMessage("received msg " + thisEventID + " (null entity)", Microsoft.Xna.Framework.Color.Orange); } - msg.ReadPadBits(); entities.Add(null); if (thisEventID == (UInt16)(lastReceivedID + 1)) { lastReceivedID++; } continue; @@ -207,7 +208,6 @@ namespace Barotrauma.Networking } msg.BitPosition += msgLength * 8; - msg.ReadPadBits(); } else { @@ -239,22 +239,22 @@ namespace Barotrauma.Networking //msg.BitPosition = (int)(msgPosition + msgLength * 8); } } - catch (Exception e) { - string errorMsg = "Failed to read event for entity \"" + entity.ToString() + "\" (" + e.Message + ")! (MidRoundSyncing: " + thisClient.MidRoundSyncing + ")\n" + e.StackTrace.CleanupStackTrace(); + string errorMsg = $"Failed to read event {thisEventID} for entity \"{entity}\"" + + $"{(entity is Entity { ID: var entityId } ? $", id {entityId}" : "")} "; + DebugConsole.ThrowError(errorMsg, e); + + errorMsg += $"({e.Message})! (MidRoundSyncing: {thisClient.MidRoundSyncing})\n{e.StackTrace.CleanupStackTrace()}"; errorMsg += "\nPrevious entities:"; for (int j = entities.Count - 2; j >= 0; j--) { errorMsg += "\n" + (entities[j] == null ? "NULL" : entities[j].ToString()); } - DebugConsole.ThrowError("Failed to read event for entity \"" + entity.ToString() + "\"!", e); - GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:ReadFailed" + entity.ToString(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); msg.BitPosition = (int)(msgPosition + msgLength * 8); - msg.ReadPadBits(); } } } @@ -272,7 +272,7 @@ namespace Barotrauma.Networking protected void ReadEvent(IReadMessage buffer, IServerSerializable entity, float sendingTime) { - entity.ClientRead(ServerNetObject.ENTITY_EVENT, buffer, sendingTime); + entity.ClientEventRead(buffer, sendingTime); } public void Clear() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs index bcecc22dd..0bca588f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -4,20 +4,21 @@ namespace Barotrauma.Networking { class ClientEntityEvent : NetEntityEvent { - private IClientSerializable serializable; + private readonly IClientSerializable serializable; - public UInt16 CharacterStateID; + public readonly UInt16 CharacterStateID; - public ClientEntityEvent(IClientSerializable entity, UInt16 id) - : base(entity, id) + public ClientEntityEvent(IClientSerializable entity, UInt16 eventId, UInt16 characterStateId) + : base(entity, eventId) { serializable = entity; + CharacterStateID = characterStateId; } public void Write(IWriteMessage msg) { msg.Write(CharacterStateID); - serializable.ClientWrite(msg, Data); + serializable.ClientEventWrite(msg, Data); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 38b49f398..47201cef6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -84,7 +84,7 @@ namespace Barotrauma.Networking if (ownerKey != 0 && (ChildServerRelay.Process?.HasExited ?? true)) { Close(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += (btn, obj) => { GameMain.MainMenuScreen.Select(); return false; }; return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index d96fb7c5f..580f16b8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -193,7 +193,7 @@ namespace Barotrauma.Networking if (ChildServerRelay.HasShutDown || (ChildServerRelay.Process?.HasExited ?? true)) { Close(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); msgBox.Buttons[0].OnClicked += (btn, obj) => { GameMain.MainMenuScreen.Select(); return false; }; return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs index 4057e960c..092f871d4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/RespawnManager.cs @@ -71,7 +71,7 @@ namespace Barotrauma.Networking }, delay: delay); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public void ClientEventRead(IReadMessage msg, float sendingTime) { bool respawnPromptPending = false; var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index f14b1e743..a0ac4f61a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -153,7 +153,7 @@ namespace Barotrauma } } - public PosInfo ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime, string parentDebugName) + public PosInfo ClientRead(IReadMessage msg, float sendingTime, string parentDebugName) { float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 9e15a34c1..9c62c6816 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -278,6 +278,17 @@ namespace Barotrauma characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant), jobPrefab)); } } + if (characterInfos.Count == 0) + { + DebugConsole.ThrowError($"No starting crew found! If you're using mods, it may be that the mods have overridden the vanilla jobs without specifying which types of characters the starting crew should consist of. If you're the developer of the mod, ensure that you've set the {nameof(JobPrefab.InitialCount)} properties for the custom jobs."); + DebugConsole.AddWarning("Choosing the first available jobs as the starting crew..."); + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + var variant = Rand.Range(0, jobPrefab.Variants); + characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant), jobPrefab)); + if (characterInfos.Count >= 3) { break; } + } + } characterInfos.Sort((a, b) => Math.Sign(b.Job.MinKarma - a.Job.MinKarma)); characterInfoColumns.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 7de7bbc32..e8b404bb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -143,14 +143,13 @@ namespace Barotrauma { if (Campaign.PurchasedHullRepairs) { - Campaign.Money += CampaignMode.HullRepairCost; + Campaign.Wallet.Refund(CampaignMode.HullRepairCost); Campaign.PurchasedHullRepairs = false; } else { - if (Campaign.Money >= CampaignMode.HullRepairCost) + if (Campaign.Wallet.TryDeduct(CampaignMode.HullRepairCost)) { - Campaign.Money -= CampaignMode.HullRepairCost; GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.HullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); Campaign.PurchasedHullRepairs = true; } @@ -189,14 +188,13 @@ namespace Barotrauma { if (Campaign.PurchasedItemRepairs) { - Campaign.Money += CampaignMode.ItemRepairCost; + Campaign.Wallet.Refund(CampaignMode.ItemRepairCost); Campaign.PurchasedItemRepairs = false; } else { - if (Campaign.Money >= CampaignMode.ItemRepairCost) + if (Campaign.Wallet.TryDeduct(CampaignMode.ItemRepairCost)) { - Campaign.Money -= CampaignMode.ItemRepairCost; GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ItemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); Campaign.PurchasedItemRepairs = true; } @@ -242,14 +240,13 @@ namespace Barotrauma if (Campaign.PurchasedLostShuttles) { - Campaign.Money += CampaignMode.ShuttleReplaceCost; + Campaign.Wallet.Refund(CampaignMode.ShuttleReplaceCost); Campaign.PurchasedLostShuttles = false; } else { - if (Campaign.Money >= CampaignMode.ShuttleReplaceCost) + if (Campaign.Wallet.TryDeduct(CampaignMode.ShuttleReplaceCost)) { - Campaign.Money -= CampaignMode.ShuttleReplaceCost; GameAnalyticsManager.AddMoneySpentEvent(CampaignMode.ShuttleReplaceCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); Campaign.PurchasedLostShuttles = true; } @@ -445,7 +442,7 @@ namespace Barotrauma { Color = MapGenerationParams.Instance.IndicatorColor, HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), - ToolTip = TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip") + ToolTip = RichString.Rich(TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip")) }; new GUITextBlock(new RectTransform(Vector2.One, beaconStationContent.RectTransform), TextManager.Get("submarinetype.beaconstation", "beaconstationsonarlabel"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) @@ -462,7 +459,7 @@ namespace Barotrauma { Color = MapGenerationParams.Instance.IndicatorColor, HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), - ToolTip = TextManager.Get("HuntingGroundsTooltip") + ToolTip = RichString.Rich(TextManager.Get("HuntingGroundsTooltip")) }; new GUITextBlock(new RectTransform(Vector2.One, huntingGroundsContent.RectTransform), TextManager.Get("missionname.huntinggrounds"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft) @@ -705,11 +702,11 @@ namespace Barotrauma { case CampaignMode.InteractionType.Repair: repairHullsButton.Enabled = - (Campaign.PurchasedHullRepairs || Campaign.Money >= CampaignMode.HullRepairCost) && + (Campaign.PurchasedHullRepairs || Campaign.Wallet.CanAfford(CampaignMode.HullRepairCost)) && Campaign.AllowedToManageCampaign(); repairHullsButton.GetChild().Selected = Campaign.PurchasedHullRepairs; repairItemsButton.Enabled = - (Campaign.PurchasedItemRepairs || Campaign.Money >= CampaignMode.ItemRepairCost) && + (Campaign.PurchasedItemRepairs || Campaign.Wallet.CanAfford(CampaignMode.ItemRepairCost)) && Campaign.AllowedToManageCampaign(); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; @@ -721,7 +718,7 @@ namespace Barotrauma else { replaceShuttlesButton.Enabled = - (Campaign.PurchasedLostShuttles || Campaign.Money >= CampaignMode.ShuttleReplaceCost) && + (Campaign.PurchasedLostShuttles || Campaign.Wallet.CanAfford(CampaignMode.ShuttleReplaceCost)) && Campaign.AllowedToManageCampaign(); replaceShuttlesButton.GetChild().Selected = Campaign.PurchasedLostShuttles; } @@ -742,7 +739,7 @@ namespace Barotrauma public static LocalizedString GetMoney() { - return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Money)); + return TextManager.GetWithVariable("PlayerCredits", "[credits]", (GameMain.GameSession?.Campaign == null) ? "0" : string.Format(CultureInfo.InvariantCulture, "{0:N0}", GameMain.GameSession.Campaign.Wallet.Balance)); } private void UpdateMaxMissions(Location location) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 1556924c7..3f382e8f7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -1668,11 +1668,7 @@ namespace Barotrauma.CharacterEditor if (contentPackage == null) { -#if DEBUG - contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(); -#else contentPackage = ContentPackageManager.EnabledPackages.All.LastOrDefault(cp => cp != vanilla); -#endif } if (contentPackage == null) { @@ -1680,13 +1676,11 @@ namespace Barotrauma.CharacterEditor DebugConsole.ThrowError(GetCharacterEditorTranslation("NoContentPackageSelected")); return false; } -#if !DEBUG if (vanilla != null && contentPackage == vanilla) { GUI.AddMessage(GetCharacterEditorTranslation("CannotEditVanillaCharacters"), GUIStyle.Red, font: GUIStyle.LargeFont); return false; } -#endif // Content package if (contentPackage is RegularPackage regular && !ContentPackageManager.EnabledPackages.Regular.Contains(regular)) { @@ -1721,9 +1715,9 @@ namespace Barotrauma.CharacterEditor } else { - config.SetAttributeValue("speciesname", name); - config.SetAttributeValue("humanoid", isHumanoid); - var ragdollElement = config.Element("ragdolls"); + config.SetAttributeValue("speciesname", name, StringComparison.OrdinalIgnoreCase); + config.SetAttributeValue("humanoid", isHumanoid, StringComparison.OrdinalIgnoreCase); + var ragdollElement = config.GetChildElement("ragdolls"); if (ragdollElement == null) { config.Add(new XElement("ragdolls", CreateRagdollPath())); @@ -1736,7 +1730,7 @@ namespace Barotrauma.CharacterEditor ragdollElement.ReplaceWith(new XElement("ragdolls", CreateRagdollPath())); } } - var animationElement = config.Element("animations"); + var animationElement = config.GetChildElement("animations"); if (animationElement == null) { config.Add(new XElement("animations", CreateAnimationPath())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 48336c09b..8fb996949 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -160,8 +160,7 @@ namespace Barotrauma.CharacterEditor bool isTextureSelected = false; void UpdatePaths() { - string pathBase = ContentPackage == GameMain.VanillaContent ? $"Content/Characters/{Name}/{Name}" - : $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; + string pathBase = $"{ContentPath.ModDirStr}/Characters/{Name}/{Name}"; XMLPath = $"{pathBase}.xml"; xmlPathElement.Text = XMLPath; if (updateTexturePath) @@ -307,10 +306,10 @@ namespace Barotrauma.CharacterEditor contentPackageDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), rightContainer.RectTransform, Anchor.TopRight)); foreach (ContentPackage contentPackage in ContentPackageManager.EnabledPackages.All) { -#if !DEBUG - if (contentPackage == GameMain.VanillaContent) { continue; } -#endif - contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); + if (contentPackage != GameMain.VanillaContent) + { + contentPackageDropDown.AddItem(contentPackage.Name, userData: contentPackage, toolTip: contentPackage.Path); + } } contentPackageDropDown.OnSelected = (obj, userdata) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index e035855de..8abc2da45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1329,6 +1329,11 @@ namespace Barotrauma } SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + levelDifficultyScrollBar.ToolTip = string.Empty; + if (!levelDifficultyScrollBar.Enabled) + { + levelDifficultyScrollBar.ToolTip = TextManager.Get("campaigndifficultydisabled"); + } traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 95f6f37c6..f2d230caf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -32,7 +32,10 @@ namespace Barotrauma private GUIScrollBar zoomBar; private readonly List selectedSprites = new List(); private readonly List dirtySprites = new List(); - private Sprite selectedTexture; + private Texture2D SelectedTexture => lastSprite?.Texture; + private Sprite lastSprite; + private string selectedTexturePath; + private Rectangle textureRect; private float zoom = 1; private const float MinZoom = 0.25f, MaxZoom = 10.0f; @@ -82,12 +85,11 @@ namespace Barotrauma { OnClicked = (button, userData) => { - if (!(textureList.SelectedData is Texture2D selectedTexture)) { return false; } var selected = selectedSprites; Sprite firstSelected = selected.First(); selected.ForEach(s => s.ReloadTexture()); RefreshLists(); - textureList.Select(firstSelected.Texture, autoScroll: false); + textureList.Select(firstSelected.FullPath, autoScroll: false); selected.ForEachMod(s => spriteList.Select(s, autoScroll: false)); texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value); texturePathText.TextColor = GUIStyle.Green; @@ -101,10 +103,10 @@ namespace Barotrauma { OnClicked = (button, userData) => { - if (selectedTexture == null) { return false; } + if (SelectedTexture == null) { return false; } foreach (Sprite sprite in loadedSprites) { - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } // Not all sprites have a sourcerect defined, in which case we'll want to use the current source rect instead of an empty rect. @@ -206,23 +208,20 @@ namespace Barotrauma { OnSelected = (listBox, userData) => { - var previousSprite = selectedTexture; - selectedTexture = userData as Sprite; - if (previousSprite != selectedTexture) + var newTexturePath = userData as string; + if (selectedTexturePath == null || selectedTexturePath != newTexturePath) { + selectedTexturePath = newTexturePath; ResetZoom(); + spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: false); + UpdateScrollBar(spriteList); } foreach (GUIComponent child in spriteList.Content.Children) { var textBlock = (GUITextBlock)child; var sprite = (Sprite)textBlock.UserData; - textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexture.FilePath ? 1.0f : 0.4f); - if (sprite.FilePath == selectedTexture.FilePath) { textBlock.Visible = true; } - } - if (selectedSprites.None(s => s.FilePath == selectedTexture.FilePath)) - { - spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexture.FilePath), autoScroll: false); - UpdateScrollBar(spriteList); + textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexturePath ? 1.0f : 0.4f); + if (sprite.FilePath == selectedTexturePath) { textBlock.Visible = true; } } texturePathText.TextColor = Color.LightGray; topPanelContents.Visible = true; @@ -251,9 +250,12 @@ namespace Barotrauma { OnSelected = (listBox, userData) => { - if (!(userData is Sprite sprite)) return false; - SelectSprite(sprite); - return true; + if (userData is Sprite sprite) + { + SelectSprite(sprite); + return true; + } + return false; } }; @@ -410,12 +412,12 @@ namespace Barotrauma private bool SaveSprites(IEnumerable sprites) { - if (selectedTexture == null) { return false; } + if (SelectedTexture == null) { return false; } if (sprites.None()) { return false; } HashSet docsToSave = new HashSet(); foreach (Sprite sprite in sprites) { - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } var element = sprite.SourceElement; if (element == null) { continue; } element.SetAttributeValue("sourcerect", XMLExtensions.RectToString(sprite.SourceRect)); @@ -469,11 +471,11 @@ namespace Barotrauma // Select rects with the mouse if (Widget.selectedWidgets.None() || Widget.EnableMultiSelect) { - if (selectedTexture != null && GUI.MouseOn == null) + if (SelectedTexture != null && GUI.MouseOn == null) { foreach (Sprite sprite in loadedSprites) { - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } if (PlayerInput.PrimaryMouseButtonClicked()) { var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom)); @@ -637,20 +639,20 @@ namespace Barotrauma var viewArea = GetViewArea; - if (selectedTexture != null) + if (SelectedTexture != null) { textureRect = new Rectangle( - (int)(viewArea.Center.X - selectedTexture.Texture.Bounds.Width / 2f * zoom), - (int)(viewArea.Center.Y - selectedTexture.Texture.Bounds.Height / 2f * zoom), - (int)(selectedTexture.Texture.Bounds.Width * zoom), - (int)(selectedTexture.Texture.Bounds.Height * zoom)); + (int)(viewArea.Center.X - SelectedTexture.Bounds.Width / 2f * zoom), + (int)(viewArea.Center.Y - SelectedTexture.Bounds.Height / 2f * zoom), + (int)(SelectedTexture.Bounds.Width * zoom), + (int)(SelectedTexture.Bounds.Height * zoom)); - spriteBatch.Draw(selectedTexture.Texture, + spriteBatch.Draw(SelectedTexture, viewArea.Center.ToVector2(), sourceRectangle: null, color: Color.White, rotation: 0.0f, - origin: new Vector2(selectedTexture.Texture.Bounds.Width / 2.0f, selectedTexture.Texture.Bounds.Height / 2.0f), + origin: new Vector2(SelectedTexture.Bounds.Width / 2.0f, SelectedTexture.Bounds.Height / 2.0f), scale: zoom, effects: SpriteEffects.None, layerDepth: 0); @@ -666,7 +668,7 @@ namespace Barotrauma foreach (GUIComponent element in spriteList.Content.Children) { if (!(element.UserData is Sprite sprite)) { continue; } - if (sprite.FullPath != selectedTexture.FullPath) { continue; } + if (sprite.FullPath != selectedTexturePath) { continue; } Rectangle sourceRect = new Rectangle( textureRect.X + (int)(sprite.SourceRect.X * zoom), @@ -874,13 +876,13 @@ namespace Barotrauma public void SelectSprite(Sprite sprite) { + lastSprite = sprite; if (!loadedSprites.Contains(sprite)) { loadedSprites.Add(sprite); RefreshLists(); } - - if (selectedSprites.Any(s => s.FullPath != selectedTexture.FullPath)) + if (selectedSprites.Any(s => s.FullPath != selectedTexturePath)) { ResetWidgets(); } @@ -902,9 +904,9 @@ namespace Barotrauma selectedSprites.Add(sprite); dirtySprites.Add(sprite); } - if (selectedTexture?.FullPath != sprite.FullPath) + if (sprite.FullPath != selectedTexturePath) { - textureList.Select(sprite.Texture, autoScroll: false); + textureList.Select(sprite.FullPath, autoScroll: false); UpdateScrollBar(textureList); } xmlPathText.Text = string.Empty; @@ -926,7 +928,6 @@ namespace Barotrauma public void RefreshLists() { - //selectedTexture = null; selectedSprites.Clear(); textureList.ClearChildren(); spriteList.ClearChildren(); @@ -936,7 +937,7 @@ namespace Barotrauma foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath.Value))) { //ignore sprites that don't have a file path (e.g. submarine pics) - if (sprite.FilePath.IsNullOrEmpty()) continue; + if (sprite.FilePath.IsNullOrEmpty()) { continue; } string normalizedFilePath = sprite.FilePath.FullPath; if (!textures.Contains(normalizedFilePath)) { @@ -944,7 +945,7 @@ namespace Barotrauma Path.GetFileName(sprite.FilePath.Value)) { ToolTip = sprite.FilePath.Value, - UserData = sprite + UserData = sprite.FullPath }; textures.Add(normalizedFilePath); } @@ -965,10 +966,10 @@ namespace Barotrauma public void ResetZoom() { - if (selectedTexture == null) { return; } + if (SelectedTexture == null) { return; } var viewArea = GetViewArea; - float width = viewArea.Width / (float)selectedTexture.Texture.Width; - float height = viewArea.Height / (float)selectedTexture.Texture.Height; + float width = viewArea.Width / (float)SelectedTexture.Width; + float height = viewArea.Height / (float)SelectedTexture.Height; zoom = Math.Min(1, Math.Min(width, height)); zoomBar.BarScroll = GetBarScrollValue(); viewAreaOffset = Point.Zero; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 09ab544ff..14d7493a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -5,12 +5,10 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; -using System.Threading.Tasks; #if DEBUG using System.IO; #else @@ -21,6 +19,12 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { + public const int MaxStructures = 2000; + public const int MaxWalls = 500; + public const int MaxItems = 5000; + public const int MaxLights = 300; + public const int MaxShadowCastingLights = 60; + private static Submarine MainSub { get => Submarine.MainSub; @@ -83,7 +87,11 @@ namespace Barotrauma NoCargoSpawnpoints, NoBallastTag, NonLinkedGaps, - TooManyLights + StructureCount, + WallCount, + ItemCount, + LightCount, + ShadowCastingLightCount } public static Vector2 MouseDragStart = Vector2.Zero; @@ -102,7 +110,7 @@ namespace Barotrauma private bool wasSelectedBefore; public GUIComponent TopPanel; - private GUIComponent showEntitiesPanel, entityCountPanel; + public GUIComponent showEntitiesPanel, entityCountPanel; private readonly List showEntitiesTickBoxes = new List(); private readonly Dictionary hiddenSubCategories = new Dictionary(); @@ -809,7 +817,7 @@ namespace Barotrauma var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => { - itemCount.TextColor = ToolBox.GradientLerp(Item.ItemList.Count / 5000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + itemCount.TextColor = Item.ItemList.Count > MaxItems ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, Item.ItemList.Count / (float)MaxItems); return Item.ItemList.Count.ToString(); }; @@ -818,8 +826,8 @@ namespace Barotrauma var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => { - int count = (MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count); - structureCount.TextColor = ToolBox.GradientLerp(count / 1000.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + int count = MapEntity.mapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count; + structureCount.TextColor = count > MaxStructures ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxStructures); return count.ToString(); }; @@ -828,7 +836,7 @@ namespace Barotrauma var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); wallCount.TextGetter = () => { - wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + wallCount.TextColor = Structure.WallList.Count > MaxWalls ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, Structure.WallList.Count / (float)MaxWalls); return Structure.WallList.Count.ToString(); }; @@ -843,7 +851,7 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(); } - lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + lightCountText.TextColor = lightCount > MaxLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxLights); return lightCount.ToString(); }; var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), @@ -857,7 +865,7 @@ namespace Barotrauma if (item.ParentInventory != null) { continue; } lightCount += item.GetComponents().Count(l => l.CastShadows); } - shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); + shadowCastingLightCountText.TextColor = lightCount > MaxShadowCastingLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxShadowCastingLights); return lightCount.ToString(); }; entityCountPanel.RectTransform.NonScaledSize = @@ -1448,7 +1456,7 @@ namespace Barotrauma case ".jpeg": if (saveFrame == null) { break; } - Texture2D texture = Sprite.LoadTexture(filePath); + Texture2D texture = Sprite.LoadTexture(filePath, compress: false); previewImage.Sprite = new Sprite(texture, null, null); if (MainSub != null) { @@ -1548,7 +1556,7 @@ namespace Barotrauma { foreach (GUIColorPicker colorPicker in msgBox.GetAllChildren()) { - colorPicker.DisposeTextures(); + colorPicker.Dispose(); } msgBox.Close(); @@ -1827,6 +1835,21 @@ namespace Barotrauma modProject.Save(packagePath); } + if (!GameMain.DebugDraw) + { + if (Submarine.GetLightCount() > MaxLights) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.lightcounterror", "[max]", MaxShadowCastingLights.ToString())); + return false; + } + + if (Submarine.GetShadowCastingLightCount() > MaxShadowCastingLights) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.shadowcastinglightcounterror", "[max]", MaxShadowCastingLights.ToString())); + return false; + } + } + if (string.IsNullOrWhiteSpace(name)) { GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red); @@ -3634,7 +3657,7 @@ namespace Barotrauma closeButton.OnClicked = (button, o) => { - colorPicker.DisposeTextures(); + colorPicker.Dispose(); msgBox.Close(); Color newColor = SetColor(null); @@ -3678,7 +3701,7 @@ namespace Barotrauma cancelButton.OnClicked = (button, o) => { - colorPicker.DisposeTextures(); + colorPicker.Dispose(); msgBox.Close(); foreach (var (e, color, prop) in entities) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index c6d3deeb0..3d95b1bbb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -21,16 +21,16 @@ namespace Barotrauma private Item? miniMapItem; private Submarine? submarine; - private Character? dummyCharacter; - public static Effect BlueprintEffect = null!; - private GUIFrame container = null!; + public static Character? dummyCharacter; + public static Effect? BlueprintEffect; + private GUIFrame? container; private TabMenu? tabMenu; public TestScreen() { Cam = new Camera(); - BlueprintEffect = GameMain.GameScreen.BlueprintEffect!; + BlueprintEffect = GameMain.GameScreen.BlueprintEffect; new GUIButton(new RectTransform(new Point(256, 256), Frame.RectTransform), "Reload shader") { @@ -38,7 +38,7 @@ namespace Barotrauma { BlueprintEffect.Dispose(); GameMain.Instance.Content.Unload(); - BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl")!; + BlueprintEffect = GameMain.Instance.Content.Load("Effects/blueprintshader_opengl"); GameMain.GameScreen.BlueprintEffect = BlueprintEffect; return true; } @@ -47,21 +47,19 @@ namespace Barotrauma } public override void Select() - { + { base.Select(); container = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); var tab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); - MedicalClinicUI clinic = new MedicalClinicUI(new MedicalClinic(null!), tab); - clinic.RequestLatestPending(); if (dummyCharacter is { Removed: false }) { dummyCharacter?.Remove(); } - // dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - // dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom()); - // dummyCharacter.Info.Name = "Galldren"; - // dummyCharacter.Inventory.CreateSlots(); + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); + dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom(Rand.RandSync.Unsynced)); + dummyCharacter.Info.Name = "Galldren"; + dummyCharacter.Inventory.CreateSlots(); Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); @@ -71,7 +69,8 @@ namespace Barotrauma public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - container.AddToGUIUpdateList(); + container?.AddToGUIUpdateList(); + tabMenu?.AddToGUIUpdateList(); // CharacterHUD.AddToGUIUpdateList(dummyCharacter); // dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); } @@ -79,15 +78,13 @@ namespace Barotrauma public override void Update(double deltaTime) { base.Update(deltaTime); - tabMenu!.Update(); if (dummyCharacter is { } dummy) { dummy.ControlLocalPlayer((float)deltaTime, Cam, false); dummy.Control((float)deltaTime, Cam); } - - GUI.Update((float)deltaTime); + tabMenu?.Update((float)deltaTime); } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 0b2da87b7..f8186ff57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -342,9 +342,9 @@ namespace Barotrauma if (property.PropertyType == typeof(string) && value == null) { value = ""; - } + } - Identifier propertyTag = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier(); + Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier(); Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier(); LocalizedString displayName = TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier()); @@ -365,7 +365,7 @@ namespace Barotrauma { displayName = property.Name.FormatCamelCaseWithSpaces(); #if DEBUG - Editable editable = property.GetAttribute(); + InGameEditable editable = property.GetAttribute(); if (editable != null) { if (!MissingLocalizations.Contains($"sp.{propertyTag}.name|{displayName}")) @@ -378,7 +378,11 @@ namespace Barotrauma #endif } - LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description", $"sp.{fallbackTag}.description"); + LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); + if (toolTip.IsNullOrEmpty()) + { + toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); + } if (toolTip == null) { @@ -1312,12 +1316,9 @@ namespace Barotrauma entity = e.Item; } - if (GameMain.Client != null) + if (GameMain.Client != null && entity is Item item) { - if (entity is IClientSerializable clientSerializable) - { - GameMain.Client.CreateEntityEvent(clientSerializable, new object[] { NetEntityEvent.Type.ChangeProperty, property }); - } + GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index d15eb2729..8abba88a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -21,8 +21,8 @@ namespace Barotrauma.SpriteDeformations private set; } - [Serialize("", IsPropertySaveable.Yes)] - public string TypeName + [Serialize("", IsPropertySaveable.No)] + public string Type { get; set; @@ -35,7 +35,7 @@ namespace Barotrauma.SpriteDeformations set; } - public string Name => $"Deformation ({TypeName})"; + public string Name => $"Deformation ({Type})"; [Serialize(1.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] public float Strength { get; private set; } @@ -85,11 +85,11 @@ namespace Barotrauma.SpriteDeformations public SpriteDeformationParams(XElement element) { - if (element != null) - { - TypeName = element.GetAttributeString("type", "").ToLowerInvariant(); - } SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + if (element != null && string.IsNullOrEmpty(Type)) + { + Type = element.GetAttributeString("typename", string.Empty); + } } } @@ -120,7 +120,7 @@ namespace Barotrauma.SpriteDeformations set { SetResolution(value); } } - public string TypeName => Params.TypeName; + public string TypeName => Params.Type; public int Sync => Params.Sync; @@ -177,7 +177,7 @@ namespace Barotrauma.SpriteDeformations if (newDeformation != null) { - newDeformation.Params.TypeName = typeName; + newDeformation.Params.Type = typeName; } return newDeformation; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs index 9c3cfb54b..c94e33092 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs @@ -522,7 +522,7 @@ namespace Barotrauma.Steam UserData = tag }; tagBtn.RectTransform.NonScaledSize - = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(5)); + = tagBtn.Font.MeasureString(tagBtn.Text).ToPoint() + new Point(GUI.IntScale(15), GUI.IntScale(5)); tagBtn.RectTransform.IsFixedSize = true; tagBtn.ClampMouseRectToParent = false; } @@ -625,7 +625,7 @@ namespace Barotrauma.Steam #region Stats box var statsHorizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, statsBox.RectTransform), isHorizontal: true); var statsVertical0 - = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform)); + = new GUILayoutGroup(new RectTransform((1.0f, 1.0f), statsHorizontalLayout.RectTransform), childAnchor: Anchor.TopCenter); statFrame("", ""); //padding @@ -680,7 +680,7 @@ namespace Barotrauma.Steam var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); - CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((1.0f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); + CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); #endregion var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs index b67cab661..d7a3f7b70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Lobby.cs @@ -71,6 +71,7 @@ namespace Barotrauma.Steam if (GameMain.Client == null) { LeaveLobby(); + return; } if (lobbyState == LobbyState.NotConnected) @@ -83,7 +84,7 @@ namespace Barotrauma.Steam return; } - var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent); currentLobby?.SetData("name", serverSettings.ServerName); currentLobby?.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs index e34fabd34..45fa95f50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -357,21 +357,31 @@ namespace Barotrauma.Steam private IEnumerable MessageBoxCoroutine(Func> subcoroutine) { - var messageBox = new GUIMessageBox("", "", relativeSize: (0.4f, 0.4f), buttons: new [] { TextManager.Get("Cancel") }); + var messageBox = new GUIMessageBox("", "...", buttons: new [] { TextManager.Get("Cancel") }); messageBox.Buttons[0].OnClicked = (button, o) => { messageBox.Close(); return false; }; - var currentStepText = new GUITextBlock(new RectTransform((1.0f, 0.8f), messageBox.InnerFrame.RectTransform), - "...", font: GUIStyle.Font) - { - CanBeFocused = false - }; - - foreach (var status in subcoroutine(currentStepText, messageBox)) + var coroutineEval = subcoroutine(messageBox.Text, messageBox); + while (true) { + bool moveNext = true; + try + { + moveNext = coroutineEval.GetEnumerator().MoveNext(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); + messageBox.Close(); + } + if (!moveNext) + { + messageBox.Close(); + } + var status = coroutineEval.GetEnumerator().Current; if (messageBox.Closed) { yield return CoroutineStatus.Success; @@ -410,7 +420,7 @@ namespace Barotrauma.Steam { SteamManager.Workshop.ForceRedownload(workshopItem); } - currentStepText.Text = $"Downloading {Percentage(workshopItem.DownloadAmount)}"; + currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } } @@ -426,7 +436,7 @@ namespace Barotrauma.Steam }); while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) { - currentStepText.Text = $"Installing"; + currentStepText.Text = TextManager.Get("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -444,7 +454,7 @@ namespace Barotrauma.Steam }); while (!localCopyMade) { - currentStepText.Text = $"Creating local copy"; + currentStepText.Text = TextManager.Get("PublishPopupCreateLocal"); yield return new WaitForSeconds(0.5f); } @@ -457,47 +467,62 @@ namespace Barotrauma.Steam GUITextBlock currentStepText, GUIMessageBox messageBox, string modVersion, Steamworks.Ugc.Editor editor, ContentPackage localPackage) { + if (!SteamManager.IsInitialized) + { + yield return CoroutineStatus.Failure; + } + bool stagingReady = false; + Exception? stagingException = null; TaskPool.Add("CreatePublishStagingCopy", SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage), (t) => { - Exception? exception = t.Exception?.InnerException ?? t.Exception; - if (exception != null) - { - throw new Exception($"Failed to create staging copy: {exception.Message} {exception.StackTrace}"); - } stagingReady = true; + stagingException = t.Exception?.GetInnermost(); }); - currentStepText.Text = "Copying item to staging folder..."; + currentStepText.Text = TextManager.Get("PublishPopupStaging"); while (!stagingReady) { yield return new WaitForSeconds(0.5f); } + if (stagingException != null) + { + throw new Exception($"Failed to create staging copy: {stagingException.Message} {stagingException.StackTrace.CleanupStackTrace()}"); + } + editor = editor .WithContent(SteamManager.Workshop.PublishStagingDir) .ForAppId(SteamManager.AppID); messageBox.Buttons[0].Enabled = false; Steamworks.Ugc.PublishResult? result = null; + Exception? resultException = null; TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", editor.SubmitAsync(), - (t) => + t => { - result = ((Task)t).Result; + t.TryGetResult(out result); + resultException = t.Exception?.GetInnermost(); }); - currentStepText.Text = "Submitting item to the Workshop..."; - while (!result.HasValue) { yield return new WaitForSeconds(0.5f); } + currentStepText.Text = TextManager.Get("PublishPopupSubmit"); + while (!result.HasValue && resultException is null) { yield return new WaitForSeconds(0.5f); } - if (result.Value.Success) + if (result is { Success: true }) { var resultId = result.Value.FileId; Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId); - SteamManager.Workshop.ForceRedownload(resultItem); - while (!resultItem.IsInstalled) + Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem); + while (!resultItem.IsInstalled && !downloadTask.IsCompleted) { - currentStepText.Text = $"Downloading {Percentage(resultItem.DownloadAmount)}"; + currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(resultItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } + if (!resultItem.IsInstalled) + { + throw new Exception($"Failed to install item: download task ended with status {downloadTask.Status}, " + + $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); + } + bool installed = false; TaskPool.Add( "InstallNewlyPublished", @@ -508,7 +533,7 @@ namespace Barotrauma.Steam }); while (!installed) { - currentStepText.Text = $"Installing"; + currentStepText.Text = TextManager.Get("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -524,8 +549,19 @@ namespace Barotrauma.Steam { SteamManager.OverlayCustomURL(resultItem.Url); } + new GUIMessageBox(string.Empty, TextManager.GetWithVariable("workshopitempublished", "[itemname]", localPackage.Name)); } + else if (resultException != null) + { + throw new Exception($"Failed to publish item: {resultException.Message} {resultException.StackTrace.CleanupStackTrace()}"); + } + else + { + new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("workshopitempublishfailed", "[itemname]", localPackage.Name)); + } + SteamManager.Workshop.DeletePublishStagingCopy(); + messageBox.Close(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs index 75372311c..9293df567 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Steam } private static int Round(float v) => (int)MathF.Round(v); - private static string Percentage(float v) => $"{Round(v * 100)}%"; + private static string Percentage(float v) => $"{Round(v * 100)}"; private struct ActionCarrier { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index 25d4144aa..fe499a4d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -1,14 +1,13 @@ #nullable enable +using Barotrauma.IO; using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Barotrauma.IO; namespace Barotrauma.Steam { @@ -180,8 +179,10 @@ namespace Barotrauma.Steam await CopyDirectory(contentPackage.Dir, contentPackage.Name, Path.GetDirectoryName(contentPackage.Path)!, PublishStagingDir); //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.ModVersion = modVersion; + ModProject modProject = new ModProject(contentPackage) + { + ModVersion = modVersion + }; modProject.Save(Path.Combine(PublishStagingDir, ContentPackage.FileListFileName)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs index c921a71aa..feab2338c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs @@ -1,16 +1,11 @@ #nullable enable -using System; using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; using System.Threading; -using System.Xml.Linq; -using Barotrauma.IO; -using Microsoft.Xna.Framework.Graphics; using ItemOrPackage = Barotrauma.Either; namespace Barotrauma.Steam @@ -25,12 +20,12 @@ namespace Barotrauma.Steam Publish } - private GUILayoutGroup tabber; - private Dictionary tabContents; + private readonly GUILayoutGroup tabber; + private readonly Dictionary tabContents; - private GUIFrame contentFrame; + private readonly GUIFrame contentFrame; - private CorePackage enabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); + private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); private readonly GUIDropDown enabledCoreDropdown; private readonly GUIListBox enabledRegularModsList; @@ -173,7 +168,7 @@ namespace Barotrauma.Steam to.DraggedElement = draggedElement; - to.BarScroll = to.BarScroll * (oldCount / newCount); + to.BarScroll *= (oldCount / newCount); } } @@ -367,7 +362,8 @@ namespace Barotrauma.Steam { ToolBox.OpenFileWithShell(mod.Dir); return false; - } + }, + ToolTip = TextManager.Get("OpenLocalModInExplorer") }; } else if (ContentPackageManager.WorkshopPackages.Contains(mod)) @@ -386,8 +382,13 @@ namespace Barotrauma.Steam onInstalledInfoButtonHit(item.Value); }); return false; - } + }, + ToolTip = TextManager.Get("ViewModDetails") }; + if (!SteamManager.IsInitialized) + { + infoButton.Enabled = false; + } TaskPool.Add( $"DetermineUpdateRequired{mod.SteamWorkshopId}", mod.IsUpToDate(), @@ -398,6 +399,7 @@ namespace Barotrauma.Steam if (!isUpToDate) { infoButton.ApplyStyle(GUIStyle.ComponentStyles["WorkshopMenu.InfoButtonUpdate"]); + infoButton.ToolTip = TextManager.Get("ViewModDetailsUpdateAvailable"); } }); } @@ -421,20 +423,26 @@ namespace Barotrauma.Steam private void CreatePopularModsTab(out GUIListBox popularModsList) { GUIFrame content = CreateNewContentFrame(Tab.PopularMods); - + if (!SteamManager.IsInitialized) + { + tabContents[Tab.PopularMods].Button.Enabled = false; + } CreateWorkshopItemList(content, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo); } private void CreatePublishTab(out GUIListBox selfModsList) { GUIFrame content = CreateNewContentFrame(Tab.Publish); - + if (!SteamManager.IsInitialized) + { + tabContents[Tab.Publish].Button.Enabled = false; + } CreateWorkshopItemOrPackageList(content, out _, out selfModsList, onSelected: PopulatePublishTab); } public void Apply() { - ContentPackageManager.EnabledPackages.SetCore(enabledCorePackage); + ContentPackageManager.EnabledPackages.SetCore(EnabledCorePackage); ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children .Where(c => c.UserData is RegularPackage).Select(c => (RegularPackage)c.UserData).ToArray()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index c6eb0b6b4..397c71876 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Text; using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma @@ -127,7 +126,7 @@ namespace Barotrauma Vector2 newPoint = new Vector2(point.X, point.Y); foreach (Vector2 otherPoint in toCheck.Concat(newPoints)) { - float diffX = Math.Abs(newPoint.X - otherPoint.X), + float diffX = Math.Abs(newPoint.X - otherPoint.X), diffY = Math.Abs(newPoint.Y - otherPoint.Y); if (diffX <= treshold) @@ -142,7 +141,7 @@ namespace Barotrauma } newPoints.Add(newPoint); } - + return newPoints; } @@ -275,7 +274,7 @@ namespace Barotrauma return pts; } } - + // Convert an RGB value into an HLS value. public static Vector3 RgbToHLS(this Color color) { @@ -320,7 +319,7 @@ namespace Barotrauma if (hue < 240) return q1 + (q2 - q1) * (240 - hue) / 60; return q1; } - + /// /// Convert a RGB value into a HSV value. /// @@ -329,7 +328,7 @@ namespace Barotrauma /// /// Vector3 where X is the hue (0-360 or NaN) /// Y is the saturation (0-1) - /// Z is the value (0-1) + /// Z is the value (0-1) /// public static Vector3 RGBToHSV(Color color) { @@ -478,7 +477,7 @@ namespace Barotrauma if (b.Build < a.Build) { return false; } return false; } - + public static void OpenFileWithShell(string filename) { ProcessStartInfo startInfo = new ProcessStartInfo() @@ -488,5 +487,24 @@ namespace Barotrauma }; Process.Start(startInfo); } + + public static Vector2 PaddingSizeParentRelative(RectTransform parent, float padding) + { + var (sizeX, sizeY) = parent.NonScaledSize.ToVector2(); + + float higher = sizeX, + lower = sizeY; + bool swap = lower > higher; + if (swap) { (higher, lower) = (lower, higher); } + + float diffY = lower - lower * padding; + + float paddingX = (higher - diffY) / higher, + paddingY = padding; + + if (swap) { (paddingX, paddingY) = (paddingY, paddingX); } + + return new Vector2(paddingX, paddingY); + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 31debdb9c..5b77eb340 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6f1f04661..48d9ebdc3 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index ca8ab114b..7ee9ab78d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 996817458..b77b4c435 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 296dc0707..d22aef776 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 4a9e5f918..49405004e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -56,7 +56,7 @@ namespace Barotrauma partial void OnMoneyChanged(int prevAmount, int newAmount) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); + GameMain.NetworkMember.CreateEntityEvent(this, new UpdateMoneyEventData()); } partial void OnTalentGiven(TalentPrefab talentPrefab) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index c3d14550f..096e49741 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -19,7 +19,7 @@ namespace Barotrauma } if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.01f) { - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateSkillsEventData()); prevSentSkill[skillIdentifier] = newLevel; } } @@ -30,14 +30,14 @@ namespace Barotrauma if (prevAmount != newAmount) { GameServer.Log($"{GameServer.CharacterLogName(Character)} has gained {newAmount - prevAmount} experience ({prevAmount} -> {newAmount})", ServerLog.MessageType.Talent); - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateExperience }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateExperienceEventData()); } } partial void OnPermanentStatChanged(StatTypes statType) { if (Character == null || Character.Removed) { return; } - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statType }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdatePermanentStatsEventData()); } public void ServerWrite(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a7c694e5d..7d8e7791e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -153,14 +153,94 @@ namespace Barotrauma } } - public virtual void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerReadInput(IReadMessage msg, Client c) { - if (GameMain.Server == null) return; - - switch (type) + if (c.Character != this) { - case ClientNetObject.CHARACTER_INPUT: +#if DEBUG + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); +#endif + return; + } + UInt16 networkUpdateID = msg.ReadUInt16(); + byte inputCount = msg.ReadByte(); + + if (AllowInput) { Enabled = true; } + + for (int i = 0; i < inputCount; i++) + { + InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal); + UInt16 newAim = 0; + UInt16 newInteract = 0; + + if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft) + { + c.KickAFKTimer = 0.0f; + } + else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft)) + { + //character changed the direction they're facing + c.KickAFKTimer = 0.0f; + } + + newAim = msg.ReadUInt16(); + if (newInput.HasFlag(InputNetFlags.Select) || + newInput.HasFlag(InputNetFlags.Deselect) || + newInput.HasFlag(InputNetFlags.Use) || + newInput.HasFlag(InputNetFlags.Health) || + newInput.HasFlag(InputNetFlags.Grab)) + { + newInteract = msg.ReadUInt16(); + } + + if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60)) + { + if ((i > 0 && memInput[i - 1].intAim != newAim)) + { + c.KickAFKTimer = 0.0f; + } + NetInputMem newMem = new NetInputMem + { + states = newInput, + intAim = newAim, + interact = newInteract, + networkUpdateID = (ushort)(networkUpdateID - i) + }; + memInput.Insert(i, newMem); + LastInputTime = Timing.TotalTime; + } + } + + if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID)) + { + LastNetworkUpdateID = networkUpdateID; + } + else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) + { +#if DEBUG || UNSTABLE + DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); +#endif + LastNetworkUpdateID = networkUpdateID; + } + if (memInput.Count > 60) + { + //deleting inputs from the queue here means the server is way behind and data needs to be dropped + //we'll make the server drop down to 30 inputs for good measure + memInput.RemoveRange(30, memInput.Count - 30); + } + } + + public virtual void ServerEventRead(IReadMessage msg, Client c) + { + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) + { + case EventType.InventoryState: + Inventory.ServerEventRead(msg, c); + break; + case EventType.Treatment: + bool doingCPR = msg.ReadBoolean(); if (c.Character != this) { #if DEBUG @@ -169,415 +249,315 @@ namespace Barotrauma return; } - UInt16 networkUpdateID = msg.ReadUInt16(); - byte inputCount = msg.ReadByte(); - - if (AllowInput) { Enabled = true; } - - for (int i = 0; i < inputCount; i++) + AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None; + break; + case EventType.Status: + if (c.Character != this) { - InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal); - UInt16 newAim = 0; - UInt16 newInteract = 0; - - if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft) - { - c.KickAFKTimer = 0.0f; - } - else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft)) - { - //character changed the direction they're facing - c.KickAFKTimer = 0.0f; - } - - newAim = msg.ReadUInt16(); - if (newInput.HasFlag(InputNetFlags.Select) || - newInput.HasFlag(InputNetFlags.Deselect) || - newInput.HasFlag(InputNetFlags.Use) || - newInput.HasFlag(InputNetFlags.Health) || - newInput.HasFlag(InputNetFlags.Grab)) - { - newInteract = msg.ReadUInt16(); - } - - if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60)) - { - if ((i > 0 && memInput[i - 1].intAim != newAim)) - { - c.KickAFKTimer = 0.0f; - } - NetInputMem newMem = new NetInputMem - { - states = newInput, - intAim = newAim, - interact = newInteract, - networkUpdateID = (ushort)(networkUpdateID - i) - }; - memInput.Insert(i, newMem); - LastInputTime = Timing.TotalTime; - } - } - - if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID)) - { - LastNetworkUpdateID = networkUpdateID; - } - else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500) - { -#if DEBUG || UNSTABLE - DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID."); +#if DEBUG + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - LastNetworkUpdateID = networkUpdateID; + return; } - if (memInput.Count > 60) + + if (IsIncapacitated) { - //deleting inputs from the queue here means the server is way behind and data needs to be dropped - //we'll make the server drop down to 30 inputs for good measure - memInput.RemoveRange(30, memInput.Count - 30); + var causeOfDeath = CharacterHealth.GetCauseOfDeath(); + Kill(causeOfDeath.type, causeOfDeath.affliction); } break; - - case ClientNetObject.ENTITY_STATE: - int eventType = msg.ReadRangedInteger(0, 4); - switch (eventType) + case EventType.UpdateTalents: + if (c.Character != this) { - case 0: - Inventory.ServerRead(type, msg, c); - break; - case 1: - bool doingCPR = msg.ReadBoolean(); - if (c.Character != this) - { #if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); + DebugConsole.Log("Received a character update message from a client who's not controlling the character"); #endif - return; - } + return; + } - AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None; - break; - case 2: - if (c.Character != this) - { -#if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); -#endif - return; - } + // get the full list of talents from the player, only give the ones + // that are not already given (or otherwise not viable) + ushort talentCount = msg.ReadUInt16(); + List talentSelection = new List(); + for (int i = 0; i < talentCount; i++) + { + UInt32 talentIdentifier = msg.ReadUInt32(); + var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier); + if (prefab == null) { continue; } - if (IsIncapacitated) - { - var causeOfDeath = CharacterHealth.GetCauseOfDeath(); - Kill(causeOfDeath.type, causeOfDeath.affliction); - } - break; - case 3: // NetEntityEvent.Type.UpdateTalents - if (c.Character != this) - { -#if DEBUG - DebugConsole.Log("Received a character update message from a client who's not controlling the character"); -#endif - return; - } - - // get the full list of talents from the player, only give the ones - // that are not already given (or otherwise not viable) - ushort talentCount = msg.ReadUInt16(); - List talentSelection = new List(); - for (int i = 0; i < talentCount; i++) - { - UInt32 talentIdentifier = msg.ReadUInt32(); - var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UintIdentifier == talentIdentifier); - if (prefab == null) { continue; } - - if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) - { - GiveTalent(prefab.Identifier); - talentSelection.Add(prefab.Identifier); - } - } - if (talentSelection.Count != talentCount) - { - DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})"); - } - break; + if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) + { + GiveTalent(prefab.Identifier); + talentSelection.Add(prefab.Identifier); + } + } + if (talentSelection.Count != talentCount) + { + DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})"); } break; } - msg.ReadPadBits(); } - public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c) { - if (GameMain.Server == null) return; + msg.Write(ID); - if (extraData != null) + IWriteMessage tempBuffer = new WriteOnlyMessage(); + + if (this == c.Character) { - const int min = 0, max = 13; - switch ((NetEntityEvent.Type)extraData[0]) + tempBuffer.Write(true); + if (LastNetworkUpdateID < memInput.Count + 1) { - case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, min, max); - msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - Inventory.ServerWrite(msg, c); - break; - case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, min, max); - Client owner = (Client)extraData[1]; - msg.Write(owner == c && owner.Character == this); - msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); - break; - case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, min, max); - WriteStatus(msg); - break; - case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, min, max); - if (Info?.Job == null) - { - msg.Write((byte)0); - } - else - { - msg.Write((byte)Info.Job.Skills.Count); - foreach (Skill skill in Info.Job.Skills) - { - msg.Write(skill.Identifier); - msg.Write(skill.Level); - } - } - break; - case NetEntityEvent.Type.SetAttackTarget: - case NetEntityEvent.Type.ExecuteAttack: - Limb attackLimb = extraData[1] as Limb; - UInt16 targetEntityID = (UInt16)extraData[2]; - int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; - msg.WriteRangedInteger(extraData[0] is NetEntityEvent.Type.SetAttackTarget ? 4 : 5, min, max); - msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb))); - msg.Write(targetEntityID); - msg.Write((byte)targetLimbIndex); - msg.Write(extraData.Length > 4 ? (float)extraData[4] : 0); - msg.Write(extraData.Length > 5 ? (float)extraData[5] : 0); - break; - case NetEntityEvent.Type.AssignCampaignInteraction: - msg.WriteRangedInteger(6, min, max); - msg.Write((byte)CampaignInteractionType); - msg.Write(RequireConsciousnessForCustomInteract); - break; - case NetEntityEvent.Type.ObjectiveManagerState: - msg.WriteRangedInteger(7, min, max); - int type = (extraData[1] as string) switch - { - "order" => 1, - "objective" => 2, - _ => 0 - }; - msg.WriteRangedInteger(type, 0, 2); - if (!(AIController is HumanAIController controller)) - { - msg.Write(false); - break; - } - if (type == 1) - { - var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); - bool validOrder = currentOrderInfo != null; - msg.Write(validOrder); - if (!validOrder) { break; } - var orderPrefab = currentOrderInfo.Prefab; - msg.Write(orderPrefab.UintIdentifier); - if (!orderPrefab.HasOptions) { break; } - int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); - if (optionIndex == -1) - { - DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\"."); - } - msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); - } - else if (type == 2) - { - var objective = controller.ObjectiveManager.CurrentObjective; - bool validObjective = objective != null && objective.Identifier != Identifier.Empty; - msg.Write(validObjective); - if (!validObjective) { break; } - msg.Write(objective.Identifier); - msg.Write(objective.Option); - UInt16 targetEntityId = 0; - if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) - { - targetEntityId = operateObjective.OperateTarget.ID; - } - msg.Write(targetEntityId); - } - break; - case NetEntityEvent.Type.TeamChange: - msg.WriteRangedInteger(8, min, max); - msg.Write((byte)TeamID); - break; - case NetEntityEvent.Type.AddToCrew: - msg.WriteRangedInteger(9, min, max); - msg.Write((byte)(CharacterTeamType)extraData[1]); // team id - ushort[] inventoryItemIDs = (ushort[])extraData[2]; - msg.Write((ushort)inventoryItemIDs.Length); - for (int i = 0; i < inventoryItemIDs.Length; i++) - { - msg.Write(inventoryItemIDs[i]); - } - break; - case NetEntityEvent.Type.UpdateExperience: - msg.WriteRangedInteger(10, min, max); - msg.Write(Info.ExperiencePoints); - break; - case NetEntityEvent.Type.UpdateTalents: - msg.WriteRangedInteger(11, min, max); - msg.Write((ushort)characterTalents.Count); - foreach (var unlockedTalent in characterTalents) - { - msg.Write(unlockedTalent.AddedThisRound); - msg.Write(unlockedTalent.Prefab.UintIdentifier); - } - break; - case NetEntityEvent.Type.UpdateMoney: - msg.WriteRangedInteger(12, min, max); - msg.Write(GameMain.GameSession.Campaign.Money); - break; - case NetEntityEvent.Type.UpdatePermanentStats: - msg.WriteRangedInteger(13, min, max); - if (Info == null || extraData.Length < 2 || !(extraData[1] is StatTypes statType)) - { - msg.Write((byte)0); - msg.Write((byte)0); - } - else if (!Info.SavedStatValues.ContainsKey(statType)) - { - msg.Write((byte)0); - msg.Write((byte)statType); - } - else - { - msg.Write((byte)Info.SavedStatValues[statType].Count); - msg.Write((byte)statType); - foreach (var savedStatValue in Info.SavedStatValues[statType]) - { - msg.Write(savedStatValue.StatIdentifier); - msg.Write(savedStatValue.StatValue); - msg.Write(savedStatValue.RemoveOnDeath); - } - } - break; - default: - DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); - break; + tempBuffer.Write((UInt16)0); + } + else + { + tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1)); } - msg.WritePadBits(); } else { - msg.Write(ID); + tempBuffer.Write(false); - IWriteMessage tempBuffer = new WriteOnlyMessage(); + bool aiming = false; + bool use = false; + bool attack = false; + bool shoot = false; - if (this == c.Character) + if (IsRemotePlayer) { - tempBuffer.Write(true); - if (LastNetworkUpdateID < memInput.Count + 1) + aiming = dequeuedInput.HasFlag(InputNetFlags.Aim); + use = dequeuedInput.HasFlag(InputNetFlags.Use); + attack = dequeuedInput.HasFlag(InputNetFlags.Attack); + shoot = dequeuedInput.HasFlag(InputNetFlags.Shoot); + } + else if (keys != null) + { + aiming = keys[(int)InputType.Aim].GetHeldQueue; + use = keys[(int)InputType.Use].GetHeldQueue; + attack = keys[(int)InputType.Attack].GetHeldQueue; + shoot = keys[(int)InputType.Shoot].GetHeldQueue; + networkUpdateSent = true; + } + + tempBuffer.Write(aiming); + tempBuffer.Write(shoot); + tempBuffer.Write(use); + if (AnimController is HumanoidAnimController) + { + tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching); + } + tempBuffer.Write(attack); + + Vector2 relativeCursorPos = cursorPosition - AimRefPosition; + tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); + + tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); + + tempBuffer.Write(AnimController.Dir > 0.0f); + } + + if (SelectedCharacter != null || SelectedConstruction != null) + { + tempBuffer.Write(true); + tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID); + tempBuffer.Write(SelectedConstruction != null ? SelectedConstruction.ID : NullEntityID); + if (SelectedCharacter != null) + { + tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR); + } + } + else + { + tempBuffer.Write(false); + } + + tempBuffer.Write(SimPosition.X); + tempBuffer.Write(SimPosition.Y); + float MaxVel = NetConfig.MaxPhysicsBodyVelocity; + AnimController.Collider.LinearVelocity = new Vector2( + MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel), + MathHelper.Clamp(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel)); + tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12); + tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); + + bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; + tempBuffer.Write(fixedRotation); + if (!fixedRotation) + { + tempBuffer.Write(AnimController.Collider.Rotation); + float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; + AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); + tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8); + } + + bool writeStatus = healthUpdateTimer <= 0.0f; + tempBuffer.Write(writeStatus); + if (writeStatus) + { + WriteStatus(tempBuffer); + AIController?.ServerWrite(tempBuffer); + HealthUpdatePending = false; + } + + tempBuffer.WritePadBits(); + + msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); + msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + } + + public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed character event: expected {nameof(Character)}.{nameof(IEventData)}, got {extraData?.GetType().Name ?? "[NULL]"}"); } + + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventData) + { + case InventoryStateEventData _: + msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); + Inventory.ServerEventWrite(msg, c); + break; + case ControlEventData controlEventData: + Client owner = controlEventData.Owner; + msg.Write(owner == c && owner.Character == this); + msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); + break; + case StatusEventData _: + WriteStatus(msg); + break; + case UpdateSkillsEventData _: + if (Info?.Job == null) { - tempBuffer.Write((UInt16)0); + msg.Write((byte)0); } else { - tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1)); + msg.Write((byte)Info.Job.Skills.Count); + foreach (Skill skill in Info.Job.Skills) + { + msg.Write(skill.Identifier); + msg.Write(skill.Level); + } } - } - else - { - tempBuffer.Write(false); - - bool aiming = false; - bool use = false; - bool attack = false; - bool shoot = false; - - if (IsRemotePlayer) + break; + case IAttackEventData attackEventData: { - aiming = dequeuedInput.HasFlag(InputNetFlags.Aim); - use = dequeuedInput.HasFlag(InputNetFlags.Use); - attack = dequeuedInput.HasFlag(InputNetFlags.Attack); - shoot = dequeuedInput.HasFlag(InputNetFlags.Shoot); + int attackLimbIndex = Removed ? -1 : Array.IndexOf(AnimController.Limbs, attackEventData.AttackLimb); + ushort targetEntityId = 0; + int targetLimbIndex = -1; + if (attackEventData.TargetEntity is Entity { Removed: false } targetEntity) + { + targetEntityId = targetEntity.ID; + if (targetEntity is Character { AnimController: { Limbs: var targetLimbsArray } }) + { + targetLimbIndex = targetLimbsArray.IndexOf(attackEventData.TargetLimb); + } + } + msg.Write((byte)(attackLimbIndex < 0 ? 255 : attackLimbIndex)); + msg.Write((ushort)targetEntityId); + msg.Write((byte)(targetLimbIndex < 0 ? 255 : targetLimbIndex)); + msg.Write(attackEventData.TargetSimPos.X); + msg.Write(attackEventData.TargetSimPos.Y); } - else if (keys != null) + break; + case AssignCampaignInteractionEventData _: + msg.Write((byte)CampaignInteractionType); + msg.Write(RequireConsciousnessForCustomInteract); + break; + case ObjectiveManagerStateEventData objectiveManagerStateEventData: + AIObjectiveManager.ObjectiveType type = objectiveManagerStateEventData.ObjectiveType; + msg.WriteRangedInteger((int)type, (int)AIObjectiveManager.ObjectiveType.MinValue, (int)AIObjectiveManager.ObjectiveType.MaxValue); + if (!(AIController is HumanAIController controller)) { - aiming = keys[(int)InputType.Aim].GetHeldQueue; - use = keys[(int)InputType.Use].GetHeldQueue; - attack = keys[(int)InputType.Attack].GetHeldQueue; - shoot = keys[(int)InputType.Shoot].GetHeldQueue; - networkUpdateSent = true; + msg.Write(false); + break; } - - tempBuffer.Write(aiming); - tempBuffer.Write(shoot); - tempBuffer.Write(use); - if (AnimController is HumanoidAnimController) + if (type == AIObjectiveManager.ObjectiveType.Order) { - tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching); + var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); + bool validOrder = currentOrderInfo != null; + msg.Write(validOrder); + if (!validOrder) { break; } + var orderPrefab = currentOrderInfo.Prefab; + msg.Write(orderPrefab.UintIdentifier); + if (!orderPrefab.HasOptions) { break; } + int optionIndex = orderPrefab.AllOptions.IndexOf(currentOrderInfo.Option); + if (optionIndex == -1) + { + DebugConsole.AddWarning($"Error while writing order data. Order option \"{currentOrderInfo.Option}\" not found in the order prefab \"{orderPrefab.Name}\"."); + } + msg.WriteRangedInteger(optionIndex, -1, orderPrefab.AllOptions.Length); } - tempBuffer.Write(attack); - - Vector2 relativeCursorPos = cursorPosition - AimRefPosition; - tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); - - tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); - - tempBuffer.Write(AnimController.Dir > 0.0f); - } - - if (SelectedCharacter != null || SelectedConstruction != null) - { - tempBuffer.Write(true); - tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : NullEntityID); - tempBuffer.Write(SelectedConstruction != null ? SelectedConstruction.ID : NullEntityID); - if (SelectedCharacter != null) + else if (type == AIObjectiveManager.ObjectiveType.Objective) { - tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR); + var objective = controller.ObjectiveManager.CurrentObjective; + bool validObjective = objective?.Identifier is { IsEmpty: false }; + msg.Write(validObjective); + if (!validObjective) { break; } + msg.Write(objective.Identifier); + msg.Write(objective.Option); + UInt16 targetEntityId = 0; + if (objective is AIObjectiveOperateItem operateObjective && operateObjective.OperateTarget != null) + { + targetEntityId = operateObjective.OperateTarget.ID; + } + msg.Write(targetEntityId); } - } - else - { - tempBuffer.Write(false); - } - - tempBuffer.Write(SimPosition.X); - tempBuffer.Write(SimPosition.Y); - float MaxVel = NetConfig.MaxPhysicsBodyVelocity; - AnimController.Collider.LinearVelocity = new Vector2( - MathHelper.Clamp(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel), - MathHelper.Clamp(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel)); - tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.X, -MaxVel, MaxVel, 12); - tempBuffer.WriteRangedSingle(AnimController.Collider.LinearVelocity.Y, -MaxVel, MaxVel, 12); - - bool fixedRotation = AnimController.Collider.FarseerBody.FixedRotation || !AnimController.Collider.PhysEnabled; - tempBuffer.Write(fixedRotation); - if (!fixedRotation) - { - tempBuffer.Write(AnimController.Collider.Rotation); - float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; - AnimController.Collider.AngularVelocity = NetConfig.Quantize(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel, 8); - tempBuffer.WriteRangedSingle(MathHelper.Clamp(AnimController.Collider.AngularVelocity, -MaxAngularVel, MaxAngularVel), -MaxAngularVel, MaxAngularVel, 8); - } - - bool writeStatus = healthUpdateTimer <= 0.0f; - tempBuffer.Write(writeStatus); - if (writeStatus) - { - WriteStatus(tempBuffer); - AIController?.ServerWrite(tempBuffer); - HealthUpdatePending = false; - } - - tempBuffer.WritePadBits(); - - msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); - msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); + break; + case TeamChangeEventData _: + msg.Write((byte)TeamID); + break; + case AddToCrewEventData addToCrewEventData: + msg.Write((byte)addToCrewEventData.TeamType); // team id + ushort[] inventoryItemIDs = addToCrewEventData.InventoryItems.Select(item => item.ID).ToArray(); + msg.Write((ushort)inventoryItemIDs.Length); + for (int i = 0; i < inventoryItemIDs.Length; i++) + { + msg.Write(inventoryItemIDs[i]); + } + break; + case UpdateExperienceEventData _: + msg.Write(Info.ExperiencePoints); + break; + case UpdateTalentsEventData _: + msg.Write((ushort)characterTalents.Count); + foreach (var unlockedTalent in characterTalents) + { + msg.Write(unlockedTalent.AddedThisRound); + msg.Write(unlockedTalent.Prefab.UintIdentifier); + } + break; + case UpdateMoneyEventData _: + msg.Write(GameMain.GameSession.Campaign.GetWallet(c).Balance); + break; + case UpdatePermanentStatsEventData updatePermanentStatsEventData: + StatTypes statType = updatePermanentStatsEventData.StatType; + if (Info == null) + { + msg.Write((byte)0); + msg.Write((byte)0); + } + else if (!Info.SavedStatValues.ContainsKey(statType)) + { + msg.Write((byte)0); + msg.Write((byte)statType); + } + else + { + msg.Write((byte)Info.SavedStatValues[statType].Count); + msg.Write((byte)statType); + foreach (var savedStatValue in Info.SavedStatValues[statType]) + { + msg.Write(savedStatValue.StatIdentifier); + msg.Write(savedStatValue.StatValue); + msg.Write(savedStatValue.RemoveOnDeath); + } + } + break; + default: + throw new Exception($"Malformed character event: did not expect {eventData.GetType().Name}"); } } @@ -656,6 +636,8 @@ namespace Barotrauma { msg.Write(true); msg.Write(ownerClient.ID); + msg.Write(Wallet.Balance); + msg.WriteRangedInteger(Wallet.RewardDistribution, 0, 100); } else if (GameMain.Server.Character == this) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index c3ad6d244..c1a0eff87 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1,16 +1,12 @@ -using Barotrauma.Networking; +using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Collections.Generic; -using System.ComponentModel; -using FarseerPhysics; -using Barotrauma.Items.Components; -using System.Threading; -using Barotrauma.IO; -using System.Text; using System.Diagnostics; using System.Globalization; +using System.Linq; +using System.Text; namespace Barotrauma { @@ -1187,7 +1183,7 @@ namespace Barotrauma NewMessage("*****************", Color.Lime); GameServer.Log("Console command \"restart\" executed: closing the server...", ServerLog.MessageType.ServerMessage); GameMain.Instance.CloseServer(); - GameMain.Instance.TryStartChildServerRelay(); + Program.TryStartChildServerRelay(GameMain.Instance.CommandLineArgs); GameMain.Instance.StartServer(); })); @@ -1400,17 +1396,71 @@ namespace Barotrauma commands.Add(new Command("eventdata", "", (string[] args) => { if (args.Length == 0) { return; } - if (!UInt16.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ushort eventId)) { return; } - ServerEntityEvent ev = GameMain.Server.EntityEventManager.Events.Find(ev => ev.ID == eventId); - if (ev != null) + + string indentStr(string s) + => string.Join('\n', s.Split('\n').Select(sub => $" {sub}")); + + string eventDataRip(object data) { + if (data is null) { return "[NULL]"; } + var type = data.GetType(); + + string retVal = $"{type.FullName} "; + + if (type.IsPrimitive + || type.IsEnum + || type.IsClass) + { + retVal += data.ToString(); + return retVal; + } + + retVal += "{\n"; + var fields = data.GetType().GetFields(); + foreach (var field in fields) + { + retVal += indentStr($"{field.Name}: {eventDataRip(field.GetValue(data))}")+"\n"; + } + + retVal += "}"; + retVal = retVal.Replace("{\n}", "{ }"); + return retVal; + } + + string eventDebugStr(ServerEntityEvent ev) + { + ushort eventId = ev.ID; + string entityData = ""; if (ev.Entity is { ID: var entityId, Removed: var removed, IdFreed: var idFreed }) { - entityData = $"Entity ID: {entityId}; Entity removed: {removed}; Entity ID freed: {idFreed}"; + entityData = $"Entity ID: {entityId}\n" + + $"Entity type {ev.Entity.GetType().Name}\n" + + $"Entity removed: {removed}\n" + + $"Entity ID freed: {idFreed}\n" + + $"Event data: {eventDataRip(ev.Data)}\n"; } - NewMessage($"EventData {eventId}\n{entityData}", Color.Lime); - //NewMessage(ev.StackTrace.CleanupStackTrace(), Color.Lime); + + return $"EventData {eventId}\n{indentStr(entityData)}"; + } + + IReadOnlyList events = GameMain.Server.EntityEventManager.Events; + ushort? eventId = null; + if (args[0].Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + eventId = events.Max(e => e.ID); + } + else if (UInt16.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ushort id)) + { + eventId = id; + } + IEnumerable matchedEvents = GameMain.Server.EntityEventManager.Events.Where(ev + => eventId.HasValue + ? ev.ID == eventId + : eventDebugStr(ev).Contains(args[0], StringComparison.OrdinalIgnoreCase)); + foreach (var ev in matchedEvents) + { + NewMessage(eventDebugStr(ev), Color.Lime); } })); @@ -1665,28 +1715,27 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { if (args.Length < 2) return; - - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => - a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier == args[0]); + string affliction = args[0]; + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction); if (afflictionPrefab == null) { - GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client, Color.Red); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + } + if (afflictionPrefab == null) + { + GameMain.Server.SendConsoleMessage("Affliction \"" + affliction + "\" not found.", client, Color.Red); return; } - if (!float.TryParse(args[1], out float afflictionStrength)) { GameMain.Server.SendConsoleMessage("\"" + args[1] + "\" is not a valid affliction strength.", client, Color.Red); return; } - bool relativeStrength = false; if (args.Length > 4) { bool.TryParse(args[4], out relativeStrength); } - Character targetCharacter = (args.Length <= 2) ? client.Character : FindMatchingCharacter(args.Skip(2).ToArray()); if (targetCharacter != null) { @@ -2217,18 +2266,50 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); return; } + + Character targetCharacter = null; + + if (args.Length >= 2) + { + targetCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + } + if (int.TryParse(args[0], out int money)) { - campaign.Money += money; + Wallet wallet = targetCharacter is null ? campaign.Bank : targetCharacter.Wallet; + wallet.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); campaign.LastUpdateID++; } else { GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient, Color.Red); - } + } } ); + + AssignOnClientRequestExecute( + "showmoney", + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) + { + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient, Color.Red); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.Append($"Bank: {campaign.Bank.Balance}"); + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (client.Character is null) { continue; } + sb.Append(Environment.NewLine); + sb.Append($"{client.Name}: {client.Character.Wallet.Balance}"); + } + GameMain.Server.SendConsoleMessage(sb.ToString(), senderClient); + } + ); + AssignOnClientRequestExecute( "campaigndestination|setcampaigndestination", (Client senderClient, Vector2 cursorWorldPos, string[] args) => @@ -2327,7 +2408,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", senderClient); } - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdateSkills }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData()); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 236257000..532d46b51 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -141,20 +141,6 @@ namespace Barotrauma }*/ } - public bool TryStartChildServerRelay() - { - for (int i = 0; i < CommandLineArgs.Length; i++) - { - switch (CommandLineArgs[i].Trim()) - { - case "-pipes": - ChildServerRelay.Start(CommandLineArgs[i + 2], CommandLineArgs[i + 1]); - return true; - } - } - return false; - } - public void StartServer() { string name = "Server"; @@ -299,7 +285,6 @@ namespace Barotrauma Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Items.Components.ItemComponent)); Hyper.ComponentModel.HyperTypeDescriptionProvider.Add(typeof(Hull)); - TryStartChildServerRelay(); Init(); StartServer(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index b888b2139..00190b2fd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -1,12 +1,13 @@ using Barotrauma.Extensions; using System.Collections.Generic; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(List itemsToSell) + public void SellBackPurchasedItems(List itemsToSell, Client client = null) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction @@ -15,12 +16,12 @@ namespace Barotrauma { var itemValue = item.Quantity * buyValues[item.ItemPrefab]; Location.StoreCurrentBalance -= itemValue; - campaign.Money += itemValue; + campaign.GetWallet(client).Give(itemValue); PurchasedItems.Remove(item); } } - public void BuyBackSoldItems(List itemsToBuy) + public void BuyBackSoldItems(List itemsToBuy, Client client) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction @@ -30,12 +31,12 @@ namespace Barotrauma int itemValue = sellValues[item.ItemPrefab]; if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } Location.StoreCurrentBalance += itemValue; - campaign.Money -= itemValue; + campaign.Bank.TryDeduct(itemValue); SoldItems.Remove(item); } } - public void SellItems(List itemsToSell) + public void SellItems(List itemsToSell, Client client) { bool canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; IEnumerable sellableItemsInSub = Enumerable.Empty(); @@ -67,7 +68,7 @@ namespace Barotrauma } SoldItems.Add(item); Location.StoreCurrentBalance -= itemValue; - campaign.Money += itemValue; + campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); } OnSoldItemsChanged?.Invoke(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs new file mode 100644 index 000000000..7c0767001 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/Data/Wallet.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Barotrauma +{ + internal partial class Wallet + { + private readonly Queue transactions = new Queue(); + + partial void SettingsChanged(Option balanceChanged, Option rewardChanged) + { + transactions.Enqueue(new WalletChangedData + { + BalanceChanged = balanceChanged, + RewardDistributionChanged = rewardChanged + }); + } + + public bool HasTransactions() => transactions.Count > 0; + + public NetWalletTransaction DequeueAndMergeTransactions(ushort id) + { + Option targetCharacterID = id == Entity.NullEntityID ? Option.None() : Option.Some(id); + + WalletChangedData changedData = new WalletChangedData + { + BalanceChanged = Option.None(), + RewardDistributionChanged = Option.None() + }; + + while (transactions.TryDequeue(out WalletChangedData transactionOut)) + { + changedData = changedData.MergeInto(transactionOut); + } + + return new NetWalletTransaction + { + CharacterID = targetCharacterID, + ChangedData = changedData, + Info = CreateWalletInfo() + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index f8c39e187..98015ddf0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System.Globalization; using System.Xml.Linq; namespace Barotrauma @@ -32,8 +31,26 @@ namespace Barotrauma { CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData); } + + if (client.Character?.Wallet.Save() is { } walletSave) + { + WalletData = walletSave; + } } + public void Refresh(Character character) + { + healthData = new XElement("health"); + character.CharacterHealth.Save(healthData); + if (character.Inventory != null) + { + itemData = new XElement("inventory"); + Character.SaveInventory(character.Inventory, itemData); + } + OrderData = new XElement("orders"); + CharacterInfo.SaveOrderData(character.Info, OrderData); + WalletData = character.Wallet.Save(); + } public CharacterCampaignData(XElement element) { @@ -63,6 +80,9 @@ namespace Barotrauma case "orders": OrderData = subElement; break; + case Wallet.LowerCaseSaveElementName: + WalletData = subElement; + break; } } } @@ -98,7 +118,7 @@ namespace Barotrauma } public void ApplyHealthData(Character character) - { + { CharacterInfo.ApplyHealthData(character, healthData); } @@ -106,5 +126,26 @@ namespace Barotrauma { CharacterInfo.ApplyOrderData(character, OrderData); } + + public void ApplyWalletData(Character character) + { + character.Wallet = new Wallet(WalletData); + } + + public XElement Save() + { + XElement element = new XElement("CharacterCampaignData", + new XAttribute("name", Name), + new XAttribute("endpoint", ClientEndPoint), + new XAttribute("steamid", SteamID)); + + CharacterInfo?.Save(element); + if (itemData != null) { element.Add(itemData); } + if (healthData != null) { element.Add(healthData); } + if (OrderData != null) { element.Add(OrderData); } + if (WalletData != null) { element.Add(WalletData); } + + return element; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index f736290f3..fbd7d030e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -12,6 +12,22 @@ namespace Barotrauma partial class MultiPlayerCampaign : CampaignMode { private readonly List characterData = new List(); + private readonly Dictionary walletsToCheck = new Dictionary(); + private readonly HashSet transactions = new HashSet(); + private const float clientCheckInterval = 10; + private float clientCheckTimer = clientCheckInterval; + + public override Wallet GetWallet(Client client = null) + { + if (client is null) { throw new ArgumentNullException(nameof(client), "Client should not be null in multiplayer"); } + + if (client.Character is { } character) + { + return character.Wallet; + } + + return Wallet.Invalid; + } private bool forceMapUI; public bool ForceMapUI @@ -229,8 +245,7 @@ namespace Barotrauma characterInfo.CauseOfDeath = null; } c.CharacterInfo = characterInfo; - characterData.RemoveAll(cd => cd.MatchesClient(c)); - characterData.Add(new CharacterCampaignData(c)); + SetClientCharacterData(c); } //refresh the character data of clients who aren't in the server anymore @@ -412,8 +427,8 @@ namespace Barotrauma LastSaveID++; } - public bool CanPurchaseSub(SubmarineInfo info) - => info.Price <= Money && GetCampaignSubs().Contains(info); + public bool CanPurchaseSub(SubmarineInfo info, Client client) + => GetWallet(client).CanAfford(info.Price) && GetCampaignSubs().Contains(info); public void DiscardClientCharacterData(Client client) { @@ -487,6 +502,54 @@ namespace Barotrauma KeepCharactersCloseToOutpost(deltaTime); } } + + UpdateClientsToCheck(deltaTime); + UpdateWallets(); + } + + private void UpdateClientsToCheck(float deltaTime) + { + if (clientCheckTimer < clientCheckInterval) + { + clientCheckTimer += deltaTime; + return; + } + + clientCheckTimer = 0; + walletsToCheck.Clear(); + walletsToCheck.Add(0, Bank); + + foreach (Character character in Mission.GetSalaryEligibleCrew()) + { + walletsToCheck.Add(character.ID, character.Wallet); + } + } + + private void UpdateWallets() + { + foreach (var (id, wallet) in walletsToCheck) + { + if (wallet.HasTransactions()) + { + transactions.Add(wallet.DequeueAndMergeTransactions(id)); + } + } + + if (transactions.Count == 0) { return; } + + NetWalletUpdate walletUpdate = new NetWalletUpdate + { + Transactions = transactions.ToArray() + }; + + transactions.Clear(); + + foreach (Client client in GameMain.Server.ConnectedClients) + { + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.MONEY); + ((INetSerializableStruct)walletUpdate).Write(msg); + GameMain.Server?.ServerPeer?.Send(msg, client.Connection, DeliveryMethod.Reliable); + } } public override void End(TransitionType transitionType = TransitionType.None) @@ -530,7 +593,6 @@ namespace Barotrauma msg.Write(ForceMapUI); - msg.Write(Money); msg.Write(PurchasedHullRepairs); msg.Write(PurchasedItemRepairs); msg.Write(PurchasedLostShuttles); @@ -644,7 +706,7 @@ namespace Barotrauma { string itemPrefabIdentifier = msg.ReadString(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); } UInt16 subSellCrateItemCount = msg.ReadUInt16(); @@ -653,7 +715,7 @@ namespace Barotrauma { string itemPrefabIdentifier = msg.ReadString(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); } UInt16 purchasedItemCount = msg.ReadUInt16(); @@ -662,7 +724,7 @@ namespace Barotrauma { string itemPrefabIdentifier = msg.ReadString(); int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); + purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); } UInt16 soldItemCount = msg.ReadUInt16(); @@ -711,57 +773,63 @@ namespace Barotrauma int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; - if (purchasedHullRepairs != this.PurchasedHullRepairs) + Wallet personalWallet = GetWallet(sender); + + if (purchasedHullRepairs != PurchasedHullRepairs) { - if (purchasedHullRepairs && Money >= hullRepairCost) + switch (purchasedHullRepairs) { - this.PurchasedHullRepairs = true; - Money -= hullRepairCost; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - } - else if (!purchasedHullRepairs) - { - this.PurchasedHullRepairs = false; - Money += hullRepairCost; + case true when personalWallet.CanAfford(hullRepairCost): + personalWallet.Deduct(hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); + break; + case false: + PurchasedHullRepairs = false; + personalWallet.Refund(hullRepairCost); + break; } } - if (purchasedItemRepairs != this.PurchasedItemRepairs) + + if (purchasedItemRepairs != PurchasedItemRepairs) { - if (purchasedItemRepairs && Money >= itemRepairCost) + switch (purchasedItemRepairs) { - this.PurchasedItemRepairs = true; - Money -= itemRepairCost; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - } - else if (!purchasedItemRepairs) - { - this.PurchasedItemRepairs = false; - Money += itemRepairCost; + case true when personalWallet.CanAfford(itemRepairCost): + personalWallet.Deduct(itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); + break; + case false: + PurchasedItemRepairs = false; + personalWallet.Refund(itemRepairCost); + break; } } - if (purchasedLostShuttles != this.PurchasedLostShuttles) + + if (purchasedLostShuttles != PurchasedLostShuttles) { - if (GameMain.GameSession?.SubmarineInfo != null && - GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) + if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - else if (purchasedLostShuttles && Money >= shuttleRetrieveCost) + else if (purchasedLostShuttles && personalWallet.TryDeduct(shuttleRetrieveCost)) { - this.PurchasedLostShuttles = true; - Money -= shuttleRetrieveCost; + PurchasedLostShuttles = true; GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } else if (!purchasedItemRepairs) { - this.PurchasedLostShuttles = false; - Money += shuttleRetrieveCost; + PurchasedLostShuttles = false; + personalWallet.Refund(shuttleRetrieveCost); } } + if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) { Map.SetLocation(currentLocIndex); } + Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } @@ -772,18 +840,18 @@ namespace Barotrauma if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) { var currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); - currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity)); - buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity)); + currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity, sender)); + buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity, sender)); CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); - CargoManager.PurchaseItems(purchasedItems, false); + CargoManager.PurchaseItems(purchasedItems, false, sender); } bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) { var currentSubSellCrateItems = new List(CargoManager.ItemsInSellFromSubCrate); - currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity)); - subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity)); + currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity, sender)); + subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity, sender)); } bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); @@ -791,29 +859,29 @@ namespace Barotrauma { // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all // sold items that are removed so they should be discarded on the next message - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems)); - CargoManager.SellItems(soldItems); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems), sender); + CargoManager.SellItems(soldItems, sender); } else if (allowedToSellInventoryItems || allowedToSellSubItems) { if (allowedToSellInventoryItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character))); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character)), sender); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); } else { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine))); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine)), sender); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); } - CargoManager.SellItems(soldItems); + CargoManager.SellItems(soldItems, sender); } if (allowedToManageCampaign) { foreach (var (prefab, category, _) in purchasedUpgrades) { - UpgradeManager.PurchaseUpgrade(prefab, category); + UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); // unstable logging int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); @@ -828,7 +896,7 @@ namespace Barotrauma } else { - UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall); + UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender); } } foreach (Item item in Item.ItemList) @@ -842,6 +910,64 @@ namespace Barotrauma } } + public void ServerReadMoney(IReadMessage msg, Client sender) + { + NetWalletTransfer transfer = INetSerializableStruct.Read(msg); + + switch (transfer.Sender) + { + case Some { Value: var id }: + + if (id != sender.CharacterID && !AllowedToManageCampaign(sender)) { return; } + + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } + + TransferMoney(wallet); + break; + + case None _: + if (!AllowedToManageCampaign(sender)) { return; } + + TransferMoney(Bank); + break; + } + + void TransferMoney(Wallet from) + { + if (!from.TryDeduct(transfer.Amount)) { return; } + + switch (transfer.Receiver) + { + case Some { Value: var id }: + Wallet wallet = GetWalletByID(id); + if (wallet is InvalidWallet) { return; } + + wallet.Give(transfer.Amount); + break; + case None _: + Bank.Give(transfer.Amount); + break; + } + } + + Wallet GetWalletByID(ushort id) + { + Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id); + return targetCharacter is null ? Wallet.Invalid : targetCharacter.Wallet; + } + } + + public void ServerReadRewardDistribution(IReadMessage msg, Client sender) + { + NetWalletSalaryUpdate update = INetSerializableStruct.Read(msg); + + if (!AllowedToManageCampaign(sender)) { return; } + + Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); + targetCharacter?.Wallet.SetRewardDistrubiton(update.NewRewardDistribution); + } + public void ServerReadCrew(IReadMessage msg, Client sender) { int[] pendingHires = null; @@ -928,7 +1054,7 @@ namespace Barotrauma { foreach (CharacterInfo hireInfo in location.HireManager.PendingHires) { - if (TryHireCharacter(location, hireInfo)) + if (TryHireCharacter(location, hireInfo, sender)) { hiredCharacters.Add(hireInfo); }; @@ -1045,7 +1171,6 @@ namespace Barotrauma { element.Add(new XAttribute("campaignid", CampaignID)); XElement modeElement = new XElement("MultiPlayerCampaign", - new XAttribute("money", Money), new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), @@ -1053,6 +1178,7 @@ namespace Barotrauma modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); + modeElement.Add(Bank.Save()); CampaignMetadata?.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index 3e2afe845..6f19e2077 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -85,7 +85,7 @@ namespace Barotrauma { if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } - HealRequestResult result = HealAllPending(); + HealRequestResult result = HealAllPending(client: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index eb8aa8e58..aa7abb66a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components { partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(docked); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index afb05dbc6..3edb8ff4e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -6,6 +6,16 @@ namespace Barotrauma.Items.Components { partial class Door { + private readonly struct EventData : IEventData + { + public readonly bool ForcedOpen; + + public EventData(bool forcedOpen) + { + ForcedOpen = forcedOpen; + } + } + partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen) { if (IsStuck || isOpen == open) @@ -19,17 +29,18 @@ namespace Barotrauma.Items.Components if (sendNetworkMessage) { - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), forcedOpen }); + item.CreateServerEvent(this, new EventData(forcedOpen)); } } - public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - base.ServerWrite(msg, c, extraData); + bool forcedOpen = TryExtractEventData(extraData, out var eventData) && eventData.ForcedOpen; + base.ServerEventWrite(msg, c, extraData); msg.Write(isOpen); msg.Write(isBroken); - msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open + msg.Write(forcedOpen); //forced open msg.Write(isStuck); msg.Write(isJammed); msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs index 6548e6046..ff82cc9d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class GeneticMaterial : ItemComponent { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(tainted); if (tainted) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs index cac426158..8fd930fc8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Growable.cs @@ -6,6 +6,16 @@ namespace Barotrauma.Items.Components { internal partial class Growable { + private readonly struct EventData : IEventData + { + public readonly int Offset; + + public EventData(int offset) + { + Offset = offset; + } + } + private const int serverHealthUpdateDelay = 10; private int serverHealthUpdateTimer; @@ -25,11 +35,12 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedSingle(Health, 0f, (float) MaxHealth, 8); - if (extraData != null && extraData.Length >= 3 && extraData[2] is int offset) + if (TryExtractEventData(extraData, out EventData eventData)) { + int offset = eventData.Offset; int amountToSend = Math.Min(Vines.Count - offset, VineChunkSize); msg.WriteRangedInteger(offset, -1, MaximumVines); msg.WriteRangedInteger(amountToSend, 0, VineChunkSize); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index a30115996..0b8521700 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -5,9 +5,9 @@ namespace Barotrauma.Items.Components { partial class Holdable : Pickable, IServerSerializable, IClientSerializable { - public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - base.ServerWrite(msg, c, extraData); + base.ServerEventWrite(msg, c, extraData); bool writeAttachData = attachable && body != null; msg.Write(writeAttachData); @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs index a1f9c67df..3d372c206 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/LevelResource.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { private float lastSentDeattachTimer; - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(deattachTimer); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs index 25d01e567..9ff312bcf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemComponent.cs @@ -1,4 +1,6 @@ -using System.Xml.Linq; +using System; +using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma.Items.Components { @@ -18,7 +20,7 @@ namespace Barotrauma.Items.Components return true; //element processed } - public virtual void ServerAppendExtraData(ref object[] extraData) { } + public virtual IEventData ServerGetEventData() => null; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index 912920409..063f23d7a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(Text); lastSentText = Text; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs index 53d331b9c..4396628d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(IsActive); lastSentState = IsActive; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index 5de5d4360..dc87c0b4a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class Controller : ItemComponent, IServerSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(State); msg.Write(user == null ? (ushort)0 : user.ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs index 420b4c685..9897bc414 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Deconstructor.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class Deconstructor : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool active = msg.ReadBoolean(); @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(user?.ID ?? 0); msg.Write(IsActive); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index 3be3b0dc5..a1f431279 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -5,14 +5,14 @@ namespace Barotrauma.Items.Components { partial class Engine : Powered, IServerSerializable, IClientSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //force can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(targetForce / 10.0f), -10, 10); msg.Write(User == null ? Entity.NullEntityID : User.ID); } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { float newTargetForce = msg.ReadRangedInteger(-10, 10) * 10.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index baddfb0d8..8ffcf9742 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class Fabricator : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { uint recipeHash = msg.ReadUInt32(); @@ -32,21 +32,33 @@ namespace Barotrauma.Items.Components } private ulong serverEventId = 0; - public override void ServerAppendExtraData(ref object[] extraData) - { - //ensuring the uniqueness of this event is - //required for the fabricator to sync correctly; - //otherwise, the event manager would incorrectly - //assume that the client actually has the latest state - Array.Resize(ref extraData, 4); - extraData[2] = serverEventId; - extraData[3] = State; - } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + private readonly struct EventData : IEventData { - FabricatorState stateAtEvent = (FabricatorState)extraData[3]; - msg.Write((byte)stateAtEvent); + public readonly ulong ServerEventId; + public readonly FabricatorState State; + + public EventData(ulong serverEventId, FabricatorState state) + { + //ensuring the uniqueness of this event is + //required for the fabricator to sync correctly; + //otherwise, the event manager would incorrectly + //assume that the client actually has the latest state + ServerEventId = serverEventId; + State = state; + } + } + + public override IEventData ServerGetEventData() + => new EventData(serverEventId, State); + + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + var componentData = ExtractEventData(extraData); + msg.Write((byte)componentData.State); msg.Write(timeUntilReady); uint recipeHash = fabricatedItem?.RecipeHash ?? 0; msg.Write(recipeHash); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs index 5944a1be1..266398e7e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/OutpostTerminal.cs @@ -4,12 +4,12 @@ namespace Barotrauma.Items.Components { partial class OutpostTerminal : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 74ba0b7ce..f2249351b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { partial class Pump : Powered, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { float newFlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; bool newIsActive = msg.ReadBoolean(); @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs index 61dbbd7e5..91f9f02ad 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Reactor.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components private float? nextServerLogWriteTime; private float lastServerLogWriteTime; - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool autoTemp = msg.ReadBoolean(); bool powerOn = msg.ReadBoolean(); @@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components unsentChanges = true; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(autoTemp); msg.Write(_powerOn); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index c26c7cde2..d7223e2c8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -5,6 +5,16 @@ namespace Barotrauma.Items.Components { partial class Steering : Powered, IServerSerializable, IClientSerializable { + private readonly struct EventData : IEventData + { + public readonly bool DockingButtonClicked; + + public EventData(bool dockingButtonClicked) + { + DockingButtonClicked = dockingButtonClicked; + } + } + // TODO: an enumeration would be much cleaner public bool MaintainPos; public bool LevelStartSelected; @@ -23,7 +33,7 @@ namespace Barotrauma.Items.Components } - public void ServerRead(ClientNetObject type, IReadMessage msg, Barotrauma.Networking.Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool autoPilot = msg.ReadBoolean(); bool dockingButtonClicked = msg.ReadBoolean(); @@ -58,7 +68,7 @@ namespace Barotrauma.Items.Components if (dockingButtonClicked) { item.SendSignal(new Signal("1", sender: c.Character), "toggle_docking"); - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true }); + item.CreateServerEvent(this, new EventData(dockingButtonClicked: true)); } if (!AutoPilot) @@ -88,10 +98,10 @@ namespace Barotrauma.Items.Components unsentChanges = true; } - public void ServerWrite(IWriteMessage msg, Barotrauma.Networking.Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Barotrauma.Networking.Client c, NetEntityEvent.IData extraData = null) { msg.Write(autoPilot); - msg.Write(extraData.Length > 2 && extraData[2] is bool && (bool)extraData[2]); + msg.Write(TryExtractEventData(extraData, out var eventData) && eventData.DockingButtonClicked); if (!autoPilot) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs index 20ae5b275..e0cf10812 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class PowerContainer : Powered, IDrawableComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { float newRechargeSpeed = msg.ReadRangedInteger(0, 10) / 10.0f * maxRechargeSpeed; @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)(rechargeSpeed / MaxRechargeSpeed * 10), 0, 10); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 7d2c4055f..2310120a0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -5,11 +5,26 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent { + private readonly struct EventData : IEventData + { + public readonly bool Launch; + + public EventData(bool launch) + { + Launch = launch; + } + } + private float launchRot; - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - bool launch = extraData.Length > 2 && (bool)extraData[2]; + var eventData = ExtractEventData(extraData); + bool launch = eventData.Launch; + msg.Write(launch); if (launch) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 673f53972..41d2f5c1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -7,13 +7,13 @@ namespace Barotrauma.Items.Components private Character prevLoggedFixer; private FixActions prevLoggedFixAction; - partial void InitProjSpecific(ContentXElement _) + public override void OnMapLoaded() { //let the clients know the initial deterioration delay item.CreateServerEvent(this); } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { if (c.Character == null) { return; } var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); @@ -42,7 +42,7 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(deteriorationTimer); msg.Write(deteriorateAlwaysResetTimer); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs index e56702fc3..81b46fe2a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class Rope : ItemComponent { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(Snapped); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs index 337b721b0..2af1acfea 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { private float LastSentScanTimer { get; set; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(scanTimer); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs index 4cdc0ec1b..0dbd4486b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs @@ -4,16 +4,16 @@ namespace Barotrauma.Items.Components { partial class ButtonTerminal : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { int signalIndex = msg.ReadRangedInteger(0, Signals.Length - 1); if (!item.CanClientAccess(c)) { return; } if (!SendSignal(signalIndex, c.Character)) { return; } GameServer.Log($"{GameServer.CharacterLogName(c.Character)} sent a signal \"{Signals[signalIndex]}\" from {item.Name}", ServerLog.MessageType.ItemInteraction); - item.CreateServerEvent(this, new object[] { signalIndex }); + item.CreateServerEvent(this, new EventData(signalIndex)); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { Write(msg, extraData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index aa3466eb1..a92cc70d9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -7,7 +7,7 @@ namespace Barotrauma.Items.Components { partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { List[] wires = new List[Connections.Count]; @@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components if (!selectedWire.Item.Removed) { selectedWire.CreateNetworkEvent(); } }, 1.0f); } - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, c.Character.ID }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, c.Character)); return; } @@ -210,10 +210,10 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(user == null ? (ushort)0 : user.ID); - ClientWrite(msg, extraData); + ClientEventWrite(msg, extraData); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index ae6f5b6ef..7c2bc102a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components { partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool[] elementStates = new bool[customInterfaceElementList.Count]; string[] elementValues = new string[customInterfaceElementList.Count]; @@ -51,17 +51,12 @@ namespace Barotrauma.Items.Components } //notify all clients of the new state - GameMain.Server.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - clickedButton - }); + item.CreateServerEvent(this, new EventData(clickedButton)); item.CreateServerEvent(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) @@ -76,7 +71,7 @@ namespace Barotrauma.Items.Components } else { - msg.Write(extraData != null && extraData.Any(d => d as CustomInterfaceElement == customInterfaceElementList[i])); + msg.Write(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs index 8a093a7d5..d99732838 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/MemoryComponent.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components yield return CoroutineStatus.Success; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(Value); lastSentValue = Value; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index cbd3638ca..9fdd21863 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -7,7 +7,19 @@ namespace Barotrauma.Items.Components { partial class Terminal : ItemComponent, IClientSerializable, IServerSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + private readonly struct ServerEventData : IEventData + { + public readonly int MsgIndex; + public readonly string MsgToSend; + + public ServerEventData(int msgIndex, string msgToSend) + { + MsgIndex = msgIndex; + MsgToSend = msgToSend; + } + } + + public void ServerEventRead(IReadMessage msg, Client c) { string newOutputValue = msg.ReadString(); @@ -47,7 +59,7 @@ namespace Barotrauma.Items.Components string msgToSend = str; if (string.IsNullOrEmpty(msgToSend)) { - item.CreateServerEvent(this, new object[] { msgIndex, msgToSend }); + item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend)); msgIndex++; continue; } @@ -73,23 +85,23 @@ namespace Barotrauma.Items.Components if (!splitMessage.Any()) { break; } tempMsg += " "; } while (tempMsg.Length + splitMessage[0].Length < MaxMessageLength); - item.CreateServerEvent(this, new object[] { msgIndex, tempMsg }); + item.CreateServerEvent(this, new ServerEventData(msgIndex, tempMsg)); msgToSend = msgToSend.Remove(0, tempMsg.Length); } } if (!string.IsNullOrEmpty(msgToSend)) { - item.CreateServerEvent(this, new object[] { msgIndex, msgToSend }); + item.CreateServerEvent(this, new ServerEventData(msgIndex, msgToSend)); } msgIndex++; } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (extraData.Length > 3 && extraData[3] is string str) + if (TryExtractEventData(extraData, out ServerEventData eventData)) { - msg.Write(str); + msg.Write(eventData.MsgToSend); } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs index 333028165..a42adf967 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/WifiComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class WifiComponent { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger(Channel, MinChannel, MaxChannel); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs index 7586f86c0..51a0fa393 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs @@ -6,6 +6,16 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable { + private readonly struct ServerEventData : IEventData + { + public readonly int EventIndex; + + public ServerEventData(int eventIndex) + { + EventIndex = eventIndex; + } + } + public void CreateNetworkEvent() { if (GameMain.Server == null) return; @@ -13,13 +23,17 @@ namespace Barotrauma.Items.Components int eventCount = Math.Max((int)Math.Ceiling(nodes.Count / (float)MaxNodesPerNetworkEvent), 1); for (int i = 0; i < eventCount; i++) { - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i }); + item.CreateServerEvent(this, new ServerEventData(i)); } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - int eventIndex = (int)extraData[2]; + var eventData = ExtractEventData(extraData); + int eventIndex = eventData.EventIndex; int nodeStartIndex = eventIndex * MaxNodesPerNetworkEvent; int nodeCount = MathHelper.Clamp(nodes.Count - nodeStartIndex, 0, MaxNodesPerNetworkEvent); @@ -32,7 +46,7 @@ namespace Barotrauma.Items.Components } } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { int nodeCount = msg.ReadByte(); Vector2 lastNodePos = Vector2.Zero; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs index 2ebea696e..4526dd037 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/TriggerComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { partial class TriggerComponent : ItemComponent, IServerSerializable { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedSingle(CurrentForceFluctuation, 0.0f, 1.0f, 8); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index ccf34fa50..14901a971 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -8,21 +8,11 @@ namespace Barotrauma { partial class Inventory : IServerSerializable, IClientSerializable { - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { List prevItems = new List(AllItems.Distinct()); - byte slotCount = msg.ReadByte(); - List[] newItemIDs = new List[slotCount]; - for (int i = 0; i < slotCount; i++) - { - newItemIDs[i] = new List(); - int itemCount = msg.ReadRangedInteger(0, MaxStackSize); - for (int j = 0; j < itemCount; j++) - { - newItemIDs[i].Add(msg.ReadUInt16()); - } - } + SharedRead(msg, out var newItemIDs); if (c == null || c.Character == null) { return; } @@ -175,7 +165,7 @@ namespace Barotrauma } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { SharedWrite(msg, extraData); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index e229c8ed9..8f1783707 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -20,103 +20,73 @@ namespace Barotrauma partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(this, new AssignCampaignInteractionEventData()); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - string errorMsg = ""; - if (extraData == null || extraData.Length == 0 || !(extraData[0] is NetEntityEvent.Type)) + Exception error(string reason) { - if (extraData == null) - { - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event data was null."; - } - else if (extraData.Length == 0) - { - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event data was empty."; - } - else - { - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - event type not set."; - } - msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:InvalidData" + Name, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return; + string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}"; + GameAnalyticsManager.AddErrorEventOnce($"Item.ServerWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + return new Exception(errorMsg); } + + if (extraData is null) { throw error("event data was null"); } + if (!(extraData is IEventData itemEventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } - int initialWritePos = msg.LengthBits; - - NetEntityEvent.Type eventType = (NetEntityEvent.Type)extraData[0]; - msg.WriteRangedInteger((int)eventType, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - switch (eventType) + msg.WriteRangedInteger((int)itemEventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + switch (itemEventData) { - case NetEntityEvent.Type.ComponentState: - if (extraData.Length < 2 || !(extraData[1] is int)) + case ComponentStateEventData componentStateEventData: + int componentIndex = components.IndexOf(componentStateEventData.Component); + if (componentIndex < 0) { - errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component index not given."; - break; + throw error($"component index out of range ({componentIndex})"); } - int componentIndex = (int)extraData[1]; - if (componentIndex < 0 || componentIndex >= components.Count) + if (!(components[componentIndex] is IServerSerializable serializableComponent)) { - errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component index out of range (" + componentIndex + ")."; - break; - } - else if (!(components[componentIndex] is IServerSerializable)) - { - errorMsg = "Failed to write a component state event for the item \"" + Name + "\" - component \"" + components[componentIndex] + "\" is not server serializable."; - break; + throw error($"component \"{components[componentIndex]}\" is not server serializable"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); - (components[componentIndex] as IServerSerializable).ServerWrite(msg, c, extraData); + serializableComponent.ServerEventWrite(msg, c, extraData); break; - case NetEntityEvent.Type.InventoryState: - if (extraData.Length < 2 || !(extraData[1] is int)) + case InventoryStateEventData inventoryStateEventData: + int containerIndex = components.IndexOf(inventoryStateEventData.Component); + if (containerIndex < 0) { - errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component index not given."; + throw error($"container index out of range ({containerIndex})"); break; } - int containerIndex = (int)extraData[1]; - if (containerIndex < 0 || containerIndex >= components.Count) + if (!(components[containerIndex] is ItemContainer itemContainer)) { - errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - container index out of range (" + containerIndex + ")."; - break; - } - else if (!(components[containerIndex] is ItemContainer)) - { - errorMsg = "Failed to write an inventory state event for the item \"" + Name + "\" - component \"" + components[containerIndex] + "\" is not server serializable."; - break; + throw error("component \"" + components[containerIndex] + "\" is not server serializable"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); - (components[containerIndex] as ItemContainer).Inventory.ServerWrite(msg, c); + itemContainer.Inventory.ServerEventWrite(msg, c); break; - case NetEntityEvent.Type.Status: + case StatusEventData _: msg.Write(condition); break; - case NetEntityEvent.Type.AssignCampaignInteraction: + case AssignCampaignInteractionEventData _: msg.Write((byte)CampaignInteractionType); break; - case NetEntityEvent.Type.ApplyStatusEffect: + case ApplyStatusEffectEventData applyStatusEffectEventData: { - ActionType actionType = (ActionType)extraData[1]; - ItemComponent targetComponent = extraData.Length > 2 ? (ItemComponent)extraData[2] : null; - ushort characterID = extraData.Length > 3 ? (ushort)extraData[3] : (ushort)0; - Limb targetLimb = extraData.Length > 4 ? (Limb)extraData[4] : null; - ushort useTargetID = extraData.Length > 5 ? (ushort)extraData[5] : (ushort)0; - Vector2? worldPosition = null; - if (extraData.Length > 6) { worldPosition = (Vector2)extraData[6]; } + ActionType actionType = applyStatusEffectEventData.ActionType; + ItemComponent targetComponent = applyStatusEffectEventData.TargetItemComponent; + Limb targetLimb = applyStatusEffectEventData.TargetLimb; + Vector2? worldPosition = applyStatusEffectEventData.WorldPosition; - Character targetCharacter = FindEntityByID(characterID) as Character; + Character targetCharacter = applyStatusEffectEventData.TargetCharacter; byte targetLimbIndex = targetLimb != null && targetCharacter != null ? (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb) : (byte)255; msg.WriteRangedInteger((int)actionType, 0, Enum.GetValues(typeof(ActionType)).Length - 1); msg.Write((byte)(targetComponent == null ? 255 : components.IndexOf(targetComponent))); - msg.Write(characterID); + msg.Write(applyStatusEffectEventData.TargetCharacter?.ID ?? (ushort)0); msg.Write(targetLimbIndex); - msg.Write(useTargetID); + msg.Write(applyStatusEffectEventData.UseTarget?.ID ?? (ushort)0); msg.Write(worldPosition.HasValue); if (worldPosition.HasValue) { @@ -125,74 +95,56 @@ namespace Barotrauma } } break; - case NetEntityEvent.Type.ChangeProperty: + case ChangePropertyEventData changePropertyEventData: try { - WritePropertyChange(msg, extraData, inGameEditableOnly: !GameMain.NetworkMember.IsServer); + WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: !GameMain.NetworkMember.IsServer); } catch (Exception e) { - errorMsg = "Failed to write a ChangeProperty network event for the item \"" + Name + "\" (" + e.Message + ")"; + throw new Exception( + $"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})"); } break; - case NetEntityEvent.Type.Upgrade: - if (extraData.Length > 0 && extraData[1] is Upgrade upgrade) + case UpgradeEventData upgradeEventData: + var upgrade = upgradeEventData.Upgrade; + var upgradeTargets = upgrade.TargetComponents; + msg.Write(upgrade.Identifier); + msg.Write((byte)upgrade.Level); + msg.Write((byte)upgradeTargets.Count); + foreach (var (_, value) in upgrade.TargetComponents) { - var upgradeTargets = upgrade.TargetComponents; - msg.Write(upgrade.Identifier); - msg.Write((byte)upgrade.Level); - msg.Write((byte)upgradeTargets.Count); - foreach (var (_, value) in upgrade.TargetComponents) + msg.Write((byte)value.Length); + foreach (var propertyReference in value) { - msg.Write((byte)value.Length); - foreach (var propertyReference in value) - { - object originalValue = propertyReference.OriginalValue; - msg.Write((float)(originalValue ?? -1)); - } + object originalValue = propertyReference.OriginalValue; + msg.Write((float)(originalValue ?? -1)); } } - else - { - errorMsg = extraData.Length > 0 - ? $"Failed to write a network event for the item \"{Name}\" - \"{extraData[1].GetType()}\" is not a valid upgrade." - : $"Failed to write a network event for the item \"{Name}\". No upgrade specified."; - } break; default: - errorMsg = "Failed to write a network event for the item \"" + Name + "\" - \"" + eventType + "\" is not a valid entity event type for items."; - break; - } - - if (!string.IsNullOrEmpty(errorMsg)) - { - //something went wrong - rewind the write position and write invalid event type to prevent creating an unreadable event - msg.BitPosition = initialWritePos; - msg.LengthBits = initialWritePos; - msg.WriteRangedInteger((int)NetEntityEvent.Type.Invalid, 0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.ServerWrite:" + errorMsg, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + throw error($"Unsupported event type {itemEventData.GetType().Name}"); } } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { - NetEntityEvent.Type eventType = - (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); + EventType eventType = + (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); c.KickAFKTimer = 0.0f; switch (eventType) { - case NetEntityEvent.Type.ComponentState: + case EventType.ComponentState: int componentIndex = msg.ReadRangedInteger(0, components.Count - 1); - (components[componentIndex] as IClientSerializable).ServerRead(type, msg, c); + (components[componentIndex] as IClientSerializable).ServerEventRead(msg, c); break; - case NetEntityEvent.Type.InventoryState: + case EventType.InventoryState: int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); - (components[containerIndex] as ItemContainer).Inventory.ServerRead(type, msg, c); + (components[containerIndex] as ItemContainer).Inventory.ServerEventRead(msg, c); break; - case NetEntityEvent.Type.Treatment: + case EventType.Treatment: if (c.Character == null || !c.Character.CanInteractWith(this)) return; UInt16 characterID = msg.ReadUInt16(); @@ -218,10 +170,10 @@ namespace Barotrauma ApplyTreatment(c.Character, targetCharacter, targetLimb); break; - case NetEntityEvent.Type.ChangeProperty: + case EventType.ChangeProperty: ReadPropertyChange(msg, inGameEditableOnly: GameMain.NetworkMember.IsServer, sender: c); break; - case NetEntityEvent.Type.Combine: + case EventType.Combine: UInt16 combineTargetID = msg.ReadUInt16(); Item combineTarget = FindEntityByID(combineTargetID) as Item; if (combineTarget == null || !c.Character.CanInteractWith(this) || !c.Character.CanInteractWith(combineTarget)) @@ -269,6 +221,7 @@ namespace Barotrauma msg.WriteRangedInteger(Quality, 0, Items.Components.Quality.MaxQuality); byte teamID = 0; + IdCard idCardComponent = null; foreach (WifiComponent wifiComponent in GetComponents()) { teamID = (byte)wifiComponent.TeamID; @@ -279,11 +232,31 @@ namespace Barotrauma foreach (IdCard idCard in GetComponents()) { teamID = (byte)idCard.TeamID; + idCardComponent = idCard; break; } } msg.Write(teamID); + + bool hasIdCard = idCardComponent != null; + msg.Write(hasIdCard); + if (hasIdCard) + { + msg.Write(idCardComponent.OwnerName); + msg.Write(idCardComponent.OwnerTags); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1)); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerHairIndex+1)); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerMoustacheIndex+1)); + msg.Write((byte)Math.Max(0, idCardComponent.OwnerFaceAttachmentIndex+1)); + msg.WriteColorR8G8B8(idCardComponent.OwnerHairColor); + msg.WriteColorR8G8B8(idCardComponent.OwnerFacialHairColor); + msg.WriteColorR8G8B8(idCardComponent.OwnerSkinColor); + msg.Write(idCardComponent.OwnerJobId); + msg.Write((byte)idCardComponent.OwnerSheetIndex.X); + msg.Write((byte)idCardComponent.OwnerSheetIndex.Y); + } + bool tagsChanged = tags.Count != base.Prefab.Tags.Count || !tags.All(t => base.Prefab.Tags.Contains(t)); msg.Write(tagsChanged); if (tagsChanged) @@ -367,39 +340,21 @@ namespace Barotrauma } } - public void ServerWritePosition(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c) { msg.Write(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); - body.ServerWrite(tempBuffer, c, extraData); + body.ServerWrite(tempBuffer); msg.WriteVariableUInt32((uint)tempBuffer.LengthBytes); msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } public void CreateServerEvent(T ic) where T : ItemComponent, IServerSerializable - { - if (GameMain.Server == null) { return; } + => CreateServerEvent(ic, ic.ServerGetEventData()); - if (!ItemList.Contains(this)) - { - string errorMsg = "Attempted to create a network event for an item (" + Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace(); - DebugConsole.AddWarning(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Item.CreateServerEvent:EventForUninitializedItem" + Name + ID, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); - return; - } - - int index = components.IndexOf(ic); - if (index == -1) { return; } - - object[] extraData = new object[] { NetEntityEvent.Type.ComponentState, index }; - ic.ServerAppendExtraData(ref extraData); - - GameMain.Server.CreateEntityEvent(this, extraData); - } - - public void CreateServerEvent(T ic, object[] extraData) where T : ItemComponent, IServerSerializable + public void CreateServerEvent(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IServerSerializable { if (GameMain.Server == null) { return; } @@ -411,11 +366,12 @@ namespace Barotrauma return; } - int index = components.IndexOf(ic); - if (index == -1) { return; } + #warning TODO: this should throw an exception + if (!components.Contains(ic)) { return; } - object[] data = new object[] { NetEntityEvent.Type.ComponentState, index }.Concat(extraData).ToArray(); - GameMain.Server.CreateEntityEvent(this, data); + var eventData = new ComponentStateEventData(ic, extraData); + if (!ic.ValidateEventData(eventData)) { throw new Exception($"Component event creation failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false"); } + GameMain.Server.CreateEntityEvent(this, eventData); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs b/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs new file mode 100644 index 000000000..cdbd0c952 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Levels/Level.cs @@ -0,0 +1,58 @@ +using System; +using Barotrauma.Networking; +using FarseerPhysics; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Level : Entity, IServerSerializable + { + public interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + public readonly struct SingleLevelWallEventData : IEventData + { + public EventType EventType => EventType.SingleDestructibleWall; + public readonly DestructibleLevelWall Wall; + + public SingleLevelWallEventData(DestructibleLevelWall wall) + { + Wall = wall; + } + } + + public readonly struct GlobalLevelWallEventData : IEventData + { + public EventType EventType => EventType.GlobalDestructibleWall; + } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed level event: expected {nameof(Level)}.{nameof(IEventData)}"); } + + msg.Write((byte)eventData.EventType); + switch (eventData) + { + case SingleLevelWallEventData { Wall: var destructibleWall }: + int index = ExtraWalls.IndexOf(destructibleWall); + msg.Write((ushort)(index == -1 ? ushort.MaxValue : index)); + //write health using one byte + msg.Write((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); + break; + case GlobalLevelWallEventData _: + foreach (LevelWall levelWall in ExtraWalls) + { + if (levelWall.Body.BodyType == BodyType.Static) { continue; } + msg.Write(levelWall.Body.Position.X); + msg.Write(levelWall.Body.Position.Y); + msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); + } + break; + default: + throw new Exception($"Malformed level event: did not expect {eventData.GetType().Name}"); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 65beaf5ba..0707bd526 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -31,32 +31,43 @@ namespace Barotrauma.MapCreatures.Behavior } } - - partial void UpdateDamage(float deltaTime) + public void ServerWrite(IWriteMessage msg, IEventData eventData) { - if (damageUpdateTimer <= 0) + msg.Write((byte)eventData.NetworkHeader); + + switch (eventData) { - foreach (BallastFloraBranch branch in Branches) - { - if (Math.Abs(branch.AccumulatedDamage) > 1.0f) - { - SendNetworkMessage(this, NetworkHeader.BranchDamage, branch); - branch.AccumulatedDamage = 0f; - } - } - damageUpdateTimer = 1f; + case SpawnEventData spawnEventData: + ServerWriteSpawn(msg); + break; + case KillEventData killEventData: + //do nothing + break; + case BranchCreateEventData branchCreateEventData: + ServerWriteBranchGrowth(msg, branchCreateEventData.NewBranch, branchCreateEventData.Parent.ID); + break; + case BranchDamageEventData branchDamageEventData: + ServerWriteBranchDamage(msg, branchDamageEventData.Branch); + break; + case InfectEventData infectEventData: + ServerWriteInfect(msg, infectEventData.Item.ID, infectEventData.Infect, infectEventData.Infector); + break; + case BranchRemoveEventData branchRemoveEventData: + ServerWriteBranchRemove(msg, branchRemoveEventData.Branch); + break; } - damageUpdateTimer -= deltaTime; + + msg.Write(PowerConsumptionTimer); } - public void ServerWriteSpawn(IWriteMessage msg) + private void ServerWriteSpawn(IWriteMessage msg) { msg.Write(Prefab.Identifier); msg.Write(Offset.X); msg.Write(Offset.Y); } - public void ServerWriteBranchGrowth(IWriteMessage msg, BallastFloraBranch branch, int parentId = -1) + private void ServerWriteBranchGrowth(IWriteMessage msg, BallastFloraBranch branch, int parentId = -1) { var (x, y) = branch.Position; msg.Write(parentId); @@ -71,30 +82,30 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.ParentBranch == null ? -1 : Branches.IndexOf(branch.ParentBranch)); } - public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) + private void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch) { msg.Write((int)branch.ID); msg.Write(branch.Health); } - public void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, bool infect, BallastFloraBranch infector = null) + private void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, InfectEventData.InfectState infect, BallastFloraBranch infector = null) { msg.Write(itemID); - msg.Write(infect); - if (infect) + msg.Write(infect == InfectEventData.InfectState.Yes); + if (infect == InfectEventData.InfectState.Yes) { msg.Write(infector?.ID ?? -1); } } - public void ServerWriteBranchRemove(IWriteMessage msg, BallastFloraBranch branch) + private void ServerWriteBranchRemove(IWriteMessage msg, BallastFloraBranch branch) { msg.Write(branch.ID); } - public void SendNetworkMessage(params object[] extraData) + public void SendNetworkMessage(IEventData extraData) { - GameMain.Server.CreateEntityEvent(Parent, extraData); + GameMain.Server.CreateEntityEvent(Parent, new Hull.BallastFloraEventData(this, extraData)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 861494dc6..024b68518 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -4,14 +4,20 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { partial class Hull : MapEntity, ISerializableEntity, IServerSerializable, IClientSerializable { - private float lastSentVolume, lastSentOxygen, lastSentFireCount; - private float sendUpdateTimer; + private float lastSentVolume; + private float lastSentOxygen; + private int lastSentFireCount; + + private float statusUpdateTimer; + private float decalUpdateTimer; + private float backgroundSectionUpdateTimer; private bool decalUpdatePending; @@ -33,225 +39,163 @@ namespace Barotrauma return; } - sendUpdateTimer -= 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% - if (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || - lastSentFireCount != FireSources.Count || FireSources.Count > 0 || - pendingSectionUpdates.Count > 0 || - sendUpdateTimer < -NetConfig.SparseHullUpdateInterval || - decalUpdatePending) - { - if (sendUpdateTimer < 0.0f) - { - if (decalUpdatePending) - { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); - } - if (pendingSectionUpdates.Count > 0) - { - foreach (int pendingSectionUpdate in pendingSectionUpdates) - { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { true, pendingSectionUpdate } ); - } - pendingSectionUpdates.Clear(); - } - else - { - GameMain.NetworkMember.CreateEntityEvent(this); - } + bool shouldSendStatusUpdate = + (Math.Abs(lastSentVolume - waterVolume) > Volume * 0.1f + || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f + || lastSentFireCount != FireSources.Count) + && statusUpdateTimer <= 0.0f; - lastSentVolume = waterVolume; - lastSentOxygen = OxygenPercentage; - lastSentFireCount = FireSources.Count; - sendUpdateTimer = NetConfig.HullUpdateInterval; + if (shouldSendStatusUpdate) + { + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + + lastSentVolume = waterVolume; + lastSentOxygen = OxygenPercentage; + lastSentFireCount = FireSources.Count; + + statusUpdateTimer = NetConfig.SparseHullUpdateInterval; + } + if (decalUpdatePending && decalUpdateTimer <= 0.0f) + { + GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData()); + + decalUpdateTimer = NetConfig.HullUpdateInterval; + decalUpdatePending = false; + } + if (pendingSectionUpdates.Count > 0 && backgroundSectionUpdateTimer <= 0.0f) + { + foreach (int pendingSectionUpdate in pendingSectionUpdates) + { + GameMain.NetworkMember.CreateEntityEvent(this, new BackgroundSectionsEventData(pendingSectionUpdate)); } + + backgroundSectionUpdateTimer = NetConfig.HullUpdateInterval; + pendingSectionUpdates.Clear(); } } - public void ServerWrite(IWriteMessage message, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (extraData != null && extraData.Length >= 2 && extraData[0] is BallastFloraBehavior behavior && extraData[1] is BallastFloraBehavior.NetworkHeader header) + if (!(extraData is IEventData eventData)) { throw new Exception($"Malformed hull event: expected {nameof(Hull)}.{nameof(IEventData)}"); } + msg.WriteRangedInteger((int)eventData.EventType, (int)EventType.MinValue, (int)EventType.MaxValue); + + switch (eventData) { - message.Write(true); - message.Write((byte)header); - - switch (header) - { - case BallastFloraBehavior.NetworkHeader.Spawn: - behavior.ServerWriteSpawn(message); - break; - case BallastFloraBehavior.NetworkHeader.Kill: - case BallastFloraBehavior.NetworkHeader.Remove: - break; - case BallastFloraBehavior.NetworkHeader.BranchCreate when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is int parentId: - behavior.ServerWriteBranchGrowth(message, branch, parentId); - break; - case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch: - behavior.ServerWriteBranchDamage(message, branch); - break; - case BallastFloraBehavior.NetworkHeader.BranchRemove when extraData.Length >= 3 && extraData[2] is BallastFloraBranch branch: - behavior.ServerWriteBranchRemove(message, branch); - break; - case BallastFloraBehavior.NetworkHeader.Infect when extraData.Length >= 4 && extraData[2] is UInt16 itemID && extraData[3] is bool infect: - BallastFloraBranch infector = null; - if (extraData.Length >= 5 && extraData[4] is BallastFloraBranch b) { infector = b; } - behavior.ServerWriteInfect(message, itemID, infect, infector); - break; - } - - message.Write(behavior.PowerConsumptionTimer); - return; - } - - message.Write(false); //not a ballast flora update - message.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); - message.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); - - message.Write(FireSources.Count > 0); - if (FireSources.Count > 0) - { - message.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); - for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) - { - var fireSource = FireSources[i]; - Vector2 normalizedPos = new Vector2( - (fireSource.Position.X - rect.X) / rect.Width, - (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); - - message.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); - message.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); - message.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); - } - } - - message.Write(extraData != null); - if (extraData != null) - { - message.Write((bool)extraData[0]); - - // Section update - if ((bool)extraData[0]) - { - int sectorToUpdate = (int)extraData[1]; - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - message.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - message.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); - message.Write(BackgroundSections[i].Color.PackedValue); - } - } - else // Decal update - { - message.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); + case StatusEventData statusEventData: + msg.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); + SharedStatusWrite(msg); + break; + case BackgroundSectionsEventData backgroundSectionsEventData: + SharedBackgroundSectionsWrite(msg, backgroundSectionsEventData); + break; + case DecalEventData decalEventData: + msg.WriteRangedInteger(decals.Count, 0, MaxDecalsPerHull); foreach (Decal decal in decals) { - message.Write(decal.Prefab.UintIdentifier); - message.Write((byte)decal.SpriteIndex); + msg.Write(decal.Prefab.UintIdentifier); + msg.Write((byte)decal.SpriteIndex); float normalizedXPos = MathHelper.Clamp(MathUtils.InverseLerp(0.0f, rect.Width, decal.CenterPosition.X), 0.0f, 1.0f); float normalizedYPos = MathHelper.Clamp(MathUtils.InverseLerp(-rect.Height, 0.0f, decal.CenterPosition.Y), 0.0f, 1.0f); - message.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); - message.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); - message.WriteRangedSingle(decal.Scale, 0f, 2f, 12); + msg.WriteRangedSingle(normalizedXPos, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(normalizedYPos, 0.0f, 1.0f, 8); + msg.WriteRangedSingle(decal.Scale, 0f, 2f, 12); } - } + break; + case BallastFloraEventData ballastFloraEventData: + ballastFloraEventData.Behavior.ServerWrite(msg, ballastFloraEventData.SubEventData); + break; + default: + throw new Exception($"Malformed hull event: did not expect {eventData.GetType().Name}"); } } //used when clients use the water/fire console commands or section / decal updates are received - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { - int messageType = msg.ReadRangedInteger(0, 2); - if (messageType == 0) + EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); + switch (eventType) { - float newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; + case EventType.Status: + SharedStatusRead( + msg, + out float newWaterVolume, + out NetworkFireSource[] newFireSources); - bool hasFireSources = msg.ReadBoolean(); - int fireSourceCount = 0; - List newFireSources = new List(); - if (hasFireSources) - { - fireSourceCount = msg.ReadRangedInteger(0, 16); - for (int i = 0; i < fireSourceCount; i++) + if (!c.HasPermission(ClientPermissions.ConsoleCommands) || + !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) { - newFireSources.Add(new Vector3( - MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f), - msg.ReadRangedSingle(0.0f, 1.0f, 8))); + return; } - } - if (!c.HasPermission(ClientPermissions.ConsoleCommands) || - !c.PermittedConsoleCommands.Any(command => command.names.Contains("fire") || command.names.Contains("editfire"))) - { - return; - } + WaterVolume = newWaterVolume; - WaterVolume = newWaterVolume; - - for (int i = 0; i < fireSourceCount; i++) - { - Vector2 pos = new Vector2( - rect.X + rect.Width * newFireSources[i].X, - rect.Y - rect.Height + (rect.Height * newFireSources[i].Y)); - float size = newFireSources[i].Z * rect.Width; - - var newFire = i < FireSources.Count ? - FireSources[i] : - new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); - newFire.Position = pos; - newFire.Size = new Vector2(size, newFire.Size.Y); - - //ignore if the fire wasn't added to this room (invalid position)? - if (!FireSources.Contains(newFire)) + for (int i = 0; i < newFireSources.Length; i++) { - newFire.Remove(); - continue; - } - } + Vector2 pos = newFireSources[i].Position; + float size = newFireSources[i].Size; - for (int i = FireSources.Count - 1; i >= fireSourceCount; i--) - { - FireSources[i].Remove(); - if (i < FireSources.Count) + var newFire = i < FireSources.Count ? + FireSources[i] : + new FireSource(Submarine == null ? pos : pos + Submarine.Position, null, true); + newFire.Position = pos; + newFire.Size = new Vector2(size, newFire.Size.Y); + + //ignore if the fire wasn't added to this room (invalid position)? + if (!FireSources.Contains(newFire)) + { + newFire.Remove(); + continue; + } + } + + for (int i = FireSources.Count - 1; i >= newFireSources.Length; i--) { - FireSources.RemoveAt(i); + FireSources[i].Remove(); + if (i < FireSources.Count) + { + FireSources.RemoveAt(i); + } } - } - } - else if (messageType == 1) - { - byte decalIndex = msg.ReadByte(); - float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); - if (decalIndex < 0 || decalIndex >= decals.Count) { return; } - if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) - { - decals[decalIndex].BaseAlpha = decalAlpha; - } - decalUpdatePending = true; - } - else - { - int sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1); - int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; - int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); - for (int i = start; i < end; i++) - { - float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8); - Color color = new Color(msg.ReadUInt32()); + break; + case EventType.BackgroundSections: + SharedBackgroundSectionRead( + msg, + bsnu => + { + int i = bsnu.SectionIndex; + Color color = bsnu.Color; + float colorStrength = bsnu.ColorStrength; - //TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches + #warning TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches + if (!(c.Character is { AllowInput: true })) { return; } + if (c.Character.HeldItems.All(it => it.GetComponent() == null)) { return; } + + BackgroundSections[i].SetColorStrength(colorStrength); + BackgroundSections[i].SetColor(color); + }, + out int sectorToUpdate); + //add to pending updates to notify other clients as well + pendingSectionUpdates.Add(sectorToUpdate); + break; + case EventType.Decal: + byte decalIndex = msg.ReadByte(); + float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); + if (decalIndex < 0 || decalIndex >= decals.Count) { return; } if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) { - BackgroundSections[i].SetColorStrength(colorStrength); - BackgroundSections[i].SetColor(color); + decals[decalIndex].BaseAlpha = decalAlpha; } - } - //add to pending updates to notify other clients as well - pendingSectionUpdates.Add(sectorToUpdate); - } + decalUpdatePending = true; + break; + default: + throw new Exception($"Malformed incoming hull event: {eventType} is not a supported event type"); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs index a9cfdc9f7..1f6f58360 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Structure.cs @@ -9,7 +9,7 @@ namespace Barotrauma GameMain.Server.KarmaManager.OnStructureHealthChanged(this, attacker, damageAmount); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write((byte)Sections.Length); for (int i = 0; i < Sections.Length; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs index 4acc60be1..9f634bd1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Submarine.cs @@ -1,17 +1,23 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; namespace Barotrauma { partial class Submarine { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWritePosition(IWriteMessage msg, Client c) { msg.Write(ID); IWriteMessage tempBuffer = new WriteOnlyMessage(); - subBody.Body.ServerWrite(tempBuffer, c, extraData); + subBody.Body.ServerWrite(tempBuffer); msg.Write((byte)tempBuffer.LengthBytes); msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); msg.WritePadBits(); } + + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) + { + throw new Exception($"Error while writing a network event for the submarine \"{Info.Name} ({ID})\". Submarines are not even supposed to send events!"); + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index edd4690ca..9d64d3876 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -1,5 +1,4 @@ -using Microsoft.Xna.Framework; -using System; +using System; using System.Text; namespace Barotrauma.Networking @@ -17,10 +16,10 @@ namespace Barotrauma.Networking Character orderTargetCharacter = null; Entity orderTargetEntity = null; OrderChatMessage orderMsg = null; - OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = Order.OrderTargetType.Entity; int? wallSectionIndex = null; Order order = null; + bool isNewOrder = false; if (type == ChatMessageType.Order) { var orderMessageInfo = OrderChatMessage.ReadOrder(msg); @@ -30,9 +29,10 @@ namespace Barotrauma.Networking if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } + isNewOrder = orderMessageInfo.IsNewOrder; orderTargetCharacter = orderMessageInfo.TargetCharacter; orderTargetEntity = orderMessageInfo.TargetEntity; - orderTargetPosition = orderMessageInfo.TargetPosition; + OrderTarget orderTargetPosition = orderMessageInfo.TargetPosition; orderTargetType = orderMessageInfo.TargetType; wallSectionIndex = orderMessageInfo.WallSectionIndex; var orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier]; @@ -165,7 +165,7 @@ namespace Barotrauma.Networking } else if (orderTargetCharacter != null) { - orderTargetCharacter.SetOrder(order); + orderTargetCharacter.SetOrder(order, isNewOrder); } } GameMain.Server.SendOrderChatMessage(orderMsg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs index 584342950..64e1ced26 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs @@ -1,4 +1,7 @@ -using System.IO.Pipes; +using System; +using System.IO.Pipes; +using System.Text; +using System.Threading; namespace Barotrauma.Networking { @@ -14,6 +17,12 @@ namespace Barotrauma.Networking PrivateStart(); } + public static void NotifyCrash(string msg) + { + errorsToWrite.Enqueue(msg); + Thread.Sleep(1000); + } + public static void ShutDown() { PrivateShutDown(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index fc56ade32..266600615 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -1,52 +1,55 @@ -using Barotrauma.Networking; +using System; +using Barotrauma.Networking; namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - public void CreateNetworkEvent(Entity entity, bool remove) + public void CreateNetworkEvent(SpawnOrRemove spawnOrRemove) { - CreateNetworkEventProjSpecific(entity, remove); + CreateNetworkEventProjSpecific(spawnOrRemove); } - partial void CreateNetworkEventProjSpecific(Entity entity, bool remove) + partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove) { - if (GameMain.Server == null || entity == null) { return; } - - GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) }); - if (entity is Character character && character.Info != null) + if (GameMain.Server == null || spawnOrRemove?.Entity == null) { return; } + + GameMain.Server.CreateEntityEvent(this, spawnOrRemove); + if (spawnOrRemove.Entity is Character { Info: { } } character) { foreach (var statKey in character.Info.SavedStatValues.Keys) { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.UpdatePermanentStats, statKey }); - } - } + GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdatePermanentStatsEventData(statKey)); + } + } } - public void ServerWrite(IWriteMessage message, Client client, object[] extraData = null) + public void ServerEventWrite(IWriteMessage message, Client client, NetEntityEvent.IData extraData = null) { - if (GameMain.Server == null) { return; } + if (GameMain.Server is null) { return; } + if (!(extraData is SpawnOrRemove entities)) { throw new Exception($"Malformed {nameof(EntitySpawner)} event: expected {nameof(SpawnOrRemove)}"); } - SpawnOrRemove entities = (SpawnOrRemove)extraData[0]; - - message.Write(entities.Remove); - if (entities.Remove) + message.Write(entities is RemoveEntity); + if (entities is RemoveEntity) { - message.Write(entities.OriginalID); + message.Write(entities.ID); } else { - if (entities.Entity is Item item) + switch (entities.Entity) { - message.Write((byte)SpawnableType.Item); - DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - item.WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID, entities.OriginalItemContainerIndex, entities.OriginalSlotIndex); - } - else if (entities.Entity is Character character) - { - message.Write((byte)SpawnableType.Character); - DebugConsole.Log("Writing character spawn data: " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - character.WriteSpawnData(message, entities.OriginalID, restrictMessageSize: true); + case Item item: + message.Write((byte)SpawnableType.Item); + DebugConsole.Log( + $"Writing item spawn data {item} (ID: {entities.ID})"); + item.WriteSpawnData(message, entities.ID, entities.InventoryID, entities.ItemContainerIndex, entities.SlotIndex); + break; + case Character character: + message.Write((byte)SpawnableType.Character); + DebugConsole.Log( + $"Writing character spawn data: {character} (ID: {entities.ID})"); + character.WriteSpawnData(message, entities.ID, restrictMessageSize: true); + break; } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 9d32305d1..7a8d742eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Networking "ModSender", Task.WhenAll( ContentPackageManager.EnabledPackages.All - .Where(p => p != ContentPackageManager.VanillaCorePackage && p.HasMultiplayerIncompatibleContent) + .Where(p => p != ContentPackageManager.VanillaCorePackage && p.HasMultiplayerSyncedContent) .Select(CompressMod)), (t) => Ready = true); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index ebd958713..804c11d40 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -814,6 +814,12 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; + case ClientPacketHeader.MONEY: + ReadMoneyMessage(inc, connectedClient); + break; + case ClientPacketHeader.REWARD_DISTRIBUTION: + ReadRewardDistributionMessage(inc, connectedClient); + break; case ClientPacketHeader.MEDICAL: ReadMedicalMessage(inc, connectedClient); break; @@ -977,12 +983,12 @@ namespace Barotrauma.Networking { if (entityEvent.Entity is EntitySpawner) { - var spawnData = entityEvent.Data[0] as EntitySpawner.SpawnOrRemove; + var spawnData = entityEvent.Data as EntitySpawner.SpawnOrRemove; errorLines.Add( entityEvent.ID + ": " + - (spawnData.Remove ? "Remove " : "Create ") + + (spawnData is EntitySpawner.RemoveEntity ? "Remove " : "Create ") + spawnData.Entity.ToString() + - " (" + spawnData.OriginalID + ", " + spawnData.Entity.ID + ")"); + " (" + spawnData.ID + ", " + spawnData.Entity.ID + ")"); } } @@ -996,7 +1002,7 @@ namespace Barotrauma.Networking File.WriteAllLines(filePath, errorLines); } - public override void CreateEntityEvent(INetSerializable entity, object[] extraData = null) + public override void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null) { if (!(entity is IServerSerializable serverSerializable)) { @@ -1203,7 +1209,7 @@ namespace Barotrauma.Networking case ClientNetObject.CHARACTER_INPUT: if (c.Character != null) { - c.Character.ServerRead(objHeader, inc, c); + c.Character.ServerReadInput(inc, c); } else { @@ -1246,6 +1252,22 @@ namespace Barotrauma.Networking } } + private void ReadMoneyMessage(IReadMessage inc, Client sender) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.ServerReadMoney(inc, sender); + } + } + + private void ReadRewardDistributionMessage(IReadMessage inc, Client sender) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.ServerReadRewardDistribution(inc, sender); + } + } + private void ReadMedicalMessage(IReadMessage inc, Client sender) { if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) @@ -1714,7 +1736,8 @@ namespace Barotrauma.Networking while (!c.NeedsMidRoundSync && c.PendingPositionUpdates.Count > 0) { var entity = c.PendingPositionUpdates.Peek(); - if (entity == null || entity.Removed || + if (!(entity is IServerPositionSync entityPositionSync) || + entity.Removed || (entity is Item item && float.IsInfinity(item.PositionUpdateInterval))) { c.PendingPositionUpdates.Dequeue(); @@ -1724,14 +1747,7 @@ namespace Barotrauma.Networking IWriteMessage tempBuffer = new ReadWriteMessage(); tempBuffer.Write(entity is Item); tempBuffer.WritePadBits(); tempBuffer.Write(entity is MapEntity me ? me.Prefab.UintIdentifier : (UInt32)0); - if (entity is Item) - { - ((Item)entity).ServerWritePosition(tempBuffer, c); - } - else - { - ((IServerSerializable)entity).ServerWrite(tempBuffer, c); - } + entityPositionSync.ServerWritePosition(tempBuffer, c); //no more room in this packet if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) @@ -1879,7 +1895,7 @@ namespace Barotrauma.Networking } outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedSub.MD5Hash.ToString()); - outmsg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + outmsg.Write(IsUsingRespawnShuttle()); var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; outmsg.Write(selectedShuttle.Name); outmsg.Write(selectedShuttle.MD5Hash.ToString()); @@ -2061,7 +2077,7 @@ namespace Barotrauma.Networking msg.Write(selectedSub.Name); msg.Write(selectedSub.MD5Hash.StringRepresentation); - msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + msg.Write(IsUsingRespawnShuttle()); msg.Write(selectedShuttle.Name); msg.Write(selectedShuttle.MD5Hash.StringRepresentation); @@ -2218,8 +2234,6 @@ namespace Barotrauma.Networking CrewManager crewManager = campaign?.CrewManager; - entityEventManager.RefreshEntityIDs(); - bool hadBots = true; //assign jobs and spawnpoints separately for each team @@ -2366,6 +2380,7 @@ namespace Barotrauma.Networking } characterData.ApplyHealthData(spawnedCharacter); characterData.ApplyOrderData(spawnedCharacter); + characterData.ApplyWalletData(spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); spawnedCharacter.LoadTalents(); @@ -2419,7 +2434,7 @@ namespace Barotrauma.Networking List spawnList = new List(); foreach (KeyValuePair kvp in serverSettings.ExtraCargo) { - spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value)); + spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value, buyer: null)); } CargoManager.CreateItems(spawnList, sub); @@ -2486,7 +2501,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.AllowLinkingWifiToChat); - msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); + msg.Write(IsUsingRespawnShuttle()); msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); @@ -2498,7 +2513,8 @@ namespace Barotrauma.Networking msg.Write(serverSettings.SelectedLevelDifficulty); msg.Write(gameSession.SubmarineInfo.Name); msg.Write(gameSession.SubmarineInfo.MD5Hash.StringRepresentation); - var selectedShuttle = gameStarted && respawnManager.UsingShuttle ? respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; + var selectedShuttle = gameStarted && respawnManager != null && respawnManager.UsingShuttle ? + respawnManager.RespawnShuttle.Info : GameMain.NetLobbyScreen.SelectedShuttle; msg.Write(selectedShuttle.Name); msg.Write(selectedShuttle.MD5Hash.StringRepresentation); msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); @@ -2527,6 +2543,11 @@ namespace Barotrauma.Networking serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } + private bool IsUsingRespawnShuttle() + { + return serverSettings.UseRespawnShuttle || (gameStarted && respawnManager != null && respawnManager.UsingShuttle); + } + private void SendRoundStartFinalize(Client client) { IWriteMessage msg = new WriteOnlyMessage(); @@ -3286,6 +3307,7 @@ namespace Barotrauma.Networking { SubmarineInfo targetSubmarine = Voting.SubVote.Sub; VoteType voteType = Voting.SubVote.VoteType; + Client starter = Voting.SubVote.VoteStarter; int deliveryFee = 0; switch (voteType) @@ -3293,7 +3315,7 @@ namespace Barotrauma.Networking case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: // Pay for submarine - GameMain.GameSession.PurchaseSubmarine(targetSubmarine); + GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: deliveryFee = Voting.SubVote.DeliveryFee; @@ -3304,7 +3326,7 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee); + SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); } serverSettings.Voting.StopSubmarineVote(true); @@ -3461,7 +3483,7 @@ namespace Barotrauma.Networking { if (client.Character != null) //removing control of the current character { - CreateEntityEvent(client.Character, new object[] { NetEntityEvent.Type.Control, null }); + CreateEntityEvent(client.Character, new Character.ControlEventData(null)); client.Character = null; } } @@ -3485,7 +3507,7 @@ namespace Barotrauma.Networking newCharacter.IsRemotePlayer = true; newCharacter.Enabled = true; client.Character = newCharacter; - CreateEntityEvent(newCharacter, new object[] { NetEntityEvent.Type.Control, client }); + CreateEntityEvent(newCharacter, new Character.ControlEventData(client)); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index b11c6bcf4..e587e882e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, Client recipient) { - serializable.ServerWrite(msg, recipient, Data); + serializable.ServerEventWrite(msg, recipient, Data); } } @@ -111,7 +111,7 @@ namespace Barotrauma.Networking lastWarningTime = -10.0; } - public void CreateEvent(IServerSerializable entity, object[] extraData = null) + public void CreateEvent(IServerSerializable entity, NetEntityEvent.IData extraData = null) { if (!ValidateEntity(entity)) { return; } @@ -291,12 +291,6 @@ namespace Barotrauma.Networking bufferedEvents.Add(bufferedEvent); } - public void RefreshEntityIDs() - { - events.ForEach(e => e.RefreshEntityID()); - uniqueEvents.ForEach(e => e.RefreshEntityID()); - } - /// /// Writes all the events that the client hasn't received yet into the outgoing message /// @@ -310,15 +304,7 @@ namespace Barotrauma.Networking /// public void Write(Client client, IWriteMessage msg, out List sentEvents) { - List eventsToSync = null; - if (client.NeedsMidRoundSync) - { - eventsToSync = GetEventsToSync(client); - } - else - { - eventsToSync = GetEventsToSync(client); - } + List eventsToSync = GetEventsToSync(client); if (eventsToSync.Count == 0) { @@ -460,6 +446,7 @@ namespace Barotrauma.Networking /// public void Read(IReadMessage msg, Client sender = null) { + msg.ReadPadBits(); UInt16 firstEventID = msg.ReadUInt16(); int eventCount = msg.ReadByte(); @@ -470,7 +457,6 @@ namespace Barotrauma.Networking if (entityID == Entity.NullEntityID) { - msg.ReadPadBits(); if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++; continue; } @@ -490,7 +476,7 @@ namespace Barotrauma.Networking } else if (entity == null) { - //entity not found -> consider the even read and skip over it + //entity not found -> consider the event read and skip over it //(can happen, for example, when a client uses a medical item repeatedly //and creates an event for it before receiving the event about it being removed) if (GameSettings.CurrentConfig.VerboseLogging) @@ -519,7 +505,6 @@ namespace Barotrauma.Networking sender.LastSentEntityEventID++; } - msg.ReadPadBits(); } } @@ -536,7 +521,7 @@ namespace Barotrauma.Networking var clientEntity = entity as IClientSerializable; if (clientEntity == null) return; - clientEntity.ServerRead(ClientNetObject.ENTITY_STATE, buffer, sender); + clientEntity.ServerEventRead(buffer, sender); } public void Clear() diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 1e2d3e6d5..bdd1fc790 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Networking case ConnectionInitialization.ContentPackageOrder: outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + var mpContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToList(); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); for (int i = 0; i < mpContentPackages.Count; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 38252b58f..429d8b164 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -447,11 +447,11 @@ namespace Barotrauma.Networking if (divingSuitPrefab != null && oxyPrefab != null) { var divingSuit = new Item(divingSuitPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(divingSuit, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(divingSuit)); respawnItems.Add(divingSuit); var oxyTank = new Item(oxyPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(oxyTank, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(oxyTank)); divingSuit.Combine(oxyTank, user: null); respawnItems.Add(oxyTank); } @@ -459,10 +459,10 @@ namespace Barotrauma.Networking if (scooterPrefab != null && batteryPrefab != null) { var scooter = new Item(scooterPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(scooter, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(scooter)); var battery = new Item(batteryPrefab, pos, respawnSub); - Spawner.CreateNetworkEvent(battery, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(battery)); scooter.Combine(battery, user: null); respawnItems.Add(scooter); @@ -528,7 +528,7 @@ namespace Barotrauma.Networking } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteRangedInteger((int)CurrentState, 0, Enum.GetNames(typeof(State)).Length); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 290833c32..f6d9cf671 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -131,7 +131,7 @@ namespace Barotrauma { string subName = inc.ReadString(); SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); - if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo) || GameMain.GameSession.IsSubmarineOwned(subInfo))) + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign && (campaign.CanPurchaseSub(subInfo, sender) || GameMain.GameSession.IsSubmarineOwned(subInfo))) { StartSubmarineVote(subInfo, voteType, sender); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs index 561192ab6..0b03fe680 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Physics/PhysicsBody.cs @@ -6,7 +6,7 @@ namespace Barotrauma { partial class PhysicsBody { - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerWrite(IWriteMessage msg) { float MaxVel = NetConfig.MaxPhysicsBodyVelocity; float MaxAngularVel = NetConfig.MaxPhysicsBodyAngularVelocity; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 0712c25b6..c2fccb428 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -5,6 +5,7 @@ using System; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.Networking; #if LINUX using System.Runtime.InteropServices; #endif @@ -26,6 +27,20 @@ namespace Barotrauma private static extern void setLinuxEnv(); #endif + public static bool TryStartChildServerRelay(string[] commandLineArgs) + { + for (int i = 0; i < commandLineArgs.Length; i++) + { + switch (commandLineArgs[i].Trim()) + { + case "-pipes": + ChildServerRelay.Start(commandLineArgs[i + 2], commandLineArgs[i + 1]); + return true; + } + } + return false; + } + /// /// The main entry point for the application. /// @@ -36,6 +51,7 @@ namespace Barotrauma AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler); #endif + TryStartChildServerRelay(args); #if LINUX setLinuxEnv(); @@ -62,22 +78,49 @@ namespace Barotrauma static GameMain Game; + private static void NotifyCrash(string reportFilePath, Exception e) + { + string errorMsg = $"{reportFilePath}||\n{e.Message} ({e.GetType().Name}) {e.StackTrace}"; + if (e.InnerException != null) + { + var innerMost = e.GetInnermost(); + errorMsg += $"\nInner exception: {innerMost.Message} ({innerMost.GetType().Name}) {e.StackTrace}"; + } + if (errorMsg.Length > ushort.MaxValue) { errorMsg = errorMsg[..ushort.MaxValue]; } + ChildServerRelay.NotifyCrash(errorMsg); + GameMain.Server?.NotifyCrash(); + } + private static void CrashHandler(object sender, UnhandledExceptionEventArgs args) { + void swallowExceptions(Action action) + { + try + { + action(); + } + catch + { + //discard exceptions and keep going + } + } + + string reportFilePath = ""; try { - Game?.Exit(); - CrashDump("servercrashreport.log", (Exception)args.ExceptionObject); - GameMain.Server?.NotifyCrash(); + reportFilePath = "servercrashreport.log"; + CrashDump(ref reportFilePath, (Exception)args.ExceptionObject); } catch { - //exception handler is broken, we have a serious problem here!! - return; + //fuck + reportFilePath = ""; } + swallowExceptions(() => NotifyCrash(reportFilePath, (Exception)args.ExceptionObject)); + swallowExceptions(() => Game?.Exit()); } - static void CrashDump(string filePath, Exception exception) + static void CrashDump(ref string filePath, Exception exception) { try { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs index 1b2c09f73..bd622322a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Steam/SteamManager.cs @@ -42,7 +42,7 @@ namespace Barotrauma.Steam return false; } - var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent); + var contentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent); // These server state variables may be changed at any time. Note that there is no longer a mechanism // to send the player count. The player count is maintained by Steam and you should use the player diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index ece1c941b..715ef5a3a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -16,7 +16,7 @@ namespace Barotrauma Role = role; Character = character; Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.StatusEventData()); } public delegate void MessageSender(string message); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 299d02f04..ecbbeb3e3 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.0.0 + 0.17.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 14c4333de..48ef25dce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -388,7 +388,7 @@ namespace Barotrauma break; } } - if (targetingTag == null) + if (targetingTag.IsNullOrEmpty()) { if (targetItem.GetComponent() != null) { @@ -2100,15 +2100,11 @@ namespace Barotrauma if (!ActiveAttack.IsRunning) { #if SERVER - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] - { - Networking.NetEntityEvent.Type.SetAttackTarget, + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData( attackingLimb, - (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, - SimPosition.X, - SimPosition.Y - }); + damageTarget, + targetLimb, + SimPosition)); #else Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); #endif @@ -2696,7 +2692,7 @@ namespace Barotrauma float target = targetParams.Threshold; if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0) { - target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin; + target = selectedTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin; } if (Character.HealthPercentage > target) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 79879e835..e3f0db038 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -569,7 +569,8 @@ namespace Barotrauma (Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) || ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || - Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10; + Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || + Character.CurrentHull.IsWetRoom; bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character; bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait(); if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) @@ -1265,15 +1266,15 @@ namespace Barotrauma { if (!IsFriendly(attacker)) { - if (Character.Submarine == null) + if (c.Submarine == null) { // Outside return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat; } - if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + if (!c.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Attacked from an unconnected submarine. - return Character.SelectedConstruction?.GetComponent() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; + return c.SelectedConstruction?.GetComponent() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat; } return c.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentOrder() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders)) @@ -1285,18 +1286,22 @@ namespace Barotrauma { cumulativeDamage = 100; } - if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID) + if (attacker.IsPlayer && c.TeamID == attacker.TeamID) { - // Bots in the player team never act aggressively in single player when attacked by the player - return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; + if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID) + { + // Bots in the player team never act aggressively in single player when attacked by the player + // In multiplayer, they react only to players attacking them or other crew members + return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; + } } - if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) + if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Outside or attacked from an unconnected submarine -> don't react. return AIObjectiveCombat.CombatMode.None; } // If there are any enemies around, just ignore the friendly fire - if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) + if (Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull))) { isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; @@ -1352,18 +1357,19 @@ namespace Barotrauma Character FindInstigator() { - if (Character.IsInstigator) + if (attacker.IsInstigator) { - return Character; + return attacker; } - else if (c.AIController is HumanAIController humanAi) + if (c.IsInstigator) + { + return c; + } + if (c.AIController is HumanAIController humanAi) { return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull)); } - else - { - return null; - } + return null; } } } @@ -1497,7 +1503,7 @@ namespace Barotrauma if (hull == null || hull.WaterPercentage > 90 || hull.LethalPressure > 0 || - hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f)) + hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) { needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index b76ebfaa3..e3e8af4cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -30,6 +30,7 @@ namespace Barotrauma if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; } } if (item.ConditionPercentage <= 0) { return false; } + if (item.IsClaimedByBallastFlora) { return false; } if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } if (IsReady(battery)) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 79732e006..c647b82d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -83,7 +83,8 @@ namespace Barotrauma container.HasTag("allowcleanup") && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && container.GetComponent() != null && - IsItemInsideValidSubmarine(container, character); + IsItemInsideValidSubmarine(container, character) && + !container.IsClaimedByBallastFlora; public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { @@ -100,6 +101,7 @@ namespace Barotrauma if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } } 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 4f89fe0ad..f93ededde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -777,7 +777,7 @@ namespace Barotrauma } if (retreatTarget != null && character.CurrentHull != retreatTarget) { - TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true) + TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager) { UsePathingOutside = false }, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9e9a1be4f..4dae997fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -162,7 +162,6 @@ namespace Barotrauma CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty, TargetName = Leak.FlowTargetHull?.DisplayName, - CheckVisibility = false, requiredCondition = () => Leak.Submarine == character.Submarine, // 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/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 2a1062948..4bf65a912 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,7 +1,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Barotrauma.Extensions; @@ -11,6 +10,8 @@ namespace Barotrauma { public override Identifier Identifier { get; set; } = "go to".ToIdentifier(); + public override bool KeepDivingGearOn => GetTargetHull() == null; + private AIObjectiveFindDivingGear findDivingGear; private readonly bool repeat; //how long until the path to the target is declared unreachable @@ -74,14 +75,6 @@ namespace Barotrauma _closeEnough = Math.Max(minDistance, value); } } - - // TODO: Currently we never check the visibility (to the end node), which is actually unintentional. - // I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better. - // However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true. - - // NOTE: This seemes to have caused an issue now Regalis11/Barotrauma#8067: namely, the bot was trying to use a waypoint that was obstructed by a shuttle - // because obstruction was only checked when checking visibility in PathFinder. Changed that so that obstructed nodes are no longer used. - public bool CheckVisibility { get; set; } public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } @@ -268,15 +261,15 @@ namespace Barotrauma { Character followTarget = Target as Character; bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure); - bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir; + bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)); if (Mimic) { - if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir) + if (HumanAIController.HasDivingSuit(followTarget)) { needsDivingGear = true; needsDivingSuit = true; } - else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir) + else if (HumanAIController.HasDivingMask(followTarget)) { needsDivingGear = true; } @@ -505,7 +498,7 @@ namespace Barotrauma startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), endNodeFilter: endNodeFilter, nodeFilter: nodeFilter, - checkVisiblity: CheckVisibility); + checkVisiblity: Target is Item || Target is Character); } if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs index 67a417900..b13723512 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItems.cs @@ -60,6 +60,7 @@ namespace Barotrauma if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } + if (item.IsClaimedByBallastFlora) { return false; } if (!item.HasAccess(character)) { return false; } // Ignore items that require power but don't have it if (item.GetComponent() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index e77eceb92..9342c4239 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -10,6 +10,16 @@ namespace Barotrauma { class AIObjectiveManager { + public enum ObjectiveType + { + None = 0, + Order = 1, + Objective = 2, + + MinValue = 0, + MaxValue = 2 + } + public const float HighestOrderPriority = 70; public const float LowestOrderPriority = 60; public const float RunPriority = 50; @@ -184,28 +194,20 @@ namespace Barotrauma { var previousObjective = CurrentObjective; var firstObjective = Objectives.FirstOrDefault(); + bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority; - if (currentObjectiveIsOrder) + + CurrentObjective = currentObjectiveIsOrder ? CurrentOrder : firstObjective; + + if (previousObjective == CurrentObjective) { return CurrentObjective; } + + previousObjective?.OnDeselected(); + CurrentObjective?.OnSelected(); + GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); + if (GameMain.NetworkMember is { IsServer: true }) { - CurrentObjective = CurrentOrder; - } - else - { - CurrentObjective = firstObjective; - } - if (previousObjective != CurrentObjective) - { - previousObjective?.OnDeselected(); - CurrentObjective?.OnSelected(); - GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] - { - NetEntityEvent.Type.ObjectiveManagerState, - currentObjectiveIsOrder ? "order" : "objective" - }); - } + GameMain.NetworkMember.CreateEntityEvent(character, + new Character.ObjectiveManagerStateEventData(currentObjectiveIsOrder ? ObjectiveType.Order : ObjectiveType.Objective)); } return CurrentObjective; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 80e9f3371..f1bf0e45f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -67,6 +67,11 @@ namespace Barotrauma Priority = 0; return Priority; } + else if (targetItem.IsClaimedByBallastFlora) + { + Priority = 0; + return Priority; + } var reactor = component?.Item.GetComponent(); if (reactor != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index f4537800a..c908ac461 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -41,6 +41,7 @@ namespace Barotrauma if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; } } if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + if (pump.Item.IsClaimedByBallastFlora) { return false; } if (IsReady(pump)) { return false; } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index adf82ad04..27b75c722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -1,7 +1,6 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; @@ -12,6 +11,7 @@ namespace Barotrauma public override Identifier Identifier { get; set; } = "repair item".ToIdentifier(); public override bool AllowInAnySub => true; + public override bool KeepDivingGearOn => Item?.CurrentHull == null; public Item Item { get; private set; } @@ -52,6 +52,10 @@ namespace Barotrauma Priority = 0; IsCompleted = true; } + else if (Item.IsClaimedByBallastFlora) + { + Priority = 0; + } else { float distanceFactor = 1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index a8b001bb3..005e3aa44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -151,6 +151,7 @@ namespace Barotrauma if (!item.IsInteractable(character)) { return false; } if (item.IsFullCondition) { return false; } if (item.Submarine == null || character.Submarine == null) { return false; } + if (item.IsClaimedByBallastFlora) { return false; } //player crew ignores items in outposts if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; } if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 7b460210f..4be832fd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -30,6 +30,7 @@ namespace Barotrauma private float findHullTimer; private bool ignoreOxygen; private readonly float findHullInterval = 1.0f; + private bool performedCpr; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -220,12 +221,12 @@ namespace Barotrauma DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(), TargetName = targetCharacter.DisplayName }, - onCompleted: () => RemoveSubObjective(ref goToObjective), - onAbandon: () => - { - RemoveSubObjective(ref goToObjective); - Abandon = true; - }); + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => + { + RemoveSubObjective(ref goToObjective); + Abandon = true; + }); } else { @@ -401,6 +402,7 @@ namespace Barotrauma { character.SelectCharacter(targetCharacter); character.AnimController.Anim = AnimController.Animation.CPR; + performedCpr = true; } else { @@ -436,9 +438,10 @@ namespace Barotrauma { bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) - { - character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name).Value, - null, 1.0f, $"targethealed{targetCharacter.Name}".ToIdentifier(), 60.0f); + { + string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed"; + string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value; + character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f); } return isCompleted; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 9c1b018f9..b9c716c78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -392,7 +392,6 @@ namespace Barotrauma return option; } - public ImmutableArray GetTargetItems(Identifier option = default) { if (option.IsEmpty || !OptionTargetItems.TryGetValue(option, out ImmutableArray optionTargetItems)) @@ -418,6 +417,28 @@ namespace Barotrauma } public override void Dispose() { } + + /// + /// Create an Order instance with a null target + /// + public Order CreateInstance(OrderTargetType targetType, Character orderGiver = null, bool isAutonomous = false) + { + try + { + return targetType switch + { + OrderTargetType.Entity => new Order(this, targetEntity: null, targetItem: null, orderGiver, isAutonomous), + OrderTargetType.Position => new Order(this, target: null, orderGiver), + OrderTargetType.WallSection => new Order(this, wall: null, sectionIndex: null, orderGiver), + _ => throw new NotImplementedException() + }; + } + catch (NotImplementedException e) + { + DebugConsole.ShowError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}"); + return null; + } + } } class Order @@ -510,28 +531,45 @@ namespace Barotrauma public readonly bool UseController; /// - /// Constructor for order instances + /// Constructor for orders with the target type OrderTargetType.Entity /// public Order(OrderPrefab prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) : this(prefab, Identifier.Empty, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } - + /// + /// Constructor for orders with the target type OrderTargetType.Entity + /// public Order(OrderPrefab prefab, Identifier option, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) : this(prefab, option, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { } + /// + /// Constructor for orders with the target type OrderTargetType.Position + /// public Order(OrderPrefab prefab, OrderTarget target, Character orderGiver = null) : this(prefab, prefab.Options.FirstOrDefault(), 0, OrderType.Current, null, target, orderGiver) { } + /// + /// Constructor for orders with the target type OrderTargetType.Position + /// public Order(OrderPrefab prefab, Identifier option, OrderTarget target, Character orderGiver = null) : this(prefab, option, 0, OrderType.Current, null, target, orderGiver) { } + /// + /// Constructor for orders with the target type OrderTargetType.WallSection + /// public Order(OrderPrefab prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, Identifier.Empty, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } + /// + /// Constructor for orders with the target type OrderTargetType.WallSection + /// public Order(OrderPrefab prefab, Identifier option, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, option, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { } - public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) + /// + /// Constructor for orders with the target type OrderTargetType.Entity + /// + private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) { Prefab = prefab; Option = option; @@ -561,14 +599,20 @@ namespace Barotrauma TargetType = OrderTargetType.Entity; } - public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null) + /// + /// Constructor for orders with the target type OrderTargetType.Position + /// + private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null) : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: null, targetItem: null, orderGiver) { TargetPosition = target; TargetType = OrderTargetType.Position; } - public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null) + /// + /// Constructor for orders with the target type OrderTargetType.WallSection + /// + private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: wall, null, orderGiver: orderGiver) { WallSectionIndex = sectionIndex; @@ -633,7 +677,7 @@ namespace Barotrauma public Order WithTargetEntity(Entity entity) { - return new Order(this, targetEntity: entity); + return new Order(this, targetEntity: entity, targetType: OrderTargetType.Entity); } public Order WithTargetSpatialEntity(ISpatialEntity spatialEntity) @@ -673,7 +717,7 @@ namespace Barotrauma public Order WithTargetPosition(OrderTarget targetPosition) { - return new Order(this, targetPosition: targetPosition); + return new Order(this, targetPosition: targetPosition, targetType: OrderTargetType.Position); } public Order Clone() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index d171db72a..a112758f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -313,8 +313,7 @@ namespace Barotrauma ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter); #endif var order = new Order(OrderPrefab.Dismissal, null).WithManualPriority(3).WithOrderGiver(character); - //character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false)); - shipIssueWorker.OrderedCharacter.SetOrder(order); + shipIssueWorker.OrderedCharacter.SetOrder(order, isNewOrder: true); shipIssueWorker.RemoveOrder(); break; } @@ -368,7 +367,7 @@ namespace Barotrauma ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this); for (int i = 0; i < crewSizeModifier; i++) { - var order = new Order(OrderPrefab.Prefabs["fixleaks"], null); + var order = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity); ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, order, shipGlobalIssueFixLeaks)); } shipGlobalIssues.Add(shipGlobalIssueFixLeaks); @@ -376,7 +375,7 @@ namespace Barotrauma ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this); for (int i = 0; i < crewSizeModifier; i++) { - var order = new Order(OrderPrefab.Prefabs["repairsystems"], null); + var order = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity); ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, order, shipGlobalIssueRepairSystems)); } shipGlobalIssues.Add(shipGlobalIssueRepairSystems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 9de13ceed..25d23300b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -442,7 +442,7 @@ namespace Barotrauma } #if SERVER - public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null) { msg.Write(IsAlive); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 50af42736..699f94e37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -1017,24 +1017,29 @@ namespace Barotrauma { if (l.IsSevered) { continue; } + float rotation = l.body.Rotation; + if (l.DoesFlip) + { + if (RagdollParams.IsSpritesheetOrientationHorizontal) + { + //horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle + rotation = -(l.body.Rotation + MathHelper.Pi); + } + else + { + //vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards) + rotation = -l.body.Rotation; + } + } + TrySetLimbPosition(l, centerOfMass, new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y), + rotation, lerp); l.body.PositionSmoothingFactor = 0.8f; - - if (!l.DoesFlip) { continue; } - if (RagdollParams.IsSpritesheetOrientationHorizontal) - { - //horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle - l.body.SetTransform(l.SimPosition, -(l.body.Rotation + MathHelper.Pi)); - } - else - { - //vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards) - l.body.SetTransform(l.SimPosition, -l.body.Rotation); - } + } if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index c82919b64..b386fdd83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1846,11 +1846,9 @@ namespace Barotrauma } float angle = flipAngle ? -limb.body.Rotation : limb.body.Rotation; - if (wrapAngle) angle = MathUtils.WrapAnglePi(angle); + if (wrapAngle) { angle = MathUtils.WrapAnglePi(angle); } - TrySetLimbPosition(limb, Collider.SimPosition, position); - - limb.body.SetTransform(limb.body.SimPosition, angle); + TrySetLimbPosition(limb, Collider.SimPosition, position, angle); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 39fd2305e..874e6e000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -803,9 +803,9 @@ namespace Barotrauma } SeverLimbJointProjSpecific(limbJoint, playSound: true); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData()); } return true; } @@ -1693,7 +1693,7 @@ namespace Barotrauma if (limb.IsSevered) { continue; } //check visibility from the new position of the collider to the new position of this limb Vector2 movePos = limb.SimPosition + limbMoveAmount; - TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms); + TrySetLimbPosition(limb, simPosition, movePos, limb.Rotation, lerp, ignorePlatforms); } } } @@ -1708,7 +1708,7 @@ namespace Barotrauma IsHanging = true; } - protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true) + protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; @@ -1730,11 +1730,12 @@ namespace Barotrauma if (lerp) { limb.body.TargetPosition = movePos; - limb.body.MoveToTargetPosition(true); + limb.body.TargetRotation = rotation; + limb.body.MoveToTargetPosition(true); } else { - limb.body.SetTransform(movePos, limb.Rotation); + limb.body.SetTransform(movePos, rotation); limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA; limb.PullJointEnabled = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 080c14292..ff3af7ccf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -487,6 +487,10 @@ namespace Barotrauma // TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition); } + if (effect.HasTargetType(StatusEffect.TargetType.Parent)) + { + effect.Apply(effectType, deltaTime, attacker, attacker); + } if (targetCharacter != null) { if (effect.HasTargetType(StatusEffect.TargetType.Character)) @@ -551,6 +555,10 @@ namespace Barotrauma { effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity); } + if (effect.HasTargetType(StatusEffect.TargetType.Parent)) + { + effect.Apply(effectType, deltaTime, attacker, attacker); + } if (effect.HasTargetType(StatusEffect.TargetType.Character)) { effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 3c0307d51..3f6245bc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -25,7 +25,7 @@ namespace Barotrauma FriendlyNPC = 3 } - partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable + partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync { public readonly static List CharacterList = new List(); @@ -130,6 +130,22 @@ namespace Barotrauma } } + private Wallet wallet = new Wallet(); + + public Wallet Wallet + { + get + { + ThrowIfAccessingWalletsInSingleplayer(); + return wallet; + } + set + { + ThrowIfAccessingWalletsInSingleplayer(); + wallet = value; + } + } + public readonly HashSet Latchers = new HashSet(); public readonly HashSet AttachedProjectiles = new HashSet(); @@ -137,6 +153,17 @@ namespace Barotrauma protected ActiveTeamChange currentTeamChange; const string OriginalTeamIdentifier = "original"; + public static void ThrowIfAccessingWalletsInSingleplayer() + { +#if CLIENT && DEBUG + if (Screen.Selected is TestScreen) { return; } +#endif + if (GameMain.NetworkMember is null || GameMain.IsSingleplayer) + { + throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead."); + } + } + public void SetOriginalTeam(CharacterTeamType newTeam) { TryRemoveTeamChange(OriginalTeamIdentifier); @@ -158,12 +185,11 @@ namespace Barotrauma return; } // clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated) - var order = new Order(OrderPrefab.Dismissal, Identifier.Empty, - manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); - SetOrder(order, speak: false); + var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetOrder(order, isNewOrder: true, speak: false); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange }); + GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData()); #endif } @@ -225,9 +251,8 @@ namespace Barotrauma if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior { - var order = new Order(OrderPrefab.Prefabs["fightintruders"], Identifier.Empty, - manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this); - SetOrder(order, speak: false); + var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority); + SetOrder(order, isNewOrder: true, speak: false); } } } @@ -1023,7 +1048,7 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null && Spawner != null && createNetworkEvent) { - Spawner.CreateNetworkEvent(newCharacter, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newCharacter)); } #endif return newCharacter; @@ -1178,6 +1203,10 @@ namespace Barotrauma info = new CharacterInfo(nonHuskedSpeciesName); } } + else if (Params.HasInfo && info == null) + { + info = new CharacterInfo(speciesName); + } if (IsHumanoid) { @@ -1418,9 +1447,9 @@ namespace Barotrauma { item.AddTag(s); } - if (createNetworkEvent && (GameMain.NetworkMember?.IsServer ?? false)) + if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties[nameof(item.Tags).ToIdentifier()] }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()])); } } } @@ -3199,7 +3228,7 @@ namespace Barotrauma } /// Force an order to be set for the character, bypassing hearing checks - public void SetOrder(Order order, bool speak = true, bool force = false) + public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force = false) { var orderGiver = order?.OrderGiver; //set the character order only if the character is close enough to hear the message @@ -3226,7 +3255,7 @@ namespace Barotrauma if (currentOrder.Identifier != order.Identifier) { continue; } if (currentOrder.TargetEntity != order.TargetEntity) { continue; } if (!currentOrder.AutoDismiss) { continue; } - character.SetOrder(currentOrder.GetDismissal(), speak: speak, force: force); + character.SetOrder(currentOrder.GetDismissal(), isNewOrder, speak: speak, force: force); break; } } @@ -3243,7 +3272,7 @@ namespace Barotrauma } if (orderToReplace is { AutoDismiss: true }) { - SetOrder(orderToReplace.GetDismissal(), speak: speak, force: force); + SetOrder(orderToReplace.GetDismissal(), isNewOrder, speak: speak, force: force); } break; } @@ -3251,10 +3280,10 @@ namespace Barotrauma } // Prevent adding duplicate orders - bool wasDuplicate = RemoveDuplicateOrders(order); + RemoveDuplicateOrders(order); AddCurrentOrder(order); - if (orderGiver != null && order.Identifier != "dismissed" && !wasDuplicate) + if (orderGiver != null && order.Identifier != "dismissed" && isNewOrder) { var abilityOrderedCharacter = new AbilityOrderedCharacter(this); orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); @@ -3976,14 +4005,14 @@ namespace Barotrauma HealthUpdateInterval = 0.0f; //clients aren't allowed to kill characters unless they receive a network message - if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + if (!isNetworkMessage && GameMain.NetworkMember is { IsClient: true }) { return; } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); } isDead = true; @@ -4072,15 +4101,7 @@ namespace Barotrauma } } - if (GameMain.GameSession != null) - { - if (GameMain.GameSession.Campaign != null && TeamID == CharacterTeamType.Team1 && !IsAssistant) - { - GameMain.GameSession.Campaign.CrewHasDied = true; - } - - GameMain.GameSession.KillCharacter(this); - } + GameMain.GameSession?.KillCharacter(this); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); @@ -4245,7 +4266,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(newItem, new object[] { NetEntityEvent.Type.Status }); + GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData()); } #if SERVER newItem.GetComponent()?.SyncHistory(); @@ -4334,7 +4355,7 @@ namespace Barotrauma hull?.Submarine ?? Submarine); extraDuffelBags.Add(newDuffelBag); #if SERVER - Spawner.CreateNetworkEvent(newDuffelBag, false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newDuffelBag)); #endif } @@ -4547,7 +4568,7 @@ namespace Barotrauma info.UnlockedTalents.Add(talentPrefab.Identifier); if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents }); + GameMain.NetworkMember.CreateEntityEvent(this, new UpdateTalentsEventData()); #endif CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); characterTalents.Add(characterTalent); @@ -4626,23 +4647,44 @@ namespace Barotrauma ///
public void GiveMoney(int amount) { - if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } + if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; } if (amount <= 0) { return; } - int prevAmount = campaign.Money; - campaign.Money += amount; - OnMoneyChanged(prevAmount, campaign.Money); + Wallet wallet; +#if SERVER + if (!(campaign is MultiPlayerCampaign mpCampaign)) { throw new InvalidOperationException("Campaign on a server is not a multiplayer campaign"); } + Client targetClient = null; + + foreach (Client client in GameMain.Server.ConnectedClients) + { + if (client.Character == this) + { + targetClient = client; + break; + } + } + + wallet = targetClient is null ? mpCampaign.Bank : mpCampaign.GetWallet(targetClient); +#else + wallet = campaign.Wallet; +#endif + + int prevAmount = wallet.Balance; + wallet.Give(amount); + OnMoneyChanged(prevAmount, wallet.Balance); } +#if CLIENT public void SetMoney(int amount) { - if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; } - if (amount == campaign.Money) { return; } + if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; } + if (amount == campaign.Wallet.Balance) { return; } - int prevAmount = campaign.Money; - campaign.Money = amount; - OnMoneyChanged(prevAmount, campaign.Money); + int prevAmount = campaign.Wallet.Balance; + campaign.Wallet.Balance = amount; + OnMoneyChanged(prevAmount, campaign.Wallet.Balance); } +#endif partial void OnMoneyChanged(int prevAmount, int newAmount); partial void OnTalentGiven(TalentPrefab talentPrefab); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs new file mode 100644 index 000000000..11eab9eba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Character + { + public enum EventType + { + InventoryState = 0, + Control = 1, + Status = 2, + Treatment = 3, + SetAttackTarget = 4, + ExecuteAttack = 5, + AssignCampaignInteraction = 6, + ObjectiveManagerState = 7, + TeamChange = 8, + AddToCrew = 9, + UpdateExperience = 10, + UpdateTalents = 11, + UpdateSkills = 12, + UpdateMoney = 13, + UpdatePermanentStats = 14, + + MinValue = 0, + MaxValue = 14 + } + + private interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + public struct InventoryStateEventData : IEventData + { + public EventType EventType => EventType.InventoryState; + } + + public struct ControlEventData : IEventData + { + public EventType EventType => EventType.Control; + public readonly Client Owner; + + public ControlEventData(Client owner) + { + Owner = owner; + } + } + + public struct StatusEventData : IEventData + { + public EventType EventType => EventType.Status; + } + + public struct TreatmentEventData : IEventData + { + public EventType EventType => EventType.Treatment; + } + + private interface IAttackEventData : IEventData + { + public Limb AttackLimb { get; } + public IDamageable TargetEntity { get; } + public Limb TargetLimb { get; } + public Vector2 TargetSimPos { get; } + } + + public struct SetAttackTargetEventData : IAttackEventData + { + public EventType EventType => EventType.SetAttackTarget; + public Limb AttackLimb { get; } + public IDamageable TargetEntity { get; } + public Limb TargetLimb { get; } + public Vector2 TargetSimPos { get; } + + public SetAttackTargetEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos) + { + AttackLimb = attackLimb; + TargetEntity = targetEntity; + TargetLimb = targetLimb; + TargetSimPos = targetSimPos; + } + } + + public struct ExecuteAttackEventData : IAttackEventData + { + public EventType EventType => EventType.ExecuteAttack; + public Limb AttackLimb { get; } + public IDamageable TargetEntity { get; } + public Limb TargetLimb { get; } + public Vector2 TargetSimPos { get; } + + public ExecuteAttackEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos) + { + AttackLimb = attackLimb; + TargetEntity = targetEntity; + TargetLimb = targetLimb; + TargetSimPos = targetSimPos; + } + } + + public struct AssignCampaignInteractionEventData : IEventData + { + public EventType EventType => EventType.AssignCampaignInteraction; + } + + public struct ObjectiveManagerStateEventData : IEventData + { + public EventType EventType => EventType.ObjectiveManagerState; + public readonly AIObjectiveManager.ObjectiveType ObjectiveType; + + public ObjectiveManagerStateEventData(AIObjectiveManager.ObjectiveType objectiveType) + { + ObjectiveType = objectiveType; + } + } + + private struct TeamChangeEventData : IEventData + { + public EventType EventType => EventType.TeamChange; + } + + public struct AddToCrewEventData : IEventData + { + public EventType EventType => EventType.AddToCrew; + public readonly CharacterTeamType TeamType; + public readonly ImmutableArray InventoryItems; + + public AddToCrewEventData(CharacterTeamType teamType, IEnumerable inventoryItems) + { + TeamType = teamType; + InventoryItems = inventoryItems.ToImmutableArray(); + } + + } + + public struct UpdateExperienceEventData : IEventData + { + public EventType EventType => EventType.UpdateExperience; + } + + public struct UpdateTalentsEventData : IEventData + { + public EventType EventType => EventType.UpdateTalents; + } + + public struct UpdateSkillsEventData : IEventData + { + public EventType EventType => EventType.UpdateSkills; + } + + private struct UpdateMoneyEventData : IEventData + { + public EventType EventType => EventType.UpdateMoney; + } + + public struct UpdatePermanentStatsEventData : IEventData + { + public EventType EventType => EventType.UpdatePermanentStats; + public readonly StatTypes StatType; + + public UpdatePermanentStatsEventData(StatTypes statType) + { + StatType = statType; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e008a0432..4142ba190 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -299,7 +299,11 @@ namespace Barotrauma { if (handleBuff) { - Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); + var head = Character.AnimController.GetLimb(LimbType.Head); + if (head != null) + { + Character.CharacterHealth.ApplyAffliction(head, AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f)); + } } idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); @@ -319,7 +323,11 @@ namespace Barotrauma if (handleBuff) { - Character.CharacterHealth.ReduceAfflictionOnLimb(Character.AnimController.GetLimb(LimbType.Head), "disguised".ToIdentifier(), 100f); + var head = Character.AnimController.GetLimb(LimbType.Head); + if (head != null) + { + Character.CharacterHealth.ReduceAfflictionOnLimb(head, "disguised".ToIdentifier(), 100f); + } } } @@ -572,15 +580,15 @@ namespace Barotrauma private void CheckColors() { - if (IsColorValid(Head.HairColor)) + if (!IsColorValid(Head.HairColor)) { Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced); } - if (IsColorValid(Head.FacialHairColor)) + if (!IsColorValid(Head.FacialHairColor)) { Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced); } - if (IsColorValid(Head.SkinColor)) + if (!IsColorValid(Head.SkinColor)) { Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced); } @@ -736,7 +744,7 @@ namespace Barotrauma private int GetIdentifier(string name) { - int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet)); + int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s))); id ^= Head.HairIndex << 12; id ^= Head.BeardIndex << 18; id ^= Head.MoustacheIndex << 24; @@ -822,12 +830,12 @@ namespace Barotrauma { foreach (var limbElement in Ragdoll.MainElement.Elements()) { - if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } - string spritePath = spriteElement.Attribute("texture").Value; + string spritePath = spriteElement.GetAttributeContentPath("texture")?.Value; if (string.IsNullOrEmpty(spritePath)) { continue; } spritePath = ReplaceVars(spritePath); @@ -1298,7 +1306,7 @@ namespace Barotrauma var orders = LoadOrders(orderData); foreach (var order in orders) { - character.SetOrder(order, speak: false, force: true); + character.SetOrder(order, isNewOrder: true, speak: false, force: true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 9e74e40e9..4069abe8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -41,9 +41,11 @@ namespace Barotrauma if (previousValue > 0.0f && value <= 0.0f) { DeactivateHusk(); + highestStrength = 0; } } } + private float highestStrength; public InfectionState State { @@ -75,6 +77,7 @@ namespace Barotrauma { if (HuskPrefab == null) { return; } base.Update(characterHealth, targetLimb, deltaTime); + highestStrength = Math.Max(_strength, highestStrength); character = characterHealth.Character; if (character == null) { return; } @@ -98,7 +101,7 @@ namespace Barotrauma DeactivateHusk(); if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { - character.SpeechImpediment = 100; + character.SpeechImpediment = 30; } State = InfectionState.Transition; } @@ -108,6 +111,10 @@ namespace Barotrauma { character.SetStun(Rand.Range(2f, 3f)); } + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) + { + character.SpeechImpediment = 100; + } State = InfectionState.Active; ActivateHusk(); } @@ -120,7 +127,57 @@ namespace Barotrauma } } - partial void UpdateMessages(); + private InfectionState? prevDisplayedMessage; + private void UpdateMessages() + { + if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; } + if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } + if (highestStrength > Strength) { return; } + + switch (State) + { + case InfectionState.Dormant: + if (Strength < DormantThreshold * 0.5f) + { + return; + } + if (character == Character.Controlled) + { +#if CLIENT + GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red); +#endif + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier()); + } + break; + case InfectionState.Transition: + if (character == Character.Controlled) + { +#if CLIENT + GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red); +#endif + } + else if (character.IsBot) + { + character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier()); + } + break; + case InfectionState.Active: +#if CLIENT + if (character == Character.Controlled && character.Params.UseHuskAppendage) + { + GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red); + } +#endif + break; + case InfectionState.Final: + default: + break; + } + prevDisplayedMessage = State; + } private void ApplyDamage(float deltaTime, bool applyForce) { @@ -209,7 +266,9 @@ namespace Barotrauma { yield return CoroutineStatus.Success; } - +#if SERVER + var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character); +#endif character.Enabled = false; Entity.Spawner.AddEntityToRemoveQueue(character); UnsubscribeFromDeathEvent(); @@ -246,7 +305,6 @@ namespace Barotrauma if (huskPrefab.ControlHusk) { #if SERVER - var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character); if (client != null) { GameMain.Server.SetClientCharacter(client, husk); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index ad88711ce..320d295d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -657,6 +657,7 @@ namespace Barotrauma if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } + if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName)) @@ -731,48 +732,47 @@ namespace Barotrauma StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; - if (Character.GodMode) { return; } - - afflictionsToRemove.Clear(); - afflictionsToUpdate.Clear(); - foreach (KeyValuePair kvp in afflictions) + if (!Character.GodMode) { - var affliction = kvp.Key; - if (affliction.Strength <= 0.0f) + afflictionsToRemove.Clear(); + afflictionsToUpdate.Clear(); + foreach (KeyValuePair kvp in afflictions) { - SteamAchievementManager.OnAfflictionRemoved(affliction, Character); - if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } - continue; + var affliction = kvp.Key; + if (affliction.Strength <= 0.0f) + { + SteamAchievementManager.OnAfflictionRemoved(affliction, Character); + if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); } + continue; + } + afflictionsToUpdate.Add(kvp); } - afflictionsToUpdate.Add(kvp); - } - foreach (KeyValuePair kvp in afflictionsToUpdate) - { - var affliction = kvp.Key; - Limb targetLimb = null; - if (kvp.Value != null) + foreach (KeyValuePair kvp in afflictionsToUpdate) { - int healthIndex = limbHealths.IndexOf(kvp.Value); - targetLimb = - Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ?? - Character.AnimController.MainLimb; + var affliction = kvp.Key; + Limb targetLimb = null; + if (kvp.Value != null) + { + int healthIndex = limbHealths.IndexOf(kvp.Value); + targetLimb = + Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ?? + Character.AnimController.MainLimb; + } + affliction.Update(this, targetLimb, deltaTime); + affliction.DamagePerSecondTimer += deltaTime; + if (affliction is AfflictionBleeding bleeding) + { + UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); + } + Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } - affliction.Update(this, targetLimb, deltaTime); - affliction.DamagePerSecondTimer += deltaTime; - if (affliction is AfflictionBleeding bleeding) + foreach (var affliction in afflictionsToRemove) { - UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); - } - Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); - } - - foreach (var affliction in afflictionsToRemove) - { - afflictions.Remove(affliction); + afflictions.Remove(affliction); + } } Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); - if (Character.InWater) { Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); @@ -782,13 +782,16 @@ namespace Barotrauma Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed)); } - UpdateLimbAfflictionOverlays(); - UpdateSkinTint(); - CalculateVitality(); - - if (Vitality <= MinVitality) + if (!Character.GodMode) { - Kill(); + UpdateLimbAfflictionOverlays(); + UpdateSkinTint(); + CalculateVitality(); + + if (Vitality <= MinVitality) + { + Kill(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 5f9372f84..f48d7aa79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -190,7 +190,7 @@ namespace Barotrauma GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); } - Entity.Spawner.CreateNetworkEvent(item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif if (itemElement.GetAttributeBool("equip", false)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 3f1993a7d..077e8a3da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -152,7 +152,7 @@ namespace Barotrauma GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); } - Entity.Spawner.CreateNetworkEvent(item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 97e51110f..b08cafe51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -1011,15 +1011,9 @@ namespace Barotrauma ExecuteAttack(damageTarget, targetLimb, out attackResult); } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(character, new object[] - { - NetEntityEvent.Type.ExecuteAttack, - this, - (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0, - attackSimPos.X, - attackSimPos.Y - }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData( + attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb, + targetSimPos: attackSimPos)); #endif } @@ -1055,7 +1049,10 @@ namespace Barotrauma if (!attack.IsRunning) { // Set the main collider where the body lands after the attack - character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); + if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f) + { + character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); + } } return wasHit; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 5733c62f1..68d156778 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -162,7 +162,7 @@ namespace Barotrauma Identifier variantOf = MainElement.VariantOf(); if (!variantOf.IsEmpty) { - VariantFile = doc; + VariantFile = new XDocument(doc); #warning TODO: determine that CreateVariantXML is equipped to do this XElement newRoot = CreateVariantXml(MainElement, CharacterPrefab.FindBySpeciesName(variantOf).ConfigElement); var oldElement = MainElement; @@ -477,12 +477,15 @@ namespace Barotrauma [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ConstantHealthRegeneration { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } [Serialize(false, IsPropertySaveable.Yes), Editable] public bool StunImmunity { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool PoisonImmunity { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable] public bool ApplyAfflictionColors { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index c25fa3367..7db320395 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -956,7 +956,7 @@ namespace Barotrauma Deformations = new Dictionary(); foreach (var deformationElement in element.GetChildElements("spritedeformation")) { - string typeName = deformationElement.GetAttributeString("typename", null) ?? deformationElement.GetAttributeString("type", ""); + string typeName = deformationElement.GetAttributeString("type", null) ?? deformationElement.GetAttributeString("typename", string.Empty); SpriteDeformationParams deformation = null; switch (typeName.ToLowerInvariant()) { @@ -982,7 +982,7 @@ namespace Barotrauma } if (deformation != null) { - deformation.TypeName = typeName; + deformation.Type = typeName; } Deformations.Add(deformation, deformationElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs index 177cd4bf2..acf06d837 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNoCrewDied.cs @@ -1,20 +1,28 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionNoCrewDied : AbilityConditionDataless { + public bool assistantsDontCount; + public AbilityConditionNoCrewDied(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { + assistantsDontCount = conditionElement.GetAttributeBool(nameof(assistantsDontCount), true); } protected override bool MatchesConditionSpecific() { - if (GameMain.GameSession?.Campaign is CampaignMode campaign) + if (GameMain.GameSession == null) { return false; } + + foreach (Character character in GameMain.GameSession.Casualties) { - return !campaign.CrewHasDied; + if (assistantsDontCount && character.Info?.Job?.Prefab.Identifier == "assistant") + { + continue; + } + if (character.CauseOfDeath != null && character.CauseOfDeath.Type != CauseOfDeathType.Disconnected) + { + return false; + } } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 9a6732858..73e07dcf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; @@ -63,7 +64,7 @@ namespace Barotrauma public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - Result fail(string error) + static Result fail(string error) => Result.Failure(error); Identifier elemName = element.NameAsIdentifier(); @@ -73,13 +74,16 @@ namespace Barotrauma { return fail($"Invalid content type \"{elemName}\""); } - if (filePath is null) { return fail($"No content path defined for file of type \"{elemName}\""); } try { + if (!File.Exists(filePath.FullPath)) + { + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); + } var file = type.CreateInstance(contentPackage, filePath); return file is null ? throw new Exception($"Content type is not implemented correctly") diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index 794968346..3523bb443 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index c097e2aa4..dae572835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -54,8 +54,10 @@ namespace Barotrauma public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); - #warning TODO: remove this, unless we truly believe that determining "multiplayer-incompatible content" is something we should do - public readonly bool HasMultiplayerIncompatibleContent; + /// + /// Does the content package include some content that needs to match between all players in multiplayer. + /// + public readonly bool HasMultiplayerSyncedContent; protected ContentPackage(XDocument doc, string path) { @@ -93,7 +95,7 @@ namespace Barotrauma .Select(f => f.Error) .ToImmutableArray(); - HasMultiplayerIncompatibleContent = Files.Any(f => !f.NotSyncedInMultiplayer); + HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); Hash = CalculateHash(); var expectedHash = rootElement.GetAttributeString("expectedhash", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 58d0bd8a4..bf2faed95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -392,6 +392,10 @@ namespace Barotrauma public static void LoadVanillaFileList() { VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList); + foreach (string error in VanillaCorePackage.Errors) + { + DebugConsole.ThrowError(error); + } } public static IEnumerable Init() diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 3d0e13e7a..8b328672a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -101,23 +101,27 @@ namespace Barotrauma } private static bool StringEquality(string? a, string? b) - => (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || - string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), - Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), - StringComparison.OrdinalIgnoreCase); + { + if (a.IsNullOrEmpty() || b.IsNullOrEmpty()) + { + return a.IsNullOrEmpty() == b.IsNullOrEmpty(); + } + return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""), + Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), StringComparison.OrdinalIgnoreCase); + } public static bool operator==(ContentPath a, ContentPath b) - => StringEquality(a.Value, b.Value); + => StringEquality(a?.Value, b?.Value); public static bool operator!=(ContentPath a, ContentPath b) => !(a == b); public static bool operator==(ContentPath a, string? b) - => StringEquality(a.Value, b); + => StringEquality(a?.Value, b); public static bool operator!=(ContentPath a, string? b) => !(a == b); public static bool operator==(string? a, ContentPath b) - => StringEquality(a, b.Value); + => StringEquality(a, b?.Value); public static bool operator!=(string? a, ContentPath b) => !(a == b); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index dfbf631f7..8562d6800 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -67,8 +67,13 @@ namespace Barotrauma public void Execute(string[] args) { - if (OnExecute == null) return; - if (!CheatsEnabled && IsCheat) + if (OnExecute == null) { return; } + + bool allowCheats = false; +#if CLIENT + allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen); +#endif + if (!allowCheats && !CheatsEnabled && IsCheat) { NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red); #if USE_STEAM @@ -603,28 +608,27 @@ namespace Barotrauma commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) => { if (args.Length < 2) { return; } - - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => - a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || - a.Identifier == args[0]); + string affliction = args[0]; + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction); if (afflictionPrefab == null) { - ThrowError("Affliction \"" + args[0] + "\" not found."); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase)); + } + if (afflictionPrefab == null) + { + ThrowError("Affliction \"" + affliction + "\" not found."); return; } - if (!float.TryParse(args[1], out float afflictionStrength)) { ThrowError("\"" + args[1] + "\" is not a valid affliction strength."); return; } - bool relativeStrength = false; if (args.Length > 4) { bool.TryParse(args[4], out relativeStrength); } - Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] }); if (targetCharacter != null) { @@ -671,7 +675,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -699,7 +703,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -720,7 +724,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -825,7 +829,7 @@ namespace Barotrauma { Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty(), new[]{ "max" }, - Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(), }; })); @@ -864,7 +868,7 @@ namespace Barotrauma return new string[][] { talentNames.Select(id => id).ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -916,7 +920,7 @@ namespace Barotrauma return new string[][] { availableArgs.ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -951,7 +955,7 @@ namespace Barotrauma return new[] { new string[] { "100" }, - Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(), }; })); @@ -1066,7 +1070,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -1413,7 +1417,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -1455,7 +1459,7 @@ namespace Barotrauma { return new string[][] { - Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; }, isCheat: true)); @@ -1467,7 +1471,7 @@ namespace Barotrauma return new string[][] { GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() }; })); @@ -1538,14 +1542,14 @@ namespace Barotrauma NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White); }, isCheat: true)); - commands.Add(new Command("money", "money [amount]: Gives the specified amount of money to the crew when a campaign is active.", args => + commands.Add(new Command("money", "money [amount] [character]: Gives the specified amount of money to the crew when a campaign is active.", args => { if (args.Length == 0) { return; } if (GameMain.GameSession?.GameMode is CampaignMode campaign) { if (int.TryParse(args[0], out int money)) { - campaign.Money += money; + campaign.Bank.Give(money); GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); } else @@ -1553,8 +1557,23 @@ namespace Barotrauma ThrowError($"\"{args[0]}\" is not a valid numeric value."); } } + }, isCheat: true, getValidArgs: () => new [] + { + new []{ string.Empty }, + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + })); + + commands.Add(new Command("showmoney", "showmoney: Shows the amount of money in everyones wallet.", args => + { + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) + { + ThrowError("No campaign active!"); + return; + } + + NewMessage($"Bank: {campaign.Bank.Balance}"); }, isCheat: true)); - + commands.Add(new Command("skipeventcooldown", "skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args => { GameMain.GameSession?.EventManager?.SkipEventCooldown(); @@ -1968,7 +1987,7 @@ namespace Barotrauma } } - private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).Select(c => c.Name).Distinct().ToArray(); + private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray(); private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 3feeabf1a..eb6756fe0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -87,7 +87,7 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null) { - Entity.Spawner.CreateNetworkEvent(item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs index 8d3141adf..ac6eef6b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckMoneyAction.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -7,15 +10,36 @@ namespace Barotrauma [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } protected override bool? DetermineSuccess() { + Client matchingClient = null; + bool hasTag = !TargetTag.IsEmpty; +#if SERVER + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + + if (hasTag) + { + foreach (Entity entity in targets) + { + if (entity is Character && GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == entity) is { } matchingCharacter) + { + matchingClient = matchingCharacter; + break; + } + } + } +#endif + if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - return campaign.Money >= Amount; + return !hasTag ? campaign.Bank.CanAfford(Amount) : campaign.GetWallet(matchingClient).CanAfford(Amount); } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 00c439419..c3b9fad93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -182,7 +182,7 @@ namespace Barotrauma speaker.ActiveConversation = null; speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); #endif var humanAI = speaker.AIController as HumanAIController; if (humanAI != null && !speaker.IsDead && !speaker.Removed) @@ -259,7 +259,7 @@ namespace Barotrauma speaker.SetCustomInteract( TryStartConversation, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); #endif } return; @@ -369,7 +369,7 @@ namespace Barotrauma speaker.CampaignInteractionType = CampaignMode.InteractionType.None; speaker.SetCustomInteract(null, null); #if SERVER - GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData()); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs index ae7273383..c871e45ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MoneyAction.cs @@ -1,5 +1,9 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -10,6 +14,9 @@ namespace Barotrauma [Serialize(0, IsPropertySaveable.Yes)] public int Amount { get; set; } + [Serialize("", IsPropertySaveable.Yes)] + public Identifier TargetTag { get; set; } + private bool isFinished; public override bool IsFinished(ref string goTo) @@ -25,13 +32,44 @@ namespace Barotrauma { if (isFinished) { return; } +#if SERVER + bool hasTag = !TargetTag.IsEmpty; + List matchingClients = new List(); + if (hasTag) + { + IEnumerable targets = ParentEvent.GetTargets(TargetTag); + + foreach (Entity entity in targets) + { + if (entity is Character && GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == entity) is { } matchingCharacter) + { + matchingClients.Add(matchingCharacter); + break; + } + } + } +#endif + if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - campaign.Money += Amount; - GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value); #if SERVER - (campaign as MultiPlayerCampaign).LastUpdateID++; + if (!hasTag) + { + campaign.Bank.Give(Amount); + } + else + { + foreach (Client client in matchingClients) + { + campaign.GetWallet(client).Give(Amount); + } + } + + ((MultiPlayerCampaign)campaign).LastUpdateID++; +#else + campaign.Wallet.Give(Amount); #endif + GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value); } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index d4a5f8ed7..90e6eb540 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -61,7 +61,7 @@ namespace Barotrauma npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true); } #if SERVER - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, TeamTag, npc.Inventory.AllItems.Select(it => it.ID).ToArray() }); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems)); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 470d6a129..09a689776 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -145,7 +145,7 @@ namespace Barotrauma npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, TextManager.Get("CampaignInteraction.Talk")); - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData()); #endif } if (!AllowMultipleTargets) { return; } @@ -194,7 +194,7 @@ namespace Barotrauma npc.SetCustomInteract(null, null); npc.RequireConsciousnessForCustomInteract = true; #if SERVER - GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); + GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData()); #endif } else if (npcOrItem.TryGet(out Item item)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index d979fc275..8bbb511bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -460,9 +460,12 @@ namespace Barotrauma selectedEvents[eventSet].Add(newEvent); } + Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; foreach (EventSet childEventSet in eventSet.ChildSets) { - CreateEvents(childEventSet, rand); + if (!IsValidForLevel(childEventSet, level)) { continue; } + if (location != null && !IsValidForLocation(childEventSet, location)) { continue; } + CreateEvents(childEventSet, rand); } } } @@ -474,10 +477,7 @@ namespace Barotrauma Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed)); var allowedEventSets = - eventSets.Where(es => - level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && - level.LevelData.Type == es.LevelType && - (es.BiomeIdentifier.IsEmpty || es.BiomeIdentifier == level.LevelData.Biome.Identifier)); + eventSets.Where(set => IsValidForLevel(set, level)); if (requireCampaignSet.HasValue) { @@ -501,13 +501,9 @@ namespace Barotrauma } Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation; - LocationType locationType = location?.GetLocationType(); - if (location != null) { - allowedEventSets = allowedEventSets.Where(set => - set.LocationTypeIdentifiers == null || - set.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier)); + allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location)); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -526,6 +522,20 @@ namespace Barotrauma return null; } + private bool IsValidForLevel(EventSet eventSet, Level level) + { + return + level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && + level.LevelData.Type == eventSet.LevelType && + (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier); + } + + private bool IsValidForLocation(EventSet eventSet, Location location) + { + return eventSet.LocationTypeIdentifiers == null || + eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier); + } + private bool CanStartEventSet(EventSet eventSet) { ISpatialEntity refEntity = GetRefEntity(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 2d91d363c..96bb78082 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -210,7 +210,7 @@ namespace Barotrauma Additive = element.GetAttributeBool("additive", false); - string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection"); + string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 0bccbf953..40c00e220 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -212,7 +212,7 @@ namespace Barotrauma { return null; } - + int probabilitySum = allowedMissions.Sum(m => m.Commonness); int randomNumber = rand.NextInt32() % probabilitySum; foreach (MissionPrefab missionPrefab in allowedMissions) @@ -377,10 +377,16 @@ namespace Barotrauma crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); int totalReward = (int)(reward * missionMoneyGainMultiplier.Value); - campaign.Money += totalReward; - GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); +#if SERVER + totalReward = DistributeRewardsToCrew(GetSalaryEligibleCrew(), totalReward); +#endif + if (totalReward > 0) + { + campaign.Bank.Give(totalReward); + } + foreach (Character character in crewCharacters) { character.Info.MissionsCompletedSinceDeath++; @@ -409,6 +415,57 @@ namespace Barotrauma } } +#if SERVER + public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) + { + int remainingRewards = totalReward; + HashSet nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet(); + float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution); + if (sum == 0) { return remainingRewards; } + foreach (Character character in nonBotCrew) + { + float rewardWeight = character.Wallet.RewardDistribution / sum; + int reward = (int)Math.Floor(totalReward * rewardWeight); + reward = Math.Max(remainingRewards, reward); + character.Wallet.Give(reward); + remainingRewards -= reward; + if (0 >= remainingRewards) { break; } + } + + return remainingRewards; + } +#endif + + public static IEnumerable GetSalaryEligibleCrew() + { + if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return Array.Empty(); } + + IEnumerable characters = crewManager.GetCharacters(); +#if SERVER + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters); +#elif CLIENT + return characters; +#endif + static bool IsAlive(Character c) { return c.Info != null && !c.IsDead; } + } + + + public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) + { + float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; + if (sum == 0) { return (0, 0); } + + float rewardWeight = rewardDistribution / sum; + int rewardPercentage = (int)(rewardWeight * 100); + + return reward switch + { + Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage), + None _ => (0, rewardPercentage), + _ => throw new ArgumentOutOfRangeException() + }; + } + protected void ChangeLocationType(LocationTypeChange change) { if (change == null) { throw new ArgumentException(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 82a9f705e..d79130b93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -361,7 +361,7 @@ namespace Barotrauma if (!SendUserStatistics) { return; } if (sentEventIdentifiers.Contains(identifier)) { return; } - if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) + if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent)) { message = "[MODDED] " + message; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index d1e7c70d0..7311d6c05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -168,7 +168,7 @@ namespace Barotrauma foreach (Item spawnedItem in spawnedItems) { #if SERVER - Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem)); #endif foreach (ItemComponent ic in spawnedItem.Components) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 234bbefa9..585de85c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; #if SERVER using Barotrauma.Networking; #endif @@ -18,12 +19,34 @@ namespace Barotrauma public int Quantity { get; set; } public bool? IsStoreComponentEnabled { get; set; } - public PurchasedItem(ItemPrefab itemPrefab, int quantity) + public readonly int BuyerCharacterInfoId; + + public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId) { ItemPrefab = itemPrefab; Quantity = quantity; IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyerCharacterInfoId; } + +#if CLIENT + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null) + { + ItemPrefab = itemPrefab; + Quantity = quantity; + IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; + } +#elif SERVER + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) + { + ItemPrefab = itemPrefab; + Quantity = quantity; + IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0; + } +#endif + } class SoldItem @@ -156,7 +179,7 @@ namespace Barotrauma OnPurchasedItemsChanged?.Invoke(); } - public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity) + public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemInCrate != null) @@ -167,15 +190,15 @@ namespace Barotrauma ItemsInBuyCrate.Remove(itemInCrate); } } - else if(changeInQuantity > 0) + else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity); + itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); ItemsInBuyCrate.Add(itemInCrate); } OnItemsInBuyCrateChanged?.Invoke(); } - public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemInCrate != null) @@ -188,13 +211,13 @@ namespace Barotrauma } else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity); + itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); ItemsInSellFromSubCrate.Add(itemInCrate); } OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void PurchaseItems(List itemsToPurchase, bool removeFromCrate) + public void PurchaseItems(List itemsToPurchase, bool removeFromCrate, Client client = null) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction @@ -210,13 +233,13 @@ namespace Barotrauma } else { - purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity); + purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); PurchasedItems.Add(purchasedItem); } // Exchange money var itemValue = item.Quantity * buyValues[item.ItemPrefab]; - campaign.Money -= itemValue; + campaign.GetWallet(client).TryDeduct(itemValue); GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); Location.StoreCurrentBalance += itemValue; @@ -427,7 +450,7 @@ namespace Barotrauma #if SERVER if (GameMain.Server != null) { - Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); + Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item)); } #endif } @@ -438,7 +461,7 @@ namespace Barotrauma itemSpawned(item); #if SERVER - Entity.Spawner?.CreateNetworkEvent(item, false); + Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); #endif (itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo; static void itemSpawned(Item item) @@ -491,7 +514,8 @@ namespace Barotrauma if (item?.ItemPrefab == null) { continue; } itemsElement.Add(new XElement("item", new XAttribute("id", item.ItemPrefab.Identifier), - new XAttribute("qty", item.Quantity))); + new XAttribute("qty", item.Quantity), + new XAttribute("buyer", item.BuyerCharacterInfoId))); } parentElement.Add(itemsElement); } @@ -503,12 +527,15 @@ namespace Barotrauma { foreach (XElement itemElement in element.GetChildElements("item")) { - var id = itemElement.GetAttributeString("id", null); + string id = itemElement.GetAttributeString("id", null); if (string.IsNullOrWhiteSpace(id)) { continue; } var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); if (prefab == null) { continue; } - var qty = itemElement.GetAttributeInt("qty", 0); - purchasedItems.Add(new PurchasedItem(prefab, qty)); + int qty = itemElement.GetAttributeInt("qty", 0); + int buyerId = itemElement.GetAttributeInt("buyer", 0); + + purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId)); + } } SetPurchasedItems(purchasedItems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 6e38a47f3..6b4cc9fd5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -20,6 +20,16 @@ namespace Barotrauma private readonly List characterInfos = new List(); private readonly List characters = new List(); + public IEnumerable GetCharacters() + { + return characters; + } + + public IEnumerable GetCharacterInfos() + { + return characterInfos; + } + private Character welcomeMessageNPC; public List CharacterInfos => characterInfos; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index c31f37f60..581566f00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,8 +1,6 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Xml.Linq; using Microsoft.Xna.Framework; +using System; namespace Barotrauma { @@ -27,6 +25,8 @@ namespace Barotrauma public LocalizedString Description { get; } public LocalizedString ShortDescription { get; } + public int MenuOrder { get; } + /// /// How low the reputation can drop on this faction /// @@ -52,6 +52,7 @@ namespace Barotrauma public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty)) { + MenuOrder = element.GetAttributeInt("menuorder", 0); MinReputation = element.GetAttributeInt("minreputation", -100); MaxReputation = element.GetAttributeInt("maxreputation", 100); InitialReputation = element.GetAttributeInt("initialreputation", 0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 820d8227e..3e33d0669 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -164,7 +164,7 @@ namespace Barotrauma ("[reputationvalue]", ((int)Math.Round(value)).ToString())); if (addColorTags) { - formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖"+ formattedReputation+"‖end‖"; + formattedReputation = $"‖color:{XMLExtensions.ToStringHex(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖"; } return formattedReputation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs new file mode 100644 index 000000000..d32c25575 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -0,0 +1,193 @@ +using System; +using System.Xml.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal readonly struct WalletChangedEvent + { + public readonly Wallet Wallet; + public readonly WalletInfo Info; + public readonly WalletChangedData ChangedData; + + public WalletChangedEvent(Wallet wallet, WalletChangedData changedData, WalletInfo info) + { + Wallet = wallet; + Info = info; + ChangedData = changedData; + } + } + + [NetworkSerialize] + internal struct WalletInfo : INetSerializableStruct + { + public int RewardDistribution; + public int Balance; + } + + internal struct NetWalletUpdate : INetSerializableStruct + { + [NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)] + public NetWalletTransaction[] Transactions; + } + + [NetworkSerialize] + internal struct NetWalletTransfer : INetSerializableStruct + { + public Option Sender; + public Option Receiver; + public int Amount; + } + + internal struct NetWalletSalaryUpdate : INetSerializableStruct + { + [NetworkSerialize] + public ushort Target; + + [NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)] + public int NewRewardDistribution; + } + + [NetworkSerialize] + internal struct WalletChangedData : INetSerializableStruct + { + public Option RewardDistributionChanged; + public Option BalanceChanged; + + public WalletChangedData MergeInto(WalletChangedData other) + { + other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged); + other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged); + return other; + + static Option AddOptionalInt(Option a, Option b) + { + return a switch + { + Some some1 => b switch + { + Some some2 => Option.Some(some1.Value + some2.Value), + None _ => Option.Some(some1.Value), + _ => throw new ArgumentOutOfRangeException(nameof(b)) + }, + None _ => b switch + { + Some some1 => Option.Some(some1.Value), + None _ => Option.None(), + _ => throw new ArgumentOutOfRangeException(nameof(b)) + }, + _ => throw new ArgumentOutOfRangeException(nameof(a)) + }; + } + } + } + + [NetworkSerialize] + internal struct NetWalletTransaction : INetSerializableStruct + { + public Option CharacterID; + public WalletChangedData ChangedData; + public WalletInfo Info; + } + + // ReSharper disable ValueParameterNotUsed + internal sealed class InvalidWallet : Wallet + { + public override int Balance + { + get => 0; + set => new InvalidOperationException("Tried to set the balance on an invalid wallet"); + } + + public override int RewardDistribution + { + get => 0; + set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet"); + } + } + + internal partial class Wallet + { + public static readonly Wallet Invalid = new InvalidWallet(); + + public const string LowerCaseSaveElementName = "wallet"; + + private const string AttributeNameBalance = "balance", + AttrubuteNameRewardDistribution = "rewarddistribution", + SaveElementName = "Wallet"; + + private int balance; + + public virtual int Balance + { + get => balance; + set => balance = ClampBalance(value); + } + + private int rewardDistribution; + + public virtual int RewardDistribution + { + get => rewardDistribution; + set => rewardDistribution = ClampRewardDistribution(value); + } + + public Wallet() { } + + public Wallet(XElement element) + { + balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0)); + rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0)); + } + + public XElement Save() + { + XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution)); + return element; + } + + public bool TryDeduct(int price) + { + if (!CanAfford(price)) { return false; } + + Deduct(price); + return true; + } + + public bool CanAfford(int price) => Balance >= price; + public void Refund(int price) => Give(price); + + public void Give(int amount) + { + Balance += amount; + SettingsChanged(balanceChanged: Option.Some(amount), rewardChanged: Option.None()); + } + + public void Deduct(int price) + { + Balance -= price; + SettingsChanged(balanceChanged: Option.Some(-price), rewardChanged: Option.None()); + } + + public void SetRewardDistrubiton(int value) + { + int oldValue = RewardDistribution; + RewardDistribution = value; + SettingsChanged(balanceChanged: Option.None(), rewardChanged: Option.Some(RewardDistribution - oldValue)); + } + + public WalletInfo CreateWalletInfo() + { + return new WalletInfo + { + Balance = Balance, + RewardDistribution = RewardDistribution + }; + } + + partial void SettingsChanged(Option balanceChanged, Option rewardChanged); + + private static int ClampBalance(int value) => Math.Clamp(value, 0, CampaignMode.MaxMoney); + private static int ClampRewardDistribution(int value) => Math.Clamp(value, 0, 100); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index b384c847f..dbde33069 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -68,7 +68,7 @@ namespace Barotrauma abstract partial class CampaignMode : GameMode { - const int MaxMoney = int.MaxValue / 2; //about 1 billion + public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; //duration of the cinematic + credits at the end of the campaign @@ -102,6 +102,8 @@ namespace Barotrauma private readonly List extraMissions = new List(); + public readonly NamedEvent OnMoneyChanged = new NamedEvent(); + public enum TransitionType { None, @@ -167,12 +169,7 @@ namespace Barotrauma } } - private int money; - public int Money - { - get { return money; } - set { money = MathHelper.Clamp(value, 0, MaxMoney); } - } + public Wallet Bank; public LevelData NextLevel { @@ -183,11 +180,20 @@ namespace Barotrauma protected CampaignMode(GameModePreset preset) : base(preset) { - Money = InitialMoney; + Bank = new Wallet + { + Balance = InitialMoney + }; + CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); } + public virtual Wallet GetWallet(Client client = null) + { + return Bank; + } + /// /// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level, /// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end @@ -200,7 +206,7 @@ namespace Barotrauma { return Level.Loaded.EndLocation; } - return Level.Loaded?.StartLocation ?? Map.CurrentLocation; + return Level.Loaded?.StartLocation ?? Map.CurrentLocation; } public List GetSubsToLeaveBehind(Submarine leavingSub) @@ -255,8 +261,6 @@ namespace Barotrauma PurchasedLostShuttles = false; var connectedSubs = Submarine.MainSub.GetConnectedSubs(); wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); - - ResetTalentData(); } public void InitCampaignData() @@ -702,21 +706,20 @@ namespace Barotrauma string eventId = "FinishCampaign:"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); - GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money); + GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance); GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime); GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels); } protected virtual void EndCampaignProjSpecific() { } - public bool TryHireCharacter(Location location, CharacterInfo characterInfo) + public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null) { if (characterInfo == null) { return false; } - if (Money < characterInfo.Salary) { return false; } + if (!GetWallet(client).TryDeduct(characterInfo.Salary)) { return false; } characterInfo.IsNewHire = true; location.RemoveHireableCharacter(characterInfo); CrewManager.AddCharacterInfo(characterInfo); - Money -= characterInfo.Salary; GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown"); return true; } @@ -740,8 +743,7 @@ namespace Barotrauma HumanAIController humanAI = npc.AIController as HumanAIController; if (humanAI == null) { yield return CoroutineStatus.Success; } - var waitOrderPrefab = OrderPrefab.Prefabs["wait"]; - var waitOrder = new Order(waitOrderPrefab, Identifier.Empty, null, orderGiver: null); + var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity); humanAI.SetForcedOrder(waitOrder); var waitObjective = humanAI.ObjectiveManager.ForcedOrder; humanAI.FaceTarget(interactor); @@ -856,7 +858,7 @@ namespace Barotrauma { GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create( - TextManager.Get("RadioAnnouncerName").Value, + TextManager.Get("RadioAnnouncerName").Value, TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c); } #endif @@ -906,7 +908,7 @@ namespace Barotrauma public void LogState() { DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White); - DebugConsole.NewMessage(" Money: " + Money, Color.White); + DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White); DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White); DebugConsole.NewMessage(" Available destinations: ", Color.White); @@ -960,13 +962,5 @@ namespace Barotrauma } } - // Talent relevant data, only stored for the duration of the mission - private void ResetTalentData() - { - CrewHasDied = false; - } - - public bool CrewHasDied { get; set; } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs index 0df3b6f0c..cc4948562 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CharacterCampaignData.cs @@ -26,33 +26,6 @@ namespace Barotrauma private XElement itemData; private XElement healthData; public XElement OrderData { get; private set; } - - public void Refresh(Character character) - { - healthData = new XElement("health"); - character.CharacterHealth.Save(healthData); - if (character.Inventory != null) - { - itemData = new XElement("inventory"); - Character.SaveInventory(character.Inventory, itemData); - } - OrderData = new XElement("orders"); - CharacterInfo.SaveOrderData(character.Info, OrderData); - } - - public XElement Save() - { - XElement element = new XElement("CharacterCampaignData", - new XAttribute("name", Name), - new XAttribute("endpoint", ClientEndPoint), - new XAttribute("steamid", SteamID)); - - CharacterInfo?.Save(element); - if (itemData != null) { element.Add(itemData); } - if (healthData != null) { element.Add(healthData); } - if (OrderData != null) { element.Add(OrderData); } - - return element; - } + public XElement WalletData; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index ab5ea60ed..81c8cfbf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -99,7 +99,6 @@ namespace Barotrauma /// private void Load(XElement element) { - Money = element.GetAttributeInt("money", 0); PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); @@ -166,6 +165,9 @@ namespace Barotrauma case "stats": LoadStats(subElement); break; + case Wallet.LowerCaseSaveElementName: + Bank = new Wallet(subElement); + break; #if SERVER case "savedexperiencepoints": foreach (XElement savedExp in subElement.Elements()) @@ -177,6 +179,15 @@ namespace Barotrauma } } + int oldMoney = element.GetAttributeInt("money", 0); + if (oldMoney > 0) + { + Bank = new Wallet + { + Balance = oldMoney + }; + } + CampaignMetadata ??= new CampaignMetadata(this); UpgradeManager ??= new UpgradeManager(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index d9188c9b1..0da770a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -30,10 +31,14 @@ namespace Barotrauma private readonly List missions = new List(); public IEnumerable Missions { get { return missions; } } + private readonly HashSet casualties = new HashSet(); + public IEnumerable Casualties { get { return casualties; } } + + public CharacterTeamType? WinningTeam; public bool IsRunning { get; private set; } - + public bool RoundEnding { get; private set; } public Level? Level { get; private set; } @@ -201,7 +206,8 @@ namespace Barotrauma var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); if (selectedSub != null) { - campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); + campaign.Bank.TryDeduct(selectedSub.Price); + campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney); } return campaign; } @@ -211,7 +217,8 @@ namespace Barotrauma var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings); if (selectedSub != null) { - campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); + campaign.Bank.TryDeduct(selectedSub.Price); + campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney); } return campaign; } @@ -264,7 +271,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost) + public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -283,19 +290,21 @@ namespace Barotrauma } } - Campaign!.Money -= cost; + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0) + { + Campaign!.GetWallet(client).TryDeduct(cost); + } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); return newSubmarine; } - public void PurchaseSubmarine(SubmarineInfo newSubmarine) + public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null) { if (Campaign is null) { return; } - if (Campaign.Money < newSubmarine.Price) { return; } + if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.GetWallet(client).TryDeduct(newSubmarine.Price)) { return; } if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { - Campaign.Money -= newSubmarine.Price; GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name); OwnedSubmarines.Add(newSubmarine); } @@ -346,7 +355,7 @@ namespace Barotrauma public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null) { AfflictionPrefab.LoadAllEffects(); - + MirrorLevel = mirrorLevel; if (SubmarineInfo == null) { @@ -411,9 +420,6 @@ namespace Barotrauma } } - //Clear out the stored grids - Powered.Grids.Clear(); - Level? level = null; if (levelData != null) { @@ -422,6 +428,11 @@ namespace Barotrauma InitializeLevel(level); + //Clear out the cached grids and force update + Powered.Grids.Clear(); + + casualties.Clear(); + GameAnalyticsManager.AddProgressionEvent( GameAnalyticsManager.ProgressionStatus.Start, GameMode?.Preset?.Identifier.Value ?? "none"); @@ -480,7 +491,7 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } - RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation); + RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation); if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) { @@ -723,7 +734,7 @@ namespace Barotrauma } partial void UpdateProjSpecific(float deltaTime); - + public static IEnumerable GetSessionCrewCharacters() { #if SERVER @@ -745,7 +756,7 @@ namespace Barotrauma { IEnumerable crewCharacters = GetSessionCrewCharacters(); - int prevMoney = (GameMode as CampaignMode)?.Money ?? 0; + int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution foreach (Mission mission in missions) { @@ -759,14 +770,13 @@ namespace Barotrauma if (missions.Any()) { - if (missions.Any()) + if (missions.Any(m => m.Completed)) { foreach (Character character in crewCharacters) { character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted); } } - if (missions.All(m => m.Completed)) { foreach (Character character in crewCharacters) @@ -818,7 +828,7 @@ namespace Barotrauma LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { - GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney); + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton campaignMode.TotalPlayTime += roundDuration; } #if CLIENT @@ -910,6 +920,10 @@ namespace Barotrauma public void KillCharacter(Character character) { + if (CrewManager != null && CrewManager.GetCharacters().Contains(character)) + { + casualties.Add(character); + } #if CLIENT CrewManager?.KillCharacter(character); #endif @@ -917,6 +931,7 @@ namespace Barotrauma public void ReviveCharacter(Character character) { + casualties.Remove(character); #if CLIENT CrewManager?.ReviveCharacter(character); #endif @@ -939,7 +954,7 @@ namespace Barotrauma List excessPackages = new List(); foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) { - //if (!cp.HasMultiplayerIncompatibleContent) { continue; } + if (!cp.HasMultiplayerSyncedContent) { continue; } if (!contentPackagePaths.Any(p => p == cp.Path)) { excessPackages.Add(cp.Name); @@ -949,7 +964,7 @@ namespace Barotrauma bool orderMismatch = false; if (missingPackages.Count == 0 && missingPackages.Count == 0) { - var enabledPackages = ContentPackageManager.EnabledPackages.All/*.Where(cp => cp.HasMultiplayerIncompatibleContent)*/.ToImmutableArray(); + var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray(); for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++) { if (contentPackagePaths[i] != enabledPackages[i].Path) @@ -1015,7 +1030,7 @@ namespace Barotrauma } if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); } rootElement.Add(new XAttribute("selectedcontentpackages", - string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); + string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Path)))); ((CampaignMode)GameMode).Save(doc.Root); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs index 235b157d2..3204ee2a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Extensions; +using Barotrauma.Networking; namespace Barotrauma { @@ -177,17 +178,20 @@ namespace Barotrauma } } + public readonly List PendingHeals = new List(); + + public Action? OnUpdate; + private readonly CampaignMode? campaign; public MedicalClinic(CampaignMode campaign) { this.campaign = campaign; +#if CLIENT + campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(MedicalClinic).ToIdentifier(), OnMoneyChanged); +#endif } - public readonly List PendingHeals = new List(); - - public Action? OnUpdate; - private static bool IsOutpostInCombat() { if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } @@ -203,14 +207,13 @@ namespace Barotrauma return false; } - private HealRequestResult HealAllPending(bool force = false) + private HealRequestResult HealAllPending(bool force = false, Client? client = null) { int totalCost = GetTotalCost(); if (!force) { - if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; } - if (IsOutpostInCombat()) { return HealRequestResult.Refused; } + if (!GetWallet(client).TryDeduct(totalCost)) { return HealRequestResult.InsufficientFunds; } } ImmutableArray crew = GetCrewCharacters(); @@ -225,11 +228,6 @@ namespace Barotrauma } } - if (campaign != null) - { - campaign.Money -= totalCost; - } - ClearPendingHeals(); return HealRequestResult.Success; @@ -316,7 +314,10 @@ namespace Barotrauma private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; - public int GetMoney() => campaign?.Money ?? 0; + public Wallet GetWallet(Client? c = null) + { + return campaign?.GetWallet(c) ?? Wallet.Invalid; + } public static ImmutableArray GetCrewCharacters() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index aede268f8..6a8df4f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; using Barotrauma.Extensions; using Microsoft.Xna.Framework; @@ -178,7 +179,7 @@ namespace Barotrauma /// Purchased upgrades are temporarily stored in and they are applied /// after the next round starts similarly how items are spawned in the stowage room after the round starts. /// - public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false) + public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null) { if (!CanUpgradeSub()) { @@ -215,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.Money >= price) + if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -227,7 +228,6 @@ namespace Barotrauma } } - Campaign.Money -= price; GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value); PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category); @@ -253,14 +253,14 @@ namespace Barotrauma else { DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" + - $"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}"); + $"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}"); } } /// /// Purchases an item swap and handles logic for deducting the credit. /// - public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false) + public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null) { if (!CanUpgradeSub()) { @@ -313,7 +313,7 @@ namespace Barotrauma price = 0; } - if (Campaign.Money >= price) + if (Campaign.GetWallet(client).TryDeduct(price)) { PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove)); if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) @@ -326,7 +326,6 @@ namespace Barotrauma } } - Campaign.Money -= price; GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value); foreach (Item itemToSwap in linkedItems) @@ -355,7 +354,7 @@ namespace Barotrauma else { DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" + - $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}"); + $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index f4818dc48..3cedefab8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -58,7 +58,7 @@ namespace Barotrauma IsEquipped = new bool[capacity]; SlotTypes = new InvSlotType[capacity]; - AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true); + AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", false); AccessibleByOwner = element.GetAttributeBool("accessiblebyowner", AccessibleWhenAlive); string[] slotTypeNames = ParseSlotTypes(element); @@ -159,14 +159,14 @@ namespace Barotrauma { return base.CanBePutInSlot(item, i, ignoreCondition) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && - (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); + (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null) { return base.CanBePutInSlot(itemPrefab, i, condition, quality) && - (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); + (SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1); } public bool CanBeAutoMovedToCorrectSlots(Item item) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index f4826ee0b..bcee8a722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components list.Remove(this); } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { //no further data needed, the event just triggers the discharge } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index 9f135fdb9..c77aa4a7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -229,7 +229,6 @@ namespace Barotrauma.Items.Components int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced); Vector2 offset = SpawnAreaOffset; - offset.Y = -offset.Y; switch (SpawnAreaShape) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 5cde984fb..bff4ee42c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -707,7 +707,7 @@ namespace Barotrauma.Items.Components #if SERVER for (int i = 0; i < Vines.Count; i += VineChunkSize) { - GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i }); + item.CreateServerEvent(this, new EventData(offset: i)); } #elif CLIENT ResetPlanterSize(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index b8caed282..6f3383913 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -12,6 +12,16 @@ namespace Barotrauma.Items.Components { partial class Holdable : Pickable, IServerSerializable, IClientSerializable { + private readonly struct EventData : IEventData + { + public readonly Vector2 AttachPos; + + public EventData(Vector2 attachPos) + { + AttachPos = attachPos; + } + } + const float MaxAttachDistance = 150.0f; //the position(s) in the item that the Character grabs @@ -155,8 +165,8 @@ namespace Barotrauma.Items.Components [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; } - [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.85f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is attached to a wall.")] - public float SpriteDepthWhenAttached + [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 { get; set; @@ -244,12 +254,12 @@ namespace Barotrauma.Items.Components } } - private bool loadedFromXml; + private bool loadedFromInstance; public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) { base.Load(componentElement, usePrefabValues, idRemap); - loadedFromXml = true; + loadedFromInstance = true; if (usePrefabValues) { @@ -583,7 +593,7 @@ namespace Barotrauma.Items.Components Attached = true; #if CLIENT - item.DrawDepthOffset = SpriteDepthWhenAttached - item.SpriteDepth; + item.DrawDepthOffset = 0.0f; #endif } @@ -600,6 +610,9 @@ namespace Barotrauma.Items.Components requiredItems.Clear(); DisplayMsg = ""; PickKey = InputType.Select; +#if CLIENT + item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth; +#endif } public override void ParseMsg() @@ -663,12 +676,7 @@ namespace Barotrauma.Items.Components { #if CLIENT Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character)); - GameMain.Client.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - attachPos - }); + item.CreateClientEvent(this, new EventData(attachPos)); #endif } return false; @@ -867,8 +875,8 @@ namespace Barotrauma.Items.Components { if (!attachable) { return; } - //a mod has overridden the item, and the base item didn't have a Holdable component = a mod made the item movable/detachable - if (item.Prefab.IsOverride && !loadedFromXml) + //the Holdable component didn't get loaded from an instance of the item, just the prefab xml = a mod or update must've made the item movable/detachable + if (!loadedFromInstance) { if (attachedByDefault) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index 68683091a..e0c965cf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -67,7 +67,6 @@ namespace Barotrauma.Items.Components [Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Color OwnerSkinColor { get; set; } - #warning TODO: figure out how to set Vector2.Zero as the default here [Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public Vector2 OwnerSheetIndex { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 95152d686..353a0b0f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -436,13 +436,10 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits { - GameMain.Server.CreateEntityEvent(item, new object[] - { - Networking.NetEntityEvent.Type.ApplyStatusEffect, + GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData( success ? ActionType.OnUse : ActionType.OnFailure, - null, //itemcomponent - targetCharacter.ID, targetLimb - }); + targetItemComponent: null, + targetCharacter, targetLimb)); string logStr = picker?.LogName + " used " + item.Name; if (item.ContainedItems != null && item.ContainedItems.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 432ae93bf..ddd615402 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -282,12 +282,12 @@ namespace Barotrauma.Items.Components } } - public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - msg.Write(activePicker == null ? (ushort)0 : activePicker.ID); + msg.Write(activePicker?.ID ?? (ushort)0); } - public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public virtual void ClientEventRead(IReadMessage msg, float sendingTime) { ushort pickerID = msg.ReadUInt16(); if (pickerID == 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 3e9121f91..40d209d5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -178,11 +178,11 @@ namespace Barotrauma.Items.Components throwDone = true; IsActive = true; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, CurrentThrower.ID }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower)); } - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + if (!(GameMain.NetworkMember is { IsClient: true })) { //Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse" ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index afe64e268..8726d8462 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.IO; +using Barotrauma.Networking; #if CLIENT using Microsoft.Xna.Framework.Graphics; using Barotrauma.Sounds; @@ -1036,6 +1037,30 @@ namespace Barotrauma.Items.Components } } + public interface IEventData { } + + public virtual bool ValidateEventData(NetEntityEvent.IData data) + => true; + + protected T ExtractEventData(NetEntityEvent.IData data) where T : IEventData + => TryExtractEventData(data, out T componentData) + ? componentData + : throw new Exception($"Malformed item component state event for {item.Name} " + + $"(item ID {item.ID}, component type {GetType().Name}): " + + $"could not extract ComponentData of type {typeof(T).Name}"); + + protected bool TryExtractEventData(NetEntityEvent.IData data, out T componentData) + { + componentData = default; + if (data is Item.ComponentStateEventData { ComponentData: T nestedData }) + { + componentData = nestedData; + return true; + } + + return false; + } + #region AI related protected const float AIUpdateInterval = 0.2f; protected float aiUpdateTimer; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index f4897de76..7a2cb168d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -370,7 +370,10 @@ namespace Barotrauma.Items.Components public Item GetFocusTarget() { - item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out"); + var positionOut = item.Connections.Find(c => c.Name == "position_out"); + if (positionOut == null) { return null; } + + item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut); for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) { @@ -380,7 +383,16 @@ namespace Barotrauma.Items.Components return item.LastSentSignalRecipients[i].Item; } } - + + foreach (var recipientPanel in item.GetConnectedComponentsRecursive(positionOut, allowTraversingBackwards: false)) + { + if (recipientPanel.Item.Condition <= 0.0f) { continue; } + if (recipientPanel.Item.Prefab.FocusOnSelected) + { + return recipientPanel.Item; + } + } + return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 004d78dd2..c09526555 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components } float condition = deconstructProduct.CopyCondition ? - percentageHealth * itemPrefab.Health : + percentageHealth * itemPrefab.Health * deconstructProduct.OutConditionMax : itemPrefab.Health * Rand.Range(deconstructProduct.OutConditionMin, deconstructProduct.OutConditionMax); if (DeconstructItemsSimultaneously && deconstructProduct.RequiredOtherItem.Length > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 4fc2c9cf0..d0ad2a519 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -354,7 +354,14 @@ namespace Barotrauma.Items.Components { foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod) { - containedItem.Drop(dropper: null); + if (availableItem.GetComponent()?.RemoveContainedItemsOnDeconstruct ?? false) + { + Entity.Spawner.AddItemToRemoveQueue(containedItem); + } + else + { + containedItem.Drop(dropper: null); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 37dbc543f..b375eb205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER - hull.BallastFlora.SendNetworkMessage(hull.BallastFlora, BallastFloraBehavior.NetworkHeader.Spawn); + hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 7abf1b323..4d9561116 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -1,11 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; using System.Globalization; +using System.Linq; namespace Barotrauma.Items.Components { @@ -287,7 +285,7 @@ namespace Barotrauma.Items.Components } else if (autoTemp) { - UpdateAutoTemp(10.0f, deltaTime * 2f); + UpdateAutoTemp(2.0f, deltaTime); } @@ -300,7 +298,14 @@ namespace Barotrauma.Items.Components if (!item.HasTag("reactorfuel")) { continue; } if (fissionRate > 0.0f) { - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost && + Item.Submarine?.TeamID == CharacterTeamType.Team1 && + Item.Submarine.GetConnectedSubs().Any(s => s.Info.IsOutpost && s.TeamID == CharacterTeamType.FriendlyNPC); + + if (!isConnectedToFriendlyOutpost) + { + item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + } } fuelLeft += item.ConditionPercentage; } @@ -418,7 +423,7 @@ namespace Barotrauma.Items.Components { float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); - newLoad = MathHelper.Clamp(loadLeft - (expectedPower + output) + loadAdjust, 0, loadLeft); + newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft); } if (float.IsNegative(newLoad)) @@ -498,7 +503,6 @@ namespace Barotrauma.Items.Components if (temperature > optimalTemperature.Y) { - float prevFireTimer = fireTimer; fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition); #if SERVER if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item) @@ -506,9 +510,10 @@ namespace Barotrauma.Items.Components GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime); } #endif - if (fireTimer >= FireDelay && prevFireTimer < fireDelay) + if (fireTimer >= FireDelay) { new FireSource(item.WorldPosition); + fireTimer = 0.0f; } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 97657bc39..d60aad468 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -349,7 +349,7 @@ namespace Barotrauma.Items.Components } } - public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + public void ServerEventRead(IReadMessage msg, Client c) { bool isActive = msg.ReadBoolean(); bool directionalPing = useDirectionalPing; @@ -394,7 +394,7 @@ namespace Barotrauma.Items.Components #endif } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(currentMode == Mode.Active); if (currentMode == Mode.Active) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index b41f7c4bb..3a8095561 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -225,7 +225,7 @@ namespace Barotrauma.Items.Components var dockingConnection = item.Connections.FirstOrDefault(c => c.Name == "toggle_docking"); if (dockingConnection != null) { - var connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); + var connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection, allowTraversingBackwards: false); DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost)); } } @@ -271,13 +271,9 @@ namespace Barotrauma.Items.Components item.CreateClientEvent(this); correctionTimer = CorrectionDelay; } - else #endif #if SERVER - if (GameMain.Server != null) - { - item.CreateServerEvent(this); - } + item.CreateServerEvent(this); #endif networkUpdateTimer = 0.1f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 3d1631663..46eac6dca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System; using System.Globalization; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -11,7 +10,7 @@ namespace Barotrauma.Items.Components //[power/min] private float capacity; - private float charge; + private float charge, prevCharge; //how fast the battery can be recharged private float maxRechargeSpeed; @@ -34,7 +33,7 @@ namespace Barotrauma.Items.Components get { return currPowerOutput; } private set { - System.Diagnostics.Debug.Assert(value >= 0.0f); + System.Diagnostics.Debug.Assert(value >= 0.0f, $"Tried to set PowerContainer's output to a negative value ({value})"); currPowerOutput = Math.Max(0, value); } } @@ -140,6 +139,7 @@ namespace Barotrauma.Items.Components { IsActive = true; InitProjSpecific(); + prevCharge = Charge; } partial void InitProjSpecific(); @@ -171,7 +171,7 @@ namespace Barotrauma.Items.Components loadReading = powerOut.Grid.Load; } - item.SendSignal(((int)Math.Round(-CurrPowerOutput)).ToString(), "power_value_out"); + item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out"); item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out"); item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge"); item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%"); @@ -194,6 +194,8 @@ namespace Barotrauma.Items.Components } else { + if (item.Condition <= 0.0f) { return 0.0f; } + float missingCharge = capacity - charge; float targetRechargeSpeed = rechargeSpeed; @@ -231,7 +233,7 @@ namespace Barotrauma.Items.Components if (connection == powerOut) { float maxOutput; - float chargeRatio = charge / capacity; + float chargeRatio = prevCharge / capacity; if (chargeRatio < 0.1f) { maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut; @@ -242,7 +244,7 @@ namespace Barotrauma.Items.Components } //Limit max power out to not exceed the charge of the container - maxOutput = Math.Min(maxOutput, charge * 60 / UpdateInterval); + maxOutput = Math.Min(maxOutput, prevCharge * 60 / UpdateInterval); return new PowerRange(0.0f, maxOutput); } @@ -261,18 +263,11 @@ namespace Barotrauma.Items.Components /// public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load) { - if (connection == powerOut) + //Only power out connection can provide power and Max poweroutput can't be negative + if (connection == powerOut && minMaxPower.Max > 0) { - //Calculate the max power the container can output - float maxPowerOutput = MaxOutPut; - float chargeRatio = charge / capacity; - if (chargeRatio < 0.1f) - { - maxPowerOutput *= Math.Max(chargeRatio * 10.0f, 0.0f); - } - //Set power output based on the relative max power output capabilities and load demand - CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * maxPowerOutput; + CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * MinMaxPowerOut(connection, load).Max; return CurrPowerOutput; } return 0.0f; @@ -292,6 +287,7 @@ namespace Barotrauma.Items.Components { //Decrease charge based on how much power is leaving the device Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + prevCharge = Charge; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 82db50a9f..388b719fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -176,23 +176,6 @@ namespace Barotrauma.Items.Components { RefreshConnections(); - float powerReadingOut = 0; - float loadReadingOut = ExtraLoad; - if (powerLoad < 0) - { - powerReadingOut = -powerLoad; - loadReadingOut = 0; - } - - if (powerOut != null && powerOut.Grid != null) - { - powerReadingOut = powerOut.Grid.Power; - loadReadingOut = powerOut.Grid.Load; - } - - item.SendSignal(((int)Math.Round(powerReadingOut)).ToString(), "power_value_out"); - item.SendSignal(((int)Math.Round(loadReadingOut)).ToString(), "load_value_out"); - if (Timing.TotalTime > extraLoadSetTime + 1.0) { //Decay the extra load to 0 from either positive or negative @@ -216,22 +199,36 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - //if the item can't be fixed, don't allow it to break - if (!item.Repairables.Any() || !CanBeOverloaded) { return; } - - if (prevSentPowerValue != (int)-CurrPowerConsumption || powerSignal == null) + float powerReadingOut = 0; + float loadReadingOut = ExtraLoad; + if (powerLoad < 0) { - prevSentPowerValue = (int)Math.Round(-CurrPowerConsumption); + powerReadingOut = -powerLoad; + loadReadingOut = 0; + } + + if (powerOut != null && powerOut.Grid != null) + { + powerReadingOut = powerOut.Grid.Power; + loadReadingOut = powerOut.Grid.Load; + } + + if (prevSentPowerValue != (int)powerReadingOut || powerSignal == null) + { + prevSentPowerValue = (int)Math.Round(powerReadingOut); powerSignal = prevSentPowerValue.ToString(); } - if (prevSentLoadValue != (int)powerLoad || loadSignal == null) + if (prevSentLoadValue != (int)loadReadingOut || loadSignal == null) { - prevSentLoadValue = (int)Math.Round(powerLoad); + prevSentLoadValue = (int)Math.Round(loadReadingOut); loadSignal = prevSentLoadValue.ToString(); } item.SendSignal(powerSignal, "power_value_out"); item.SendSignal(loadSignal, "load_value_out"); + //if the item can't be fixed, don't allow it to break + if (!item.Repairables.Any() || !CanBeOverloaded) { return; } + float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); Overload = Voltage > maxOverVoltage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 7d0e21ff8..be83e2267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -187,14 +187,11 @@ namespace Barotrauma.Items.Components protected void UpdateOnActiveEffects(float deltaTime) { - if (currPowerConsumption <= 0.0f) + if (currPowerConsumption <= 0.0f && PowerConsumption <= 0.0f) { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - if (PowerConsumption <= 0.0f) - { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - } + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); return; } @@ -216,6 +213,11 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = false; } #endif + if (powerIn == null) + { + //power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic) + Voltage -= deltaTime; + } } public override void Update(float deltaTime, Camera cam) @@ -238,7 +240,11 @@ namespace Barotrauma.Items.Components else if (c.Name == "power_out") { powerOut = c; - powerOut.Priority = Priority; + // Connection takes the lowest priority + if (Priority > powerOut.Priority) + { + powerOut.Priority = Priority; + } } else if (c.Name == "power") { @@ -258,7 +264,11 @@ namespace Barotrauma.Items.Components #endif } powerOut = c; - powerOut.Priority = Priority; + // Connection takes the lowest priority + if (Priority > powerOut.Priority) + { + powerOut.Priority = Priority; + } } else { @@ -591,7 +601,7 @@ namespace Barotrauma.Items.Components foreach (Connection con in grid.Connections) { Powered device = con.Item.GetComponent(); - device.GridResolved(con); + device?.GridResolved(con); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 2890cc214..fcddb442b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -196,6 +196,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, IsPropertySaveable.No)] + public bool FriendlyFire + { + get; + set; + } + private float deactivationTimer; [Serialize(0f, IsPropertySaveable.No)] @@ -309,7 +316,7 @@ namespace Barotrauma.Items.Components { #if SERVER launchRot = rotation; - Item.CreateServerEvent(this, new object[] { true }); //true = indicate that this is a launch event + Item.CreateServerEvent(this, new EventData(launch: true)); #endif } } @@ -673,7 +680,7 @@ namespace Barotrauma.Items.Components { Unstick(); #if SERVER - item.CreateServerEvent(this); + item.CreateServerEvent(this, new EventData(launch: false)); #endif } } @@ -697,9 +704,9 @@ namespace Barotrauma.Items.Components return false; } if (hits.Contains(target.Body)) { return false; } - if (ShouldIgnoreSubmarineCollision(target, contact)) + if (target.Body.UserData is Submarine) { - return false; + if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; } } else if (target.Body.UserData is Limb limb) { @@ -709,6 +716,10 @@ namespace Barotrauma.Items.Components limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition); return false; } + if (!FriendlyFire && User != null && limb.character.IsFriendly(User)) + { + return false; + } } else if (target.Body.UserData is Item item) { @@ -893,8 +904,8 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember.IsServer) { - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition)); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition)); } #endif } @@ -905,8 +916,8 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember.IsServer) { - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition)); } #endif } @@ -952,7 +963,7 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - item.CreateServerEvent(this); + item.CreateServerEvent(this, new EventData(launch: false)); } #endif item.body.LinearVelocity *= speedMultiplier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 3e1439340..cbd83caea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components { ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer); #if SERVER - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, CurrentFixer.ID }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, CurrentFixer)); #endif } } @@ -256,10 +256,10 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(character, bestRepairItem)) { GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction); - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); + GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, character)); if (bestRepairItem != null && bestRepairItem.GetComponent() is Holdable h) { - GameMain.Server?.CreateEntityEvent(bestRepairItem, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, h, character.ID }); + GameMain.Server?.CreateEntityEvent(bestRepairItem, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, h, character)); } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index c31b4ff6e..b999a961b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; + protected readonly Character[] signalSender = new Character[2]; + [Serialize(999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax @@ -33,7 +35,7 @@ namespace Barotrauma.Items.Components [InGameEditable(DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." + - " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true, translationTextTag: "sp.")] public float TimeFrame { get { return timeFrame; } @@ -71,7 +73,7 @@ namespace Barotrauma.Items.Components float output = Calculate(receivedSignal[0], receivedSignal[1]); if (MathUtils.IsValid(output)) { - item.SendSignal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out"); + item.SendSignal(new Signal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } @@ -83,11 +85,13 @@ namespace Barotrauma.Items.Components { case "signal_in1": float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + signalSender[0] = signal.sender; timeSinceReceived[0] = 0.0f; IsActive = true; break; case "signal_in2": float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + signalSender[1] = signal.sender; timeSinceReceived[1] = 0.0f; IsActive = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index b9dd02749..c7fd99f51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -109,10 +109,23 @@ namespace Barotrauma.Items.Components return true; } - private void Write(IWriteMessage msg, object[] extraData) + private readonly struct EventData : IEventData { - if (extraData == null || extraData.Length < 3) { return; } - msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1); + public readonly int SignalIndex; + + public EventData(int signalIndex) + { + SignalIndex = signalIndex; + } + } + + public override bool ValidateEventData(NetEntityEvent.IData data) + => TryExtractEventData(data, out _); + + private void Write(IWriteMessage msg, NetEntityEvent.IData extraData) + { + var eventData = ExtractEventData(extraData); + msg.WriteRangedInteger(eventData.SignalIndex, 0, Signals.Length - 1); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 48258b2ca..f7f7d38eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -400,7 +400,7 @@ namespace Barotrauma.Items.Components } - public void ClientWrite(IWriteMessage msg, object[] extraData = null) + public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { #if CLIENT TriggerRewiringSound(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index fb9b68e44..cdeb9bf46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -8,6 +8,16 @@ namespace Barotrauma.Items.Components { partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable { + private readonly struct EventData : IEventData + { + public readonly CustomInterfaceElement BtnElement; + + public EventData(CustomInterfaceElement btnElement) + { + BtnElement = btnElement; + } + } + class CustomInterfaceElement : ISerializableEntity { public bool ContinuousSignal; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 111ed4ea2..e274d06eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -1,7 +1,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/Signal/NotComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs index 6f6059251..77b054dce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs @@ -1,13 +1,11 @@ -using System.Xml.Linq; - -namespace Barotrauma.Items.Components +namespace Barotrauma.Items.Components { class NotComponent : ItemComponent { private bool signalReceived; private bool continuousOutput; - [Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get { return continuousOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 9ade22e1c..ab0034584 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -372,12 +372,12 @@ namespace Barotrauma.Items.Components IsOn = on; } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write(isOn); } - public void ClientRead(ServerNetObject type, IReadMessage msg, float _) + public void ClientEventRead(IReadMessage msg, float sendingTime) { SetState(msg.ReadBoolean(), true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index 22cbef2ee..24f68fa5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components { foreach (FireSource fireSource in hull.FireSources) { - if (fireSource.IsInDamageRange(item.WorldPosition, fireSource.DamageRange * 2.0f)) { return true; } + if (fireSource.IsInDamageRange(item.WorldPosition, Math.Max(fireSource.DamageRange * 2.0f, 500.0f))) { return true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs index a8c69fdeb..4533df4ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/StringComponent.cs @@ -1,7 +1,4 @@ -using Microsoft.Xna.Framework; -using System; -using System.Globalization; -using System.Xml.Linq; +using System; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index b16dce24c..5804411b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -20,6 +20,8 @@ namespace Barotrauma.Items.Components private readonly float[] receivedSignal = new float[2]; private readonly float[] timeSinceReceived = new float[2]; + protected Character signalSender; + [Serialize(FunctionType.Sin, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { @@ -56,7 +58,7 @@ namespace Barotrauma.Items.Components { float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(angle.ToString("G", CultureInfo.InvariantCulture), "signal_out"); + item.SendSignal(new Signal(angle.ToString("G", CultureInfo.InvariantCulture), sender: signalSender), "signal_out"); } } } @@ -65,6 +67,7 @@ namespace Barotrauma.Items.Components { float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); bool sendOutputImmediately = true; + signalSender = signal.sender; switch (Function) { case FunctionType.Sin: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index ee21d0939..19588f114 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -455,12 +455,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.NetworkMember != null) { - GameMain.Client.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - nodes.Count - }); + item.CreateClientEvent(this, new ClientEventData(nodes.Count)); } #endif } @@ -482,12 +477,7 @@ namespace Barotrauma.Items.Components #if CLIENT if (GameMain.NetworkMember != null) { - GameMain.Client.CreateEntityEvent(item, new object[] - { - NetEntityEvent.Type.ComponentState, - item.GetComponentIndex(this), - nodes.Count - }); + item.CreateClientEvent(this, new ClientEventData(nodes.Count)); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index ca3398fee..824c85659 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -49,8 +49,6 @@ namespace Barotrauma.Items.Components private ChargingState currentChargingState; - private float currentBarrelSpin = 0f; - private readonly List activeProjectiles = new List(); public IEnumerable ActiveProjectiles => activeProjectiles; @@ -330,10 +328,10 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { base.OnMapLoaded(); - FindLightComponent(); if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } targetRotation = rotation; + FindLightComponent(); UpdateTransformedBarrelPos(); } @@ -736,6 +734,16 @@ namespace Barotrauma.Items.Components return true; } + private readonly struct EventData : IEventData + { + public readonly Item Projectile; + + public EventData(Item projectile) + { + Projectile = projectile; + } + } + private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f) { reload = reloadTime; @@ -749,7 +757,7 @@ namespace Barotrauma.Items.Components if (projectile != null) { activeProjectiles.Add(projectile); - projectile.Drop(null); + projectile.Drop(null, setTransform: false); if (projectile.body != null) { projectile.body.Dir = 1.0f; @@ -787,12 +795,11 @@ namespace Barotrauma.Items.Components } } - if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } - } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); + projectile.Container?.RemoveContained(projectile); } +#if SERVER + item.CreateServerEvent(this, new EventData(projectile)); +#endif ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user); LaunchProjSpecific(); @@ -1630,11 +1637,11 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - if (extraData.Length > 2) + if (TryExtractEventData(extraData, out EventData eventData)) { - msg.Write(!(extraData[2] is Item item) ? ushort.MaxValue : item.ID); + msg.Write(eventData.Projectile.ID); msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 784669f37..8c148b411 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -540,16 +540,16 @@ namespace Barotrauma.Items.Components Variant = loadedVariant; } } - public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.Write((byte)Variant); - base.ServerWrite(msg, c, extraData); + base.ServerEventWrite(msg, c, extraData); } - public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + public override void ClientEventRead(IReadMessage msg, float sendingTime) { Variant = (int)msg.ReadByte(); - base.ClientRead(type, msg, sendingTime); + base.ClientEventRead(msg, sendingTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 4348caf46..dedbe948c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -10,7 +10,7 @@ namespace Barotrauma { partial class Inventory : IServerSerializable, IClientSerializable { - public const int MaxStackSize = 32; + public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63 public class ItemSlot { @@ -18,15 +18,7 @@ namespace Barotrauma public bool HideIfEmpty; - public IEnumerable Items - { - get { return items; } - } - - public int ItemCount - { - get { return items.Count; } - } + public IReadOnlyList Items => items; public bool CanBePut(Item item, bool ignoreCondition = false) { @@ -631,7 +623,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].ItemCount < item.Prefab.MaxStackSize) { return false; } + if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; } } } else @@ -842,29 +834,30 @@ namespace Barotrauma public virtual void CreateNetworkEvent() { - if (GameMain.NetworkMember != null) + if (GameMain.NetworkMember == null) { return; } + if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } + + if (Owner is Character character) { - if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new object[] { NetEntityEvent.Type.InventoryState }); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData()); + } + else if (Owner is Item item) + { + GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData()); } } public Item FindItem(Func predicate, bool recursive) { - Item match = AllItems.FirstOrDefault(i => predicate(i)); + Item match = AllItems.FirstOrDefault(predicate); if (match == null && recursive) { foreach (var item in AllItems) { - if (item == null) { continue; } - if (item.OwnInventory != null) - { - match = item.OwnInventory.FindItem(predicate, recursive: true); - if (match != null) - { - return match; - } - } + if (item?.OwnInventory == null) { continue; } + + match = item.OwnInventory.FindItem(predicate, recursive: true); + if (match != null) { return match; } } } return match; @@ -946,16 +939,31 @@ namespace Barotrauma slots[index].RemoveItem(item); } - - public void SharedWrite(IWriteMessage msg, object[] extraData = null) + public void SharedRead(IReadMessage msg, out List[] newItemIds) + { + byte slotCount = msg.ReadByte(); + newItemIds = new List[slotCount]; + for (int i = 0; i < slotCount; i++) + { + newItemIds[i] = new List(); + int itemCount = msg.ReadRangedInteger(0, MaxStackSize); + for (int j = 0; j < itemCount; j++) + { + newItemIds[i].Add(msg.ReadUInt16()); + } + } + } + + public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { msg.Write((byte)capacity); for (int i = 0; i < capacity; i++) { - msg.WriteRangedInteger(slots[i].ItemCount, 0, MaxStackSize); - foreach (Item item in slots[i].Items) + msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize); + for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++) { - msg.Write((ushort)(item == null ? 0 : item.ID)); + var item = slots[i].Items[j]; + msg.Write(item?.ID ?? (ushort)0); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index e892953f4..1e2638509 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -20,7 +20,7 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable + partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable { public static List ItemList = new List(); public new ItemPrefab Prefab => base.Prefab as ItemPrefab; @@ -573,7 +573,7 @@ namespace Barotrauma if (connections == null) { return; } foreach (Connection c in connections.Values) { - if (c.IsPower && c.Grid != null) + if (c.IsPower) { Powered.ChangedConnections.Add(c); foreach (Connection conn in c.Recipients) @@ -827,6 +827,23 @@ namespace Barotrauma public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } + public bool HasBallastFloraInHull + { + get + { + return CurrentHull?.BallastFlora != null; + } + } + + public bool IsClaimedByBallastFlora + { + get + { + if (CurrentHull?.BallastFlora == null) { return false; } + return CurrentHull.BallastFlora.ClaimedTargets.Contains(this); + } + } + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -1417,6 +1434,7 @@ namespace Barotrauma public bool HasAccess(Character character) { + if (HiddenInGame) { return false; } if (character.IsBot && IgnoreByAI(character)) { return false; } if (!IsInteractable(character)) { return false; } var itemContainer = GetComponent(); @@ -1656,14 +1674,13 @@ namespace Barotrauma public void SendPendingNetworkUpdates() { - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsServer) { return; } - if (conditionUpdatePending) - { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); - lastSentCondition = condition; - sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; - conditionUpdatePending = false; - } + if (!(GameMain.NetworkMember is { IsServer: true })) { return; } + if (!conditionUpdatePending) { return; } + + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + lastSentCondition = condition; + sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; + conditionUpdatePending = false; } private bool isActive = true; @@ -1919,20 +1936,19 @@ namespace Barotrauma private void HandleCollision(float impact) { OnCollisionProjSpecific(impact); - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) - { - if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) - { - ApplyStatusEffects(ActionType.OnImpact, 1.0f); -#if SERVER - GameMain.Server?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact }); -#endif - } + if (GameMain.NetworkMember is { IsClient: true }) { return; } - foreach (Item contained in ContainedItems) - { - if (contained.body != null) { contained.HandleCollision(impact); } - } + if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance) + { + ApplyStatusEffects(ActionType.OnImpact, 1.0f); +#if SERVER + GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact)); +#endif + } + + foreach (Item contained in ContainedItems) + { + if (contained.body != null) { contained.HandleCollision(impact); } } } @@ -1995,14 +2011,14 @@ namespace Barotrauma /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponents(bool recursive = false) where T : ItemComponent + public List GetConnectedComponents(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent { List connectedComponents = new List(); if (recursive) { HashSet alreadySearched = new HashSet(); - GetConnectedComponentsRecursive(alreadySearched, connectedComponents); + GetConnectedComponentsRecursive(alreadySearched, connectedComponents, allowTraversingBackwards: allowTraversingBackwards); return connectedComponents; } @@ -2025,7 +2041,7 @@ namespace Barotrauma return connectedComponents; } - private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays = false) where T : ItemComponent + private void GetConnectedComponentsRecursive(HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent { ConnectionPanel connectionPanel = GetComponent(); if (connectionPanel == null) { return; } @@ -2034,18 +2050,18 @@ namespace Barotrauma { if (alreadySearched.Contains(c)) { continue; } alreadySearched.Add(c); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } } /// /// Note: This function generates garbage and might be a bit too heavy to be used once per frame. /// - public List GetConnectedComponentsRecursive(Connection c, bool ignoreInactiveRelays = false) where T : ItemComponent + public List GetConnectedComponentsRecursive(Connection c, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent { List connectedComponents = new List(); HashSet alreadySearched = new HashSet(); - GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); return connectedComponents; } @@ -2062,7 +2078,7 @@ namespace Barotrauma ("signal_in2".ToIdentifier(), "signal_out".ToIdentifier()) }.ToImmutableArray(); - private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent + private void GetConnectedComponentsRecursive(Connection c, HashSet alreadySearched, List connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent { alreadySearched.Add(c); @@ -2087,12 +2103,12 @@ namespace Barotrauma foreach (Connection wifiOutput in receiverConnections) { if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; } - GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } } } - recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays); + recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } if (ignoreInactiveRelays) @@ -2111,12 +2127,12 @@ namespace Barotrauma if (pairedConnection != null) { if (alreadySearched.Contains(pairedConnection)) { return; } - GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays); + GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards); } } } searchFromAToB(input, output); - searchFromAToB(output, input); + if (allowTraversingBackwards) { searchFromAToB(output, input); } } } @@ -2490,7 +2506,7 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Treatment, character.ID, targetLimb }); + GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb)); return; } #endif @@ -2523,12 +2539,10 @@ namespace Barotrauma } } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] - { - NetEntityEvent.Type.ApplyStatusEffect, actionType, ic, character.ID, targetLimb - }); + GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData( + actionType, ic, character, targetLimb)); } if (ic.DeleteOnUse) { remove = true; } @@ -2553,12 +2567,18 @@ namespace Barotrauma if (ic.Combine(item, user)) { isCombined = true; } } #if CLIENT - if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Combine, item.ID }); } + if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new CombineEventData(item)); } #endif return isCombined; } - public void Drop(Character dropper, bool createNetworkEvent = true) + /// + /// + /// + /// Character who dropped the item + /// Should clients be notified of the item being dropped + /// Should the transform of the physics body be updated. Only disable this if you're moving the item somewhere else / calling SetTransform manually immediately after dropping! + public void Drop(Character dropper, bool createNetworkEvent = true, bool setTransform = true) { if (createNetworkEvent) { @@ -2585,7 +2605,7 @@ namespace Barotrauma "Failed to drop the item \"" + Name + "\" (body has been removed" + (Removed ? ", item has been removed)" : ")")); } - else + else if (setTransform) { body.SetTransform(dropper.SimPosition, 0.0f); } @@ -2596,7 +2616,10 @@ namespace Barotrauma if (Container != null) { - SetTransform(Container.SimPosition, 0.0f); + if (setTransform) + { + SetTransform(Container.SimPosition, 0.0f); + } Container.RemoveContained(this); Container = null; } @@ -2646,12 +2669,12 @@ namespace Barotrauma return allProperties; } - private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly) + private void WritePropertyChange(IWriteMessage msg, ChangePropertyEventData extraData, bool inGameEditableOnly) { //ignoreConditions: true = include all ConditionallyEditable properties at this point, //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); - SerializableProperty property = extraData[1] as SerializableProperty; + SerializableProperty property = extraData.SerializableProperty; if (property != null) { var propertyOwner = allProperties.Find(p => p.Second == property); @@ -2910,9 +2933,9 @@ namespace Barotrauma } #endif - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ChangeProperty, property }); + GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property)); } } @@ -2980,7 +3003,7 @@ namespace Barotrauma #if SERVER if (createNetworkEvent) { - Spawner.CreateNetworkEvent(item, remove: false); + Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item)); } #endif @@ -3016,7 +3039,7 @@ namespace Barotrauma { if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property }); + GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs new file mode 100644 index 000000000..a4a939944 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -0,0 +1,114 @@ +using System; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class Item + { + public enum EventType + { + ComponentState = 0, + InventoryState = 1, + Treatment = 2, + ChangeProperty = 3, + Combine = 4, + Status = 5, + AssignCampaignInteraction = 6, + ApplyStatusEffect = 7, + Upgrade = 8, + + MinValue = 0, + MaxValue = 6 + } + + public interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + public struct ComponentStateEventData : IEventData + { + public EventType EventType => EventType.ComponentState; + public readonly ItemComponent Component; + public readonly ItemComponent.IEventData ComponentData; + + public ComponentStateEventData(ItemComponent component, ItemComponent.IEventData componentData) + { + Component = component; + ComponentData = componentData; + } + } + + public readonly struct InventoryStateEventData : IEventData + { + public EventType EventType => EventType.InventoryState; + public readonly ItemContainer Component; + + public InventoryStateEventData(ItemContainer component) + { + Component = component; + } + } + + public readonly struct ChangePropertyEventData : IEventData + { + public EventType EventType => EventType.ChangeProperty; + public readonly SerializableProperty SerializableProperty; + + public ChangePropertyEventData(SerializableProperty serializableProperty) + { + SerializableProperty = serializableProperty; + } + } + + private readonly struct StatusEventData : IEventData + { + public EventType EventType => EventType.Status; + } + + private readonly struct AssignCampaignInteractionEventData : IEventData + { + public EventType EventType => EventType.AssignCampaignInteraction; + } + + public readonly struct ApplyStatusEffectEventData : IEventData + { + public EventType EventType => EventType.ApplyStatusEffect; + public readonly ActionType ActionType; + public readonly ItemComponent TargetItemComponent; + public readonly Character TargetCharacter; + public readonly Limb TargetLimb; + public readonly Entity UseTarget; + public readonly Vector2? WorldPosition; + + public ApplyStatusEffectEventData( + ActionType actionType, + ItemComponent targetItemComponent = null, + Character targetCharacter = null, + Limb targetLimb = null, + Entity useTarget = null, + Vector2? worldPosition = null) + { + ActionType = actionType; + TargetItemComponent = targetItemComponent; + TargetCharacter = targetCharacter; + TargetLimb = targetLimb; + UseTarget = useTarget; + WorldPosition = worldPosition; + } + } + + private readonly struct UpgradeEventData : IEventData + { + public EventType EventType => EventType.Upgrade; + public readonly Upgrade Upgrade; + + public UpgradeEventData(Upgrade upgrade) + { + Upgrade = upgrade; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 7df7912f9..a25062e49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -48,14 +48,14 @@ namespace Barotrauma if (ItemOwnsSelf(item)) { return false; } if (i < 0 || i >= slots.Length) { return false; } if (!container.CanBeContained(item, i)) { return false; } - return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.GetMaxStackSize(i); + return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].Items.Count < container.GetMaxStackSize(i); } public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null) { if (i < 0 || i >= slots.Length) { return false; } if (!container.CanBeContained(itemPrefab, i)) { return false; } - return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].ItemCount < container.GetMaxStackSize(i); + return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i); } public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) @@ -74,7 +74,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } + if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } } } else @@ -145,8 +145,7 @@ namespace Barotrauma return; } - int componentIndex = container.Item.GetComponentIndex(container); - if (componentIndex == -1) + if (!container.Item.Components.Contains(container)) { DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components"); return; @@ -155,7 +154,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null) { if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; } - GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new object[] { NetEntityEvent.Type.InventoryState, componentIndex }); + GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 23137fab7..eaa18470a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -43,10 +43,6 @@ namespace Barotrauma OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); - if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null) - { - DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); - } RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]); RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 9e02177fb..9708fd2e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -58,7 +59,9 @@ namespace Barotrauma.MapCreatures.Behavior public float AccumulatedDamage; public float DamageVisualizationTimer; +#if CLIENT public Vector2 ShakeAmount; +#endif // Adjacent tiles, used to free up sides when this branch gets removed public readonly Dictionary Connections = new Dictionary(); @@ -286,7 +289,7 @@ namespace Barotrauma.MapCreatures.Behavior public float PowerConsumptionTimer; private float defenseCooldown, toxinsCooldown, fireCheckCooldown; - private float selfDamageTimer, toxinsTimer; + private float selfDamageTimer, toxinsTimer, toxinsSpawnTimer; private readonly List branchesVulnerableToFire = new List(); @@ -552,11 +555,12 @@ namespace Barotrauma.MapCreatures.Behavior Anger -= deltaTime; } - // This entire scope is probably very heavy for GC, need to experiment if (toxinsTimer > 0.1f) { - if (!AttackItemPrefab.IsEmpty) + toxinsSpawnTimer -= deltaTime; + if (!AttackItemPrefab.IsEmpty && toxinsSpawnTimer <= 0.0f) { + toxinsSpawnTimer = 1.0f; Dictionary> branches = new Dictionary>(); foreach (BallastFloraBranch branch in Branches) { @@ -581,7 +585,7 @@ namespace Barotrauma.MapCreatures.Behavior randomBranch.SpawningItem = true; ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab); - #warning TODO: Parent needs a nullability sanity check +#warning TODO: Parent needs a nullability sanity check Entity.Spawner?.AddItemToSpawnQueue(prefab, Parent!.Position + Offset + randomBranch.Position, Parent.Submarine, onSpawned: item => { randomBranch.AttackItem = item; @@ -826,13 +830,13 @@ namespace Barotrauma.MapCreatures.Behavior { if (root != null) { - Vector2 rootGrowthPos = Rand.Vector(rootGrowthCount * Rand.Range(3.0f, 5.0f)); + Vector2 rootGrowthPos = Rand.Vector(Math.Max(rootGrowthCount, 1) * Rand.Range(3.0f, 5.0f)); TryGrowBranch(root, TileSide.None, out List newRootGrowth, isRootGrowth: true, forcePosition: rootGrowthPos); } } #if SERVER - SendNetworkMessage(this, NetworkHeader.BranchCreate, newBranch, parent.ID); + SendNetworkMessage(new BranchCreateEventData(newBranch, parent)); #endif return true; } @@ -874,7 +878,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true, branch); + SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); } #endif } @@ -1002,8 +1006,10 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); defenseCooldown = 180f; } - - defenseCooldown = 10f; + else + { + defenseCooldown = 10f; + } } } @@ -1104,7 +1110,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!wasRemoved) { - SendNetworkMessage(this, NetworkHeader.BranchRemove, branch); + SendNetworkMessage(new BranchRemoveEventData(branch)); } #endif } @@ -1135,7 +1141,7 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); + SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); #endif } @@ -1153,7 +1159,7 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - SendNetworkMessage(this, NetworkHeader.Kill); + SendNetworkMessage(new KillEventData()); #endif } @@ -1175,7 +1181,7 @@ namespace Barotrauma.MapCreatures.Behavior _entityList.Remove(this); #if SERVER - SendNetworkMessage(this, NetworkHeader.Remove); + SendNetworkMessage(new KillEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs new file mode 100644 index 000000000..40c174e1a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraEventData.cs @@ -0,0 +1,74 @@ +using Barotrauma.Networking; + +namespace Barotrauma.MapCreatures.Behavior +{ + internal partial class BallastFloraBehavior + { + public interface IEventData : NetEntityEvent.IData + { + public NetworkHeader NetworkHeader { get; } + } + + public readonly struct SpawnEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.Spawn; + } + + private readonly struct KillEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.Kill; + } + + private readonly struct BranchCreateEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.BranchCreate; + public readonly BallastFloraBranch NewBranch; + public readonly BallastFloraBranch Parent; + + public BranchCreateEventData(BallastFloraBranch newBranch, BallastFloraBranch parent) + { + NewBranch = newBranch; + Parent = parent; + } + } + + private readonly struct BranchRemoveEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.BranchRemove; + public readonly BallastFloraBranch Branch; + + public BranchRemoveEventData(BallastFloraBranch branch) + { + Branch = branch; + } + } + + private readonly struct BranchDamageEventData : IEventData + { + public NetworkHeader NetworkHeader => NetworkHeader.BranchDamage; + public readonly BallastFloraBranch Branch; + + public BranchDamageEventData(BallastFloraBranch branch) + { + Branch = branch; + } + } + + private readonly struct InfectEventData : IEventData + { + public enum InfectState { Yes, No } + + public NetworkHeader NetworkHeader => NetworkHeader.Infect; + public readonly Item Item; + public readonly InfectState Infect; + public readonly BallastFloraBranch Infector; + + public InfectEventData(Item item, InfectState infect, BallastFloraBranch infector) + { + Item = item; + Infect = infect; + Infector = infector; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs index 7d0eb16bd..af4265b28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs @@ -17,6 +17,8 @@ namespace Barotrauma.MapCreatures.Behavior { lastState = State; State?.Exit(); + State = null; + newState.Enter(); State = newState; } @@ -35,11 +37,9 @@ namespace Barotrauma.MapCreatures.Behavior { case ExitState.Running: break; - case ExitState.ReturnLast when lastState != null && lastState.GetState() == ExitState.Running: EnterState(lastState); break; - default: EnterState(new GrowIdleState(parent)); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index ab74f2a8e..beace2553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.Networking; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 442917563..a26a12e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -237,9 +237,9 @@ namespace Barotrauma if (!fireProof) { item.ApplyStatusEffects(ActionType.OnFire, 1.0f); - if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index ee66d4483..d00151206 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -363,9 +363,9 @@ namespace Barotrauma if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) { continue; } item.ApplyStatusEffects(ActionType.OnFire, deltaTime); - if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index bc8844700..698152a24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -248,16 +248,17 @@ namespace Barotrauma } linkedTo.Clear(); + int tolerance = 1; Vector2[] searchPos = new Vector2[2]; if (IsHorizontal) { - searchPos[0] = new Vector2(rect.X, rect.Y - rect.Height / 2); - searchPos[1] = new Vector2(rect.Right, rect.Y - rect.Height / 2); + searchPos[0] = new Vector2(rect.X - tolerance, rect.Y - rect.Height / 2); + searchPos[1] = new Vector2(rect.Right + tolerance, rect.Y - rect.Height / 2); } else { - searchPos[0] = new Vector2(rect.Center.X, rect.Y); - searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height); + searchPos[0] = new Vector2(rect.Center.X, rect.Y + tolerance); + searchPos[1] = new Vector2(rect.Center.X, rect.Y - rect.Height - tolerance); } for (int i = 0; i < 2; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index d86671e8d..c83684db8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -320,7 +320,8 @@ namespace Barotrauma roomName != null && ( roomName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || roomName.Contains("bilge", StringComparison.OrdinalIgnoreCase) || - roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)); + roomName.Contains("airlock", StringComparison.OrdinalIgnoreCase) || + roomName.Contains("dockingport", StringComparison.OrdinalIgnoreCase)); private bool isWetRoom; [Editable, Serialize(false, IsPropertySaveable.Yes, description: "It's normal for this hull to be filled with water. If the room name contains 'ballast', 'bilge', or 'airlock', you can't disable this setting.")] @@ -729,9 +730,9 @@ namespace Barotrauma var decal = DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex); if (decal != null) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { false }); + GameMain.NetworkMember.CreateEntityEvent(this, new DecalEventData()); } decals.Add(decal); } @@ -739,6 +740,96 @@ namespace Barotrauma return decal; } + #region Shared network write + private void SharedStatusWrite(IWriteMessage msg) + { + msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); + + msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16); + for (int i = 0; i < Math.Min(FireSources.Count, 16); i++) + { + var fireSource = FireSources[i]; + Vector2 normalizedPos = new Vector2( + (fireSource.Position.X - rect.X) / rect.Width, + (fireSource.Position.Y - (rect.Y - rect.Height)) / rect.Height); + + msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.X, 0.0f, 1.0f), 0.0f, 1.0f, 8); + msg.WriteRangedSingle(MathHelper.Clamp(normalizedPos.Y, 0.0f, 1.0f), 0.0f, 1.0f, 8); + msg.WriteRangedSingle(MathHelper.Clamp(fireSource.Size.X / rect.Width, 0.0f, 1.0f), 0, 1.0f, 8); + } + } + + private void SharedBackgroundSectionsWrite(IWriteMessage msg, in BackgroundSectionsEventData backgroundSectionsEventData) + { + int sectorToUpdate = backgroundSectionsEventData.SectorStartIndex; + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + msg.WriteRangedInteger(sectorToUpdate, 0, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + msg.WriteRangedSingle(BackgroundSections[i].ColorStrength, 0.0f, 1.0f, 8); + msg.Write(BackgroundSections[i].Color.PackedValue); + } + } + #endregion + + #region Shared network read + public readonly struct NetworkFireSource + { + public readonly Vector2 Position; + public readonly float Size; + + public NetworkFireSource(Hull hull, Vector2 normalizedPosition, float normalizedSize) + { + Position = hull.Rect.Location.ToVector2() + + new Vector2(0, -hull.Rect.Height) + + normalizedPosition * hull.Rect.Size.ToVector2(); + Size = normalizedSize * hull.Rect.Width; + } + } + + private void SharedStatusRead(IReadMessage msg, out float newWaterVolume, out NetworkFireSource[] newFireSources) + { + newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; + + int fireSourceCount = msg.ReadRangedInteger(0, 16); + newFireSources = new NetworkFireSource[fireSourceCount]; + for (int i = 0; i < fireSourceCount; i++) + { + float x = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f); + float y = MathHelper.Clamp(msg.ReadRangedSingle(0.0f, 1.0f, 8), 0.05f, 0.95f); + float size = msg.ReadRangedSingle(0.0f, 1.0f, 8); + newFireSources[i] = new NetworkFireSource(this, new Vector2(x, y), size); + } + } + + private readonly struct BackgroundSectionNetworkUpdate + { + public readonly int SectionIndex; + public readonly Color Color; + public readonly float ColorStrength; + public BackgroundSectionNetworkUpdate(int sectionIndex, Color color, float colorStrength) + { + SectionIndex = sectionIndex; + Color = color; + ColorStrength = colorStrength; + } + } + + private void SharedBackgroundSectionRead(IReadMessage msg, Action action, out int sectorToUpdate) + { + sectorToUpdate = msg.ReadRangedInteger(0, BackgroundSections.Count - 1); + int start = sectorToUpdate * BackgroundSectionsPerNetworkEvent; + int end = Math.Min((sectorToUpdate + 1) * BackgroundSectionsPerNetworkEvent, BackgroundSections.Count - 1); + for (int i = start; i < end; i++) + { + float colorStrength = msg.ReadRangedSingle(0.0f, 1.0f, 8); + Color color = new Color(msg.ReadUInt32()); + + action(new BackgroundSectionNetworkUpdate(i, color, colorStrength)); + } + } + #endregion public override void Update(float deltaTime, Camera cam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs new file mode 100644 index 000000000..bf28c09aa --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/HullEventData.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class Hull + { + [Flags] + public enum EventType + { + Status = 0, + Decal = 1, + BackgroundSections = 2, + BallastFlora = 3, + + MinValue = 0, + MaxValue = 3 + } + + public interface IEventData : NetEntityEvent.IData + { + public EventType EventType { get; } + } + + private readonly struct StatusEventData : IEventData + { + public EventType EventType => EventType.Status; + } + + private readonly struct DecalEventData : IEventData + { + public EventType EventType => EventType.Decal; + public readonly Decal Decal; + + public DecalEventData(Decal decal) + { + Decal = decal; + } + } + + private readonly struct BackgroundSectionsEventData : IEventData + { + public EventType EventType => EventType.BackgroundSections; + public readonly int SectorStartIndex; + + public BackgroundSectionsEventData(int sectorStartIndex) + { + SectorStartIndex = sectorStartIndex; + } + } + + public readonly struct BallastFloraEventData : IEventData + { + public EventType EventType => EventType.BallastFlora; + public readonly BallastFloraBehavior Behavior; + public readonly BallastFloraBehavior.IEventData SubEventData; + + public BallastFloraEventData(BallastFloraBehavior behavior, BallastFloraBehavior.IEventData subEventData) + { + Behavior = behavior; + SubEventData = subEventData; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs index 0d653cd60..f48a4ec23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/IDamageable.cs @@ -1,24 +1,31 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; namespace Barotrauma { interface IDamageable { - Vector2 SimPosition - { - get; - } - - Vector2 WorldPosition - { - get; - } - - float Health - { - get; - } + Vector2 SimPosition { get; } + Vector2 WorldPosition { get; } + float Health { get; } AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound=true); + + + public readonly struct AttackEventData + { + public readonly ISpatialEntity Attacker; + public readonly IDamageable TargetEntity; + public readonly Limb TargetLimb; + public readonly Vector2 AttackSimPosition; + + public AttackEventData(ISpatialEntity attacker, IDamageable targetEntity, Limb targetLimb, Vector2 attackSimPosition) + { + Attacker = attacker; + TargetEntity = targetEntity; + TargetLimb = targetLimb; + AttackSimPosition = attackSimPosition; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 5fac8f4b8..cd5e684d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -18,6 +18,12 @@ namespace Barotrauma { partial class Level : Entity, IServerSerializable { + public enum EventType + { + SingleDestructibleWall, + GlobalDestructibleWall + } + //all entities are disabled after they reach this depth public const int MaxEntityDepth = -300000; public const float ShaftHeight = 1000.0f; @@ -3136,13 +3142,14 @@ namespace Barotrauma UnsyncedExtraWalls[i].Update(deltaTime); } - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) +#if SERVER + if (GameMain.NetworkMember is { IsServer: true }) { foreach (LevelWall wall in ExtraWalls) { - if (wall is DestructibleLevelWall destructibleWall && destructibleWall.NetworkUpdatePending) + if (wall is DestructibleLevelWall { NetworkUpdatePending: true } destructibleWall) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { destructibleWall }); + GameMain.NetworkMember.CreateEntityEvent(this, new SingleLevelWallEventData(destructibleWall)); destructibleWall.NetworkUpdatePending = false; } } @@ -3151,11 +3158,12 @@ namespace Barotrauma { if (ExtraWalls.Any(w => w.Body.BodyType != BodyType.Static)) { - GameMain.NetworkMember.CreateEntityEvent(this); + GameMain.NetworkMember.CreateEntityEvent(this, new GlobalLevelWallEventData()); } networkUpdateTimer = 0.0f; } } +#endif #if CLIENT backgroundCreatureManager.Update(deltaTime, cam); @@ -4288,28 +4296,5 @@ namespace Barotrauma Loaded = null; } - - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) - { - if (extraData != null && extraData.Length > 0 && extraData[0] is DestructibleLevelWall destructibleWall) - { - int index = ExtraWalls.IndexOf(destructibleWall); - msg.Write(false); - msg.Write((ushort)(index == -1 ? ushort.MaxValue : index)); - //write health using one byte - msg.Write((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); - } - else - { - msg.Write(true); - foreach (LevelWall levelWall in ExtraWalls) - { - if (levelWall.Body.BodyType == BodyType.Static) { continue; } - msg.Write(levelWall.Body.Position.X); - msg.Write(levelWall.Body.Position.Y); - msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 9bb8b1561..c3b30aac8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -30,6 +30,16 @@ namespace Barotrauma { } + private readonly struct EventData : NetEntityEvent.IData + { + public readonly LevelObject LevelObject; + + public EventData(LevelObject levelObject) + { + LevelObject = levelObject; + } + } + class SpawnPosition { public readonly GraphEdge GraphEdge; @@ -522,12 +532,12 @@ namespace Barotrauma foreach (LevelObject obj in updateableObjects) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is { IsServer: true }) { obj.NetworkUpdateTimer -= deltaTime; if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) { - GameMain.NetworkMember.CreateEntityEvent(this, new object[] { obj }); + GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); obj.NeedsNetworkSyncing = false; obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } @@ -607,9 +617,10 @@ namespace Barotrauma partial void RemoveProjSpecific(); - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { - LevelObject obj = extraData[0] as LevelObject; + if (!(extraData is EventData eventData)) { throw new Exception($"Malformed LevelObjectManager event: expected {nameof(LevelObjectManager)}.{nameof(EventData)}"); } + LevelObject obj = eventData.LevelObject; msg.WriteRangedInteger(objects.IndexOf(obj), 0, objects.Count); obj.ServerWrite(msg, c); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index a6884d570..b70413c68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -734,7 +734,7 @@ namespace Barotrauma if (prefab == null) { continue; } var qty = stockElement.GetAttributeInt("qty", 0); if (qty < 1) { continue; } - StoreStock.Add(new PurchasedItem(prefab, qty)); + StoreStock.Add(new PurchasedItem(prefab, qty, buyer: null)); } StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); @@ -792,7 +792,7 @@ namespace Barotrauma { quantity = priceInfo.MinAvailableAmount; } - stock.Add(new PurchasedItem(prefab, quantity)); + stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); } } return stock; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 91da0bd7d..84bf74588 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -34,7 +34,7 @@ namespace Barotrauma public override Sprite Sprite { get; } - public override string OriginalName => Name.Value; + public override string OriginalName { get; } public override ImmutableHashSet Tags { get; } @@ -132,7 +132,7 @@ namespace Barotrauma public StructurePrefab(ContentXElement element, StructureFile file) : base(element, file) { - Name = element.GetAttributeString("name", ""); + OriginalName = element.GetAttributeString("name", ""); ConfigElement = element; var parentType = element.Parent?.GetAttributeIdentifier("prefabtype", Identifier.Empty) ?? Identifier.Empty; @@ -144,20 +144,16 @@ namespace Barotrauma Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", ""); - if (Name.IsNullOrEmpty()) - { - Name = TextManager.Get($"EntityName.{Identifier}"); - if (!nameIdentifier.IsEmpty) - { - Name = TextManager.Get($"EntityName.{nameIdentifier}").Fallback(Name); - } + Name = TextManager.Get(nameIdentifier.IsEmpty + ? $"EntityName.{Identifier}" + : $"EntityName.{nameIdentifier}", + $"EntityName.{fallbackNameIdentifier}"); - if (!fallbackNameIdentifier.IsEmpty) - { - Name = Name.Fallback(TextManager.Get($"EntityName.{fallbackNameIdentifier}")); - } + if (parentType == "wrecked") + { + Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); } - + var tags = new HashSet(); string joinedTags = element.GetAttributeString("tags", ""); if (string.IsNullOrEmpty(joinedTags)) joinedTags = element.GetAttributeString("Tags", ""); @@ -251,14 +247,6 @@ namespace Barotrauma DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary(); #endif - if (parentType == "wrecked") - { - if (!Name.IsNullOrEmpty()) - { - Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); - } - } - string categoryStr = element.GetAttributeString("category", "Structure"); if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { @@ -323,6 +311,15 @@ namespace Barotrauma DebugConsole.ThrowError( "Structure prefab \"" + Name + "\" has no identifier. All structure prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } +#if DEBUG + if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) + { + if (!string.IsNullOrEmpty(OriginalName)) + { + DebugConsole.AddWarning($"Structure \"{(Identifier == Identifier.Empty ? Name : Identifier.Value)}\" has a hard-coded name, and won't be localized to other languages."); + } + } +#endif Tags = tags.ToImmutableHashSet(); AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 1ba321562..b205c4af5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -21,7 +21,7 @@ namespace Barotrauma None = 0, Left = 1, Right = 2 } - partial class Submarine : Entity, IServerSerializable + partial class Submarine : Entity, IServerPositionSync { public SubmarineInfo Info { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index b56eb93b7..3706116ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -219,8 +219,12 @@ namespace Barotrauma.Networking public static string ApplyDistanceEffect(string message, ChatMessageType type, Character sender, Character receiver) { if (sender == null) { return ""; } - - string spokenMsg = ApplyDistanceEffect(receiver, sender, message, SpeakRange * (1.0f - sender.SpeechImpediment / 100.0f), 3.0f); + float range = SpeakRange; + if (type == ChatMessageType.Default && sender.SpeechImpediment > 0) + { + range *= 1.0f - sender.SpeechImpediment / 100.0f; + } + string spokenMsg = ApplyDistanceEffect(receiver, sender, message, range, 3.0f); switch (type) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index c5095ed42..f48d81a46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Barotrauma.Extensions; @@ -18,6 +19,13 @@ namespace Barotrauma.Networking private static PipeType writeStream; private static PipeType readStream; + private enum WriteStatus : byte + { + Success = 0x00, + Heartbeat = 0x01, + Crash = 0xFF + } + private static ManualResetEvent writeManualResetEvent; private static volatile bool shutDown; @@ -29,6 +37,8 @@ namespace Barotrauma.Networking private static int readIncTotal; private static ConcurrentQueue msgsToWrite; + private static ConcurrentQueue errorsToWrite; + private static ConcurrentQueue msgsToRead; private static Thread readThread; @@ -44,6 +54,8 @@ namespace Barotrauma.Networking readTempBytes = new byte[ReadBufferSize]; msgsToWrite = new ConcurrentQueue(); + errorsToWrite = new ConcurrentQueue(); + msgsToRead = new ConcurrentQueue(); shutDown = false; @@ -127,9 +139,11 @@ namespace Barotrauma.Networking } } + static partial void HandleCrashString(string str); + private static void UpdateRead() { - Span msgLengthSpan = stackalloc byte[2]; + Span msgLengthSpan = stackalloc byte[3]; while (!shutDown) { CheckPipeConnected(nameof(readStream), readStream); @@ -154,13 +168,26 @@ namespace Barotrauma.Networking if (!readBytes(msgLengthSpan)) { shutDown = true; break; } int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8); + WriteStatus writeStatus = (WriteStatus)msgLengthSpan[2]; if (msgLength > 0) { byte[] msg = new byte[msgLength]; if (!readBytes(msg.AsSpan())) { shutDown = true; break; } - msgsToRead.Enqueue(msg); + switch (writeStatus) + { + case WriteStatus.Success: + msgsToRead.Enqueue(msg); + break; + case WriteStatus.Heartbeat: + //do nothing + break; + case WriteStatus.Crash: + HandleCrashString(Encoding.UTF8.GetString(msg)); + shutDown = true; + break; + } } Thread.Yield(); @@ -173,9 +200,9 @@ namespace Barotrauma.Networking { CheckPipeConnected(nameof(writeStream), writeStream); - bool msgAvailable; byte[] msg; + byte[] msg; - void writeMsg() + void writeMsg(WriteStatus writeStatus) { // It's SUPER IMPORTANT that this stack allocation // remains in this local function and is never inlined, @@ -183,11 +210,12 @@ 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[2 + msg.Length]; + Span bytesToWrite = stackalloc byte[3 + msg.Length]; bytesToWrite[0] = (byte)(msg.Length & 0xFF); bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF); - Span msgSlice = bytesToWrite.Slice(2, msg.Length); + bytesToWrite[2] = (byte)writeStatus; + Span msgSlice = bytesToWrite.Slice(3, msg.Length); msg.AsSpan().CopyTo(msgSlice); @@ -209,15 +237,20 @@ namespace Barotrauma.Networking } } - msgAvailable = msgsToWrite.TryDequeue(out msg); - while (msgAvailable) + while (errorsToWrite.TryDequeue(out var error)) { - writeMsg(); + msg = Encoding.UTF8.GetBytes(error); + writeMsg(WriteStatus.Crash); + shutDown = true; + } + + while (msgsToWrite.TryDequeue(out msg)) + { + writeMsg(WriteStatus.Success); if (shutDown) { break; } - - msgAvailable = msgsToWrite.TryDequeue(out msg); } + if (!shutDown) { writeManualResetEvent.Reset(); @@ -226,7 +259,7 @@ namespace Barotrauma.Networking if (shutDown) { return; } //heartbeat to keep the other end alive - msg = Array.Empty(); writeMsg(); + msg = Array.Empty(); writeMsg(WriteStatus.Heartbeat); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index f411d9107..8865bf8dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Steamworks.ServerList; namespace Barotrauma { @@ -196,51 +197,60 @@ namespace Barotrauma private readonly Queue spawnQueue; private readonly Queue removeQueue; - public class SpawnOrRemove + public abstract class SpawnOrRemove : NetEntityEvent.IData { public readonly Entity Entity; + public UInt16 ID => Entity.ID; + + public readonly UInt16 InventoryID; - public readonly UInt16 OriginalID, OriginalInventoryID; - public readonly int OriginalSlotIndex; - - public readonly byte OriginalItemContainerIndex; - - public readonly bool Remove = false; + public readonly byte ItemContainerIndex; + public readonly int SlotIndex; public override string ToString() { return - (Remove ? "Remove" : "Spawn") + "(" + + "(" + ((Entity as MapEntity)?.Name ?? "[NULL]") + - $", {OriginalID}, {OriginalInventoryID})"; + $", {ID}, {InventoryID}, {SlotIndex})"; } - public SpawnOrRemove(Entity entity, bool remove) + protected SpawnOrRemove(Entity entity) { Entity = entity; - OriginalID = entity.ID; - if (entity is Item item && item.ParentInventory?.Owner != null) + if (!(entity is Item { ParentInventory: { Owner: { } } } item)) { return; } + + InventoryID = item.ParentInventory.Owner.ID; + SlotIndex = item.ParentInventory.FindIndex(item); + //find the index of the ItemContainer this item is inside to get the item to + //spawn in the correct inventory in multi-inventory items like fabricators + if (item.Container == null) { return; } + + foreach (ItemComponent component in item.Container.Components) { - OriginalInventoryID = item.ParentInventory.Owner.ID; - OriginalSlotIndex = item.ParentInventory.FindIndex(item); - //find the index of the ItemContainer this item is inside to get the item to - //spawn in the correct inventory in multi-inventory items like fabricators - if (item.Container != null) + if (component is ItemContainer container && + container.Inventory == item.ParentInventory) { - foreach (ItemComponent component in item.Container.Components) - { - if (component is ItemContainer container && - container.Inventory == item.ParentInventory) - { - OriginalItemContainerIndex = (byte)item.Container.GetComponentIndex(component); - break; - } - } + ItemContainerIndex = (byte)item.Container.GetComponentIndex(component); + break; } } - Remove = remove; } } + + public sealed class SpawnEntity : SpawnOrRemove + { + public SpawnEntity(Entity entity) : base(entity) { } + public override string ToString() + => $"Spawn {base.ToString()}"; + } + + public sealed class RemoveEntity : SpawnOrRemove + { + public RemoveEntity(Entity entity) : base(entity) { } + public override string ToString() + => $"Remove {base.ToString()}"; + } public EntitySpawner() : base(null, Entity.EntitySpawnerID) @@ -397,20 +407,19 @@ namespace Barotrauma public void Update(bool createNetworkEvents = true) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } while (spawnQueue.Count > 0) { var entitySpawnInfo = spawnQueue.Dequeue(); var spawnedEntity = entitySpawnInfo.Spawn(); - if (spawnedEntity != null) - { - if (createNetworkEvents) - { - CreateNetworkEventProjSpecific(spawnedEntity, false); - } - entitySpawnInfo.OnSpawned(spawnedEntity); + if (spawnedEntity == null) { continue; } + + if (createNetworkEvents) + { + CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity)); } + entitySpawnInfo.OnSpawned(spawnedEntity); } while (removeQueue.Count > 0) @@ -422,13 +431,13 @@ namespace Barotrauma } if (createNetworkEvents) { - CreateNetworkEventProjSpecific(removedEntity, true); + CreateNetworkEventProjSpecific(new RemoveEntity(removedEntity)); } removedEntity.Remove(); } } - partial void CreateNetworkEventProjSpecific(Entity entity, bool remove); + partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove); public void Reset() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs index 9313fec02..1547b1b5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializable.cs @@ -3,28 +3,41 @@ interface INetSerializable { } /// - /// Interface for entities that the clients can send information of to the server + /// Interface for entities that the clients can send events to the server /// interface IClientSerializable : INetSerializable { #if CLIENT - void ClientWrite(IWriteMessage msg, object[] extraData = null); + void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null); #endif #if SERVER - void ServerRead(ClientNetObject type, IReadMessage msg, Client c); + void ServerEventRead(IReadMessage msg, Client c); #endif } /// - /// Interface for entities that the server can send information of to the clients + /// Interface for entities that the server can send events to the clients /// interface IServerSerializable : INetSerializable { #if SERVER - void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null); + void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null); #endif #if CLIENT - void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime); + void ClientEventRead(IReadMessage msg, float sendingTime); +#endif + } + + /// + /// Interface for entities that handle ServerNetObject.ENTITY_POSITION + /// + interface IServerPositionSync : IServerSerializable + { +#if SERVER + void ServerWritePosition(IWriteMessage msg, Client c); +#endif +#if CLIENT + void ClientReadPosition(IReadMessage msg, float sendingTime); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index a1631780f..950a58719 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -42,6 +44,13 @@ namespace Barotrauma public int NumberOfBits = 8; public bool IncludeColorAlpha = false; public int ArrayMaxSize = ushort.MaxValue; + + public readonly int OrderKey; + + public NetworkSerialize([CallerLineNumber] int lineNumber = 0) + { + OrderKey = lineNumber; + } } /// @@ -52,6 +61,7 @@ namespace Barotrauma public readonly struct ReadWriteBehavior { public delegate dynamic? ReadDelegate(IReadMessage inc, Type type, NetworkSerialize attribute); + public delegate void WriteDelegate(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg); public readonly ReadDelegate ReadAction; @@ -64,6 +74,58 @@ namespace Barotrauma } } + public readonly struct CachedReflectedVariable + { + public delegate object? GetValueDelegate(object? obj); + + public delegate void SetValueDelegate(object? obj, object? value); + + public readonly Type Type; + public readonly ReadWriteBehavior Behavior; + public readonly NetworkSerialize Attribute; + public readonly SetValueDelegate SetValue; + public readonly GetValueDelegate GetValue; + public readonly bool HasOwnAttribute; + + public CachedReflectedVariable(MemberInfo info, ReadWriteBehavior behavior, Type baseClassType) + { + Behavior = behavior; + + switch (info) + { + case PropertyInfo pi: + Type = pi.PropertyType; + GetValue = pi.GetValue; + SetValue = pi.SetValue; + break; + case FieldInfo fi: + Type = fi.FieldType; + GetValue = fi.GetValue; + SetValue = fi.SetValue; + break; + default: + throw new ArgumentException($"Expected {nameof(FieldInfo)} or {nameof(PropertyInfo)} but found {info.GetType()}.", nameof(info)); + } + + if (info.GetCustomAttribute() is { } ownAttriute) + { + HasOwnAttribute = true; + Attribute = ownAttriute; + } + else if (baseClassType.GetCustomAttribute() is { } globalAttribute) + { + HasOwnAttribute = false; + Attribute = globalAttribute; + } + else + { + throw new InvalidOperationException($"Unable to serialize \"{Type}\" in \"{baseClassType}\" because it has no {nameof(NetworkSerialize)} attribute."); + } + } + } + + private static readonly Dictionary> CachedVariables = new Dictionary>(); + private static readonly ImmutableDictionary TypeBehaviors = new Dictionary { { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) }, @@ -82,8 +144,6 @@ namespace Barotrauma { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } }.ToImmutableDictionary(); - private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid); - private static readonly ImmutableDictionary, ReadWriteBehavior> TypePredicates = new Dictionary, ReadWriteBehavior> { // Arrays @@ -99,15 +159,18 @@ namespace Barotrauma { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) }, // Option - { type => type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } + { type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Option<>), new ReadWriteBehavior(ReadOption, WriteOption) } }.ToImmutableDictionary(); + private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid); + private static readonly Dictionary cachedSomeCreateMethods = new Dictionary(); private static readonly Dictionary cachedNoneCreateMethod = new Dictionary(); - private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?"); + private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => + throw new SerializationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement {nameof(INetSerializableStruct)}?"); - private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?"); + private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new SerializationException($"Type {type} cannot be deserialized. Did you forget to implement {nameof(INetSerializableStruct)}?"); private static void WriteOption(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) { @@ -131,7 +194,7 @@ namespace Barotrauma } else { - throw new InvalidOperationException("Option type was neither None<> or Some<>"); + throw new ArgumentOutOfRangeException(nameof(obj), "Option type was neither None or Some"); } } @@ -147,7 +210,7 @@ namespace Barotrauma if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) { dynamic? value = behavior.ReadAction(inc, underlyingType, attribute); - return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new []{ value }); + return GetCreateMethod(typeof(Some<>), underlyingType, cachedSomeCreateMethods).Invoke(null, new[] { value }); } throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadOption)}"); @@ -349,7 +412,7 @@ namespace Barotrauma private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble(); private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString(); - + private static dynamic ReadIdentifier(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadIdentifier(); private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); @@ -411,7 +474,7 @@ namespace Barotrauma return new Range(values.Min(), values.Max()); } - public static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) + private static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) { if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; } @@ -427,6 +490,46 @@ namespace Barotrauma behavior = InvalidReadWriteBehavior; return false; } + + public static ImmutableArray GetPropertiesAndFields(Type type, Type baseClassType) + { + if (CachedVariables.TryGetValue(type, out var cached)) { return cached; } + + List variables = new List(); + + IEnumerable propertyInfos = type.GetProperties().Where(HasAttribute); + IEnumerable fieldInfos = type.GetFields().Where(HasAttribute); + + foreach (PropertyInfo info in propertyInfos) + { + if (TryFindBehavior(info.PropertyType, out ReadWriteBehavior behavior)) + { + variables.Add(new CachedReflectedVariable(info, behavior, baseClassType)); + } + else + { + throw new SerializationException($"Unable to serialize type \"{type}\"."); + } + } + + foreach (FieldInfo info in fieldInfos) + { + if (TryFindBehavior(info.FieldType, out ReadWriteBehavior behavior)) + { + variables.Add(new CachedReflectedVariable(info, behavior, baseClassType)); + } + else + { + throw new SerializationException($"Unable to serialize type \"{type}\"."); + } + } + + ImmutableArray array = variables.All(v => v.HasOwnAttribute) ? variables.OrderBy(v => v.Attribute.OrderKey).ToImmutableArray() : variables.ToImmutableArray(); + CachedVariables.Add(type, array); + return array; + + bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute() ?? baseClassType.GetCustomAttribute()) != null; + } } /// @@ -512,38 +615,11 @@ namespace Barotrauma object? newObject = Activator.CreateInstance(type); if (newObject is null) { return default!; } - PropertyInfo[] properties = type.GetProperties(); - foreach (PropertyInfo info in properties) + var properties = NetSerializableProperties.GetPropertiesAndFields(type, type); + foreach (NetSerializableProperties.CachedReflectedVariable property in properties) { - NetworkSerialize? attribute = GetAttribute(info, newObject); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior)) - { - object? value = behavior.ReadAction(inc, info.PropertyType, attribute); - info.SetValue(newObject, value); - } - else - { - DebugConsole.ThrowError($"Unsupported property type \"{info.PropertyType}\" in {newObject}!"); - } - } - - FieldInfo[] fields = type.GetFields(); - foreach (FieldInfo info in fields) - { - NetworkSerialize? attribute = GetAttribute(info, newObject); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior)) - { - object? value = behavior.ReadAction(inc, info.FieldType, attribute); - info.SetValue(newObject, value); - } - else - { - DebugConsole.ThrowError($"Unsupported field type \"{info.FieldType}\" in {newObject}!"); - } + NetworkSerialize attribute = property.Attribute; + property.SetValue(newObject, property.Behavior.ReadAction(inc, property.Type, attribute)); } return newObject; @@ -575,39 +651,34 @@ namespace Barotrauma /// Outgoing network message public void Write(IWriteMessage msg) { - PropertyInfo[] properties = GetType().GetProperties(); - foreach (PropertyInfo info in properties) + Type type = GetType(); + var properties = NetSerializableProperties.GetPropertiesAndFields(type, type); + foreach (NetSerializableProperties.CachedReflectedVariable property in properties) { - NetworkSerialize? attribute = GetAttribute(info, this); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior)) - { - behavior.WriteAction(info.GetValue(this), attribute, msg); - } - else - { - throw new InvalidOperationException($"Unsupported property type \"{info.PropertyType}\" in {this}"); - } - } - - FieldInfo[] fields = GetType().GetFields(); - foreach (FieldInfo info in fields) - { - NetworkSerialize? attribute = GetAttribute(info, this); - if (attribute is null) { continue; } - - if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior)) - { - behavior.WriteAction(info.GetValue(this), attribute, msg); - } - else - { - throw new InvalidOperationException($"Unsupported field type \"{info.FieldType}\" in {this}"); - } + NetworkSerialize attribute = property.Attribute; + property.Behavior.WriteAction(property.GetValue(this), attribute, msg); } } + } - private static NetworkSerialize? GetAttribute(MemberInfo info, object baseClass) => info.GetCustomAttribute() ?? baseClass.GetType().GetCustomAttribute(); + public static class WriteOnlyMessageExtensions + { +#if CLIENT + public static IWriteMessage WithHeader(this IWriteMessage msg, ClientPacketHeader header) + { + msg.Write((byte)header); + return msg; + } +#elif SERVER + public static IWriteMessage WithHeader(this IWriteMessage msg, ServerPacketHeader header) + { + msg.Write((byte)header); + return msg; + } +#endif + public static void Write(this IWriteMessage msg, INetSerializableStruct serializableStruct) + { + serializableStruct.Write(msg); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 8396c48a3..9ec8f57fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -4,47 +4,16 @@ namespace Barotrauma.Networking { abstract class NetEntityEvent { - public enum Type - { - Invalid, - ComponentState, - InventoryState, - Status, - Treatment, - ApplyStatusEffect, - ChangeProperty, - Control, - UpdateSkills, - Combine, - SetAttackTarget, - ExecuteAttack, - Upgrade, - AssignCampaignInteraction, - TeamChange, - ObjectiveManagerState, - AddToCrew, - UpdateExperience, - UpdateTalents, - UpdateMoney, - UpdatePermanentStats, - } + public interface IData { } public readonly Entity Entity; public readonly UInt16 ID; - public UInt16 EntityID - { - get; - private set; - } + public UInt16 EntityID => Entity.ID; //arbitrary extra data that will be passed to the Write method of the serializable entity //(the index of an itemcomponent for example) - public object[] Data - { - get; - private set; - } + public IData Data { get; private set; } public bool Sent; @@ -52,43 +21,18 @@ namespace Barotrauma.Networking { this.ID = id; this.Entity = serializableEntity as Entity; - RefreshEntityID(); } - public void RefreshEntityID() - { - this.EntityID = this.Entity is Entity entity ? entity.ID : Entity.NullEntityID; - } - - public void SetData(object[] data) + public void SetData(IData data) { this.Data = data; } public bool IsDuplicate(NetEntityEvent other) { - if (other.Entity != this.Entity) return false; + if (other.Entity != this.Entity) { return false; } - if (Data != null && other.Data != null) - { - if (Data.Length != other.Data.Length) return false; - - for (int i = 0; i < Data.Length; i++) - { - if (Data[i] == null) - { - if (other.Data[i] != null) return false; - } - else - { - if (other.Data[i] == null) return false; - if (!Data[i].Equals(other.Data[i])) return false; - } - } - return true; - } - - return Data == other.Data; + return Equals(Data, other.Data); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index f2bb95b69..0b154b322 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -38,7 +38,6 @@ namespace Barotrauma.Networking //(otherwise the clients might read the next event in the message and think its ID //is consecutive to the previous one, even though we skipped over this broken event) tempBuffer.Write(Entity.NullEntityID); - tempBuffer.WritePadBits(); eventCount++; continue; } @@ -53,7 +52,6 @@ namespace Barotrauma.Networking tempBuffer.Write(e.EntityID); tempBuffer.WriteVariableUInt32((uint)tempEventBuffer.LengthBytes); tempBuffer.Write(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); - tempBuffer.WritePadBits(); sentEvents.Add(e); eventCount++; @@ -61,6 +59,7 @@ namespace Barotrauma.Networking if (eventCount > 0) { + msg.WritePadBits(); msg.Write(eventsToSync[0].ID); msg.Write((byte)eventCount); msg.Write(tempBuffer.Buffer, 0, tempBuffer.LengthBytes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index dd3b80645..90f123c95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Barotrauma.Networking { - enum ClientPacketHeader + public enum ClientPacketHeader { UPDATE_LOBBY, //update state in lobby UPDATE_INGAME, //update state ingame @@ -31,9 +31,10 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic + MONEY, //wallet updates + REWARD_DISTRIBUTION, // wallet reward distribution READY_CHECK, READY_TO_SPAWN - } enum ClientNetObject { @@ -52,7 +53,7 @@ namespace Barotrauma.Networking MISSING_ENTITY //client can't find an entity of a certain ID } - enum ServerPacketHeader + public enum ServerPacketHeader { AUTH_RESPONSE, //tell the player if they require a password to log in AUTH_FAILURE, //the server won't authorize player yet, however connection is still alive @@ -82,6 +83,7 @@ namespace Barotrauma.Networking EVENTACTION, CREW, //anything related to managing bots in multiplayer MEDICAL, //medical clinic + MONEY, READY_CHECK //start, end and update a ready check } enum ServerNetObject @@ -169,7 +171,7 @@ namespace Barotrauma.Networking get { return false; } } - public abstract void CreateEntityEvent(INetSerializable entity, object[] extraData = null); + public abstract void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null); #if DEBUG public Dictionary messageCount = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index b39783e44..00711ed04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -654,7 +654,7 @@ namespace Barotrauma.Networking { get { - return lengthBits / 8; + return (LengthBits + 7) / 8; } } @@ -867,7 +867,7 @@ namespace Barotrauma.Networking { get { - return (LengthBits + ((8 - (LengthBits % 8)) % 8)) / 8; + return (LengthBits + 7) / 8; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs index 6915d1dfc..bf0a96dd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/PerformanceCounter.cs @@ -6,6 +6,8 @@ namespace Barotrauma { public class PerformanceCounter { + private readonly object mutex = new object(); + public double AverageFramesPerSecond { get; private set; } public double CurrentFramesPerSecond { get; private set; } @@ -13,34 +15,106 @@ namespace Barotrauma private readonly Queue sampleBuffer = new Queue(); + public class TickInfo + { + public Queue ElapsedTicks { get; set; } = new Queue(); + public long AvgTicksPerFrame { get; set; } + } + 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); #endif - public IEnumerable GetSavedIdentifiers + private readonly List tempSavedIdentifiers = new List(); + public IReadOnlyList GetSavedIdentifiers { - get { return avgTicksPerFrame.Keys; } + get + { + lock (mutex) + { + tempSavedIdentifiers.Clear(); + tempSavedIdentifiers.AddRange(avgTicksPerFrame.Keys); + } + return tempSavedIdentifiers; + } + } + + 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) { - if (!elapsedTicks.ContainsKey(identifier)) elapsedTicks.Add(identifier, new Queue()); - elapsedTicks[identifier].Enqueue(ticks); - - if (elapsedTicks[identifier].Count > MaximumSamples) + lock (mutex) { - elapsedTicks[identifier].Dequeue(); - avgTicksPerFrame[identifier] = (long)elapsedTicks[identifier].Average(i => i); + if (!elapsedTicks.ContainsKey(identifier)) { elapsedTicks.Add(identifier, new Queue()); } + elapsedTicks[identifier].Enqueue(ticks); + + if (elapsedTicks[identifier].Count > MaximumSamples) + { + elapsedTicks[identifier].Dequeue(); + avgTicksPerFrame[identifier] = (long)elapsedTicks[identifier].Average(i => i); + } + } + } + + 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) { - if (!avgTicksPerFrame.ContainsKey(identifier)) return 0.0f; - return avgTicksPerFrame[identifier] * 1000.0f / (float)Stopwatch.Frequency; + long ticksPerFrame = 0; + lock (mutex) + { + avgTicksPerFrame.TryGetValue(identifier, out ticksPerFrame); + } + 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 9b4584325..84690b56b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -133,10 +133,17 @@ namespace Barotrauma e.IsHighlighted = false; } - if (GameMain.GameSession != null) GameMain.GameSession.Update((float)deltaTime); #if CLIENT var sw = new System.Diagnostics.Stopwatch(); sw.Start(); +#endif + + GameMain.GameSession?.Update((float)deltaTime); + +#if CLIENT + sw.Stop(); + GameMain.PerformanceCounter.AddElapsedTicks("GameSessionUpdate", sw.ElapsedTicks); + sw.Restart(); GameMain.ParticleManager.Update((float)deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 4809ed0ca..765a62374 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -100,7 +100,7 @@ namespace Barotrauma } case ConditionType.Attachable: { - return entity is Holdable holdable && holdable.Attachable; + return entity is Holdable holdable && holdable.Attachable && Screen.Selected == GameMain.SubEditorScreen; } } return false; @@ -949,8 +949,7 @@ namespace Barotrauma break; } } - - element.Attribute(property.Name)?.Remove(); + element.GetAttribute(property.Name)?.Remove(); element.SetAttributeValue(property.Name, stringValue); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 43b51b551..dacc2c8a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -143,8 +143,9 @@ namespace Barotrauma public static string GetAttributeString(this XElement element, string name, string defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - string str = GetAttributeString(element.GetAttribute(name), defaultValue); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + string str = GetAttributeString(attribute, defaultValue); #if DEBUG if (!str.IsNullOrEmpty() && (str.Contains("%ModDir", StringComparison.OrdinalIgnoreCase) @@ -160,8 +161,9 @@ namespace Barotrauma public static string GetAttributeStringUnrestricted(this XElement element, string name, string defaultValue) { #warning TODO: remove? - if (element?.GetAttribute(name) == null) { return defaultValue; } - return GetAttributeString(element.GetAttribute(name), defaultValue); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return GetAttributeString(attribute, defaultValue); } public static bool DoesAttributeReferenceFileNameAlone(this XElement element, string name) @@ -190,14 +192,15 @@ namespace Barotrauma private static string GetAttributeString(XAttribute attribute, string defaultValue) { string value = attribute.Value; - return String.IsNullOrEmpty(value) ? defaultValue : value; + return string.IsNullOrEmpty(value) ? defaultValue : value; } public static string[] GetAttributeStringArray(this XElement element, string name, string[] defaultValue, bool trim = true, bool convertToLowerInvariant = false) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(',', ','); @@ -224,50 +227,15 @@ namespace Barotrauma foreach (string name in matchingAttributeName) { - if (element.GetAttribute(name) == null) { continue; } - - float val; - try - { - string strVal = element.GetAttribute(name).Value; - if (strVal.LastOrDefault() == 'f') - { - strVal = strVal.Substring(0, strVal.Length - 1); - } - val = float.Parse(strVal, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "!", e); - continue; - } - return val; + var attribute = element.GetAttribute(name); + if (attribute == null) { continue; } + return GetAttributeFloat(attribute, defaultValue); } return defaultValue; } - public static float GetAttributeFloat(this XElement element, string name, float defaultValue) - { - if (element?.GetAttribute(name) == null) { return defaultValue; } - - float val = defaultValue; - try - { - string strVal = element.GetAttribute(name).Value; - if (strVal.LastOrDefault() == 'f') - { - strVal = strVal.Substring(0, strVal.Length - 1); - } - val = float.Parse(strVal, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error in " + element + "!", e); - } - - return val; - } + public static float GetAttributeFloat(this XElement element, string name, float defaultValue) => GetAttributeFloat(element?.GetAttribute(name), defaultValue); public static float GetAttributeFloat(this XAttribute attribute, float defaultValue) { @@ -292,14 +260,16 @@ namespace Barotrauma return val; } - public static double GetAttributeDouble(this XElement element, string name, double defaultValue) + public static double GetAttributeDouble(this XElement element, string name, double defaultValue) => GetAttributeDouble(element?.GetAttribute(name), defaultValue); + + public static double GetAttributeDouble(this XAttribute attribute, double defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + if (attribute == null) { return defaultValue; } double val = defaultValue; try { - string strVal = element.Attribute(name).Value; + string strVal = attribute.Value; if (strVal.LastOrDefault() == 'f') { strVal = strVal.Substring(0, strVal.Length - 1); @@ -308,17 +278,19 @@ namespace Barotrauma } catch (Exception e) { - DebugConsole.ThrowError("Error in " + element + "!", e); + DebugConsole.ThrowError("Error in " + attribute + "!", e); } return val; } + public static float[] GetAttributeFloatArray(this XElement element, string name, float[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -345,13 +317,14 @@ namespace Barotrauma public static int GetAttributeInt(this XElement element, string name, int defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } int val = defaultValue; try { - if (!Int32.TryParse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) + if (!Int32.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out val)) { val = (int)float.Parse(element.GetAttribute(name).Value, CultureInfo.InvariantCulture); } @@ -366,13 +339,14 @@ namespace Barotrauma public static uint GetAttributeUInt(this XElement element, string name, uint defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } uint val = defaultValue; try { - val = UInt32.Parse(element.GetAttribute(name).Value); + val = UInt32.Parse(attribute.Value); } catch (Exception e) { @@ -384,13 +358,14 @@ namespace Barotrauma public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = UInt64.Parse(element.GetAttribute(name).Value, NumberStyles.Any, CultureInfo.InvariantCulture); + val = UInt64.Parse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture); } catch (Exception e) { @@ -402,13 +377,14 @@ namespace Barotrauma public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue) { - if (element?.GetAttribute(name) == null) return defaultValue; + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } Version val = defaultValue; try { - val = Version.Parse(element.GetAttribute(name).Value); + val = Version.Parse(attribute.Value); } catch (Exception e) { @@ -420,13 +396,14 @@ namespace Barotrauma public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } UInt64 val = defaultValue; try { - val = Steam.SteamManager.SteamIDStringToUInt64(element.GetAttribute(name).Value); + val = Steam.SteamManager.SteamIDStringToUInt64(attribute.Value); } catch (Exception e) { @@ -438,9 +415,10 @@ namespace Barotrauma public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -462,9 +440,10 @@ namespace Barotrauma } public static ushort[] GetAttributeUshortArray(this XElement element, string name, ushort[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(','); @@ -496,8 +475,9 @@ namespace Barotrauma public static bool GetAttributeBool(this XElement element, string name, bool defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - return element.GetAttribute(name).GetAttributeBool(defaultValue); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return attribute.GetAttributeBool(defaultValue); } public static bool GetAttributeBool(this XAttribute attribute, bool defaultValue) @@ -520,45 +500,52 @@ namespace Barotrauma public static Point GetAttributePoint(this XElement element, string name, Point defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - return ParsePoint(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParsePoint(attribute.Value); } public static Vector2 GetAttributeVector2(this XElement element, string name, Vector2 defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } - return ParseVector2(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseVector2(attribute.Value); } public static Vector3 GetAttributeVector3(this XElement element, string name, Vector3 defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseVector3(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseVector3(attribute.Value); } public static Vector4 GetAttributeVector4(this XElement element, string name, Vector4 defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseVector4(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseVector4(attribute.Value); } public static Color GetAttributeColor(this XElement element, string name, Color defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseColor(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseColor(attribute.Value); } public static Color? GetAttributeColor(this XElement element, string name) { - if (element == null || element.GetAttribute(name) == null) { return null; } - return ParseColor(element.GetAttribute(name).Value); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return null; } + return ParseColor(attribute.Value); } public static Color[] GetAttributeColorArray(this XElement element, string name, Color[] defaultValue) { - if (element?.GetAttribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.GetAttribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } string[] splitValue = stringValue.Split(';'); @@ -602,8 +589,9 @@ namespace Barotrauma public static Rectangle GetAttributeRect(this XElement element, string name, Rectangle defaultValue) { - if (element == null || element.GetAttribute(name) == null) { return defaultValue; } - return ParseRect(element.GetAttribute(name).Value, false); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } + return ParseRect(attribute.Value, false); } //TODO: nested tuples and and n-uples where n!=2 are unsupported @@ -617,9 +605,10 @@ namespace Barotrauma public static (T1, T2)[] GetAttributeTupleArray(this XElement element, string name, (T1, T2)[] defaultValue) { - if (element?.Attribute(name) == null) { return defaultValue; } + var attribute = element?.GetAttribute(name); + if (attribute == null) { return defaultValue; } - string stringValue = element.Attribute(name).Value; + string stringValue = attribute.Value; if (string.IsNullOrEmpty(stringValue)) { return defaultValue; } return stringValue.Split(';').Select(s => ParseTuple(s, default)).ToArray(); @@ -918,6 +907,8 @@ namespace Barotrauma public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); + public static void SetAttributeValue(this XElement element, string name, object value, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => GetAttribute(element, name, comparisonMethod)?.SetValue(value); + public static XAttribute GetAttribute(this XElement element, Identifier name) => element.GetAttribute(name.Value, StringComparison.OrdinalIgnoreCase); public static XAttribute GetAttribute(this XElement element, Func predicate) => element.Attributes().FirstOrDefault(predicate); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index bc1da8b5c..688ebbfbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -1227,7 +1228,15 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) { Entity.Spawner?.AddEntityToRemoveQueue(character); } + var target = targets[i]; + if (target is Character character) + { + Entity.Spawner?.AddEntityToRemoveQueue(character); + } + else if (target is Limb limb) + { + Entity.Spawner?.AddEntityToRemoveQueue(limb.character); + } } } if (breakLimb || hideLimb) @@ -1848,11 +1857,11 @@ namespace Barotrauma float prevVitality = targetCharacter.Vitality; if (targetLimb != null) { - targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount * deltaTime, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnLimb(targetLimb, affliction, reduceAmount, treatmentAction: actionType); } else { - targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount * deltaTime, treatmentAction: actionType); + targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType); } if (element.User != null && element.User != targetCharacter) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 4a5731b4a..3963df1ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -30,8 +30,6 @@ namespace Barotrauma public readonly HashSet EnteredCrushDepth = new HashSet(); public readonly HashSet ReactorMeltdown = new HashSet(); - public readonly HashSet Casualties = new HashSet(); - public bool SubWasDamaged; } @@ -269,8 +267,6 @@ namespace Barotrauma } #endif - roundData?.Casualties.Add(character); - UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier()); if (character.CurrentHull != null) { @@ -406,7 +402,7 @@ namespace Barotrauma //made it to the destination if (gameSession.Submarine.AtEndExit) { - bool noDamageRun = !roundData.SubWasDamaged && !roundData.Casualties.Any(c => !(c.AIController is EnemyAIController)); + bool noDamageRun = !roundData.SubWasDamaged && !gameSession.Casualties.Any(); #if SERVER if (GameMain.Server != null) @@ -434,8 +430,8 @@ namespace Barotrauma if (charactersInSub.Count == 1) { - //there must be some non-enemy casualties to get the last mant standing achievement - if (roundData.Casualties.Any(c => !(c.AIController is EnemyAIController) && c.TeamID == charactersInSub[0].TeamID)) + //there must be some casualties to get the last mant standing achievement + if (gameSession.Casualties.Any()) { UnlockAchievement(charactersInSub[0], "lastmanstanding".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs index 30e3678e7..ff86da1d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -32,7 +32,7 @@ namespace Barotrauma #if CLIENT private readonly GUIFont? font; private readonly GUIComponentStyle? componentStyle; - private bool forceUpperCase = false; + private readonly bool forceUpperCase = false; private bool fontOrStyleForceUpperCase => font is { ForceUpperCase: true } || componentStyle is { ForceUpperCase: true }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs new file mode 100644 index 000000000..7b46b9467 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal sealed class NamedEvent : IDisposable + { + private readonly Dictionary> events = new Dictionary>(); + + ~NamedEvent() + { + ReleaseUnmanagedResources(); + } + + public void Register(Identifier identifier, Action action) + { + if (HasEvent(identifier)) + { + throw new ArgumentException($"Event with the identifier \"{identifier}\" has already been registered.", nameof(identifier)); + } + + events.Add(identifier, action); + } + + public void RegisterOverwriteExisting(Identifier identifier, Action action) + { + if (HasEvent(identifier)) + { + Deregister(identifier); + } + + Register(identifier, action); + } + + public void Deregister(Identifier identifier) + { + events.Remove(identifier); + } + + public bool HasEvent(Identifier identifier) => events.ContainsKey(identifier); + + public void Invoke(T data) + { + foreach (var (_, action) in events) + { + action?.Invoke(data); + } + } + + private void ReleaseUnmanagedResources() + { + events.Clear(); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs index 1bdc94160..c9152e8c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Option/Option.cs @@ -1,3 +1,5 @@ +using System; + namespace Barotrauma { /// @@ -10,5 +12,15 @@ namespace Barotrauma { public static Option Some(T value) => Some.Create(value); public static Option None() => None.Create(); + public bool IsNone() => this is None; + public bool IsSome() => this is Some; + + public Option Select(Func selector) => + this switch + { + Some { Value: var value } => Option.Some(selector.Invoke(value)), + None _ => Option.None(), + _ => throw new ArgumentOutOfRangeException() + }; } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index 47445df31..2522ce1ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -49,7 +49,7 @@ namespace Barotrauma string[] attributes = segments[i].Split(attributeSeparator); for (int j = 0; j < attributes.Length; j++) { - if (attributes[j].Contains(endDefinition)) + if (attributes[j].Contains(endDefinition, System.StringComparison.OrdinalIgnoreCase)) { if (tempData != null) { @@ -59,7 +59,7 @@ namespace Barotrauma } tempData = null; } - else if (attributes[j].StartsWith(colorDefinition)) + else if (attributes[j].StartsWith(colorDefinition, System.StringComparison.OrdinalIgnoreCase)) { if (tempData == null) { tempData = new RichTextData(); } string valueStr = attributes[j].Substring(attributes[j].IndexOf(keyValueSeparator) + 1); @@ -72,7 +72,7 @@ namespace Barotrauma tempData.Color = XMLExtensions.ParseColor(valueStr); } } - else if (attributes[j].StartsWith(metadataDefinition)) + else if (attributes[j].StartsWith(metadataDefinition, System.StringComparison.OrdinalIgnoreCase)) { if (tempData == null) { tempData = new RichTextData(); } tempData.Metadata = attributes[j].Substring(attributes[j].IndexOf(keyValueSeparator) + 1); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index e863904a0..a24e889aa 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,87 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.1.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Added personal wallets. Everyone in the crew now has their personal wallet that they can use to purchase whatever they wish in outposts. The host (or people with campaign management permissions) can distribute a portion of the mission rewards to the crew or transfer money from the shared funds to the players. +- Made supercapacitors take some damage when they're being charged faster than 70%. +- Increased Orca 2's reactor output. +- Adjustments to mission distribution: only easy missions at the beginning of the campaign, moved some of the more difficult missions later into the campaign. +- Made Not Component's ContinuousOutput property editable in-game. +- Show warnings when saving a sub in the sub editor if any of the entity counts (walls, items, lights, etc) are very high, don't allow saving if the light counts are above the upper limits. +- Visual/layout mprovements to the Workshop menu. +- Added "power_value_out" and "load_value_out" connections to relays, batteries and supercapacitors. +- Increased SMG Magazine's capacity to 21 to make it divisible with the Deadeye Carbine's 3-bullet bursts (unstable only). +- Made molochs, abyss monsters and fractal guardians immune to poisons. +- Boosted the structure damage from the small crawler eggs from 150 to 200. +- Adjusted structure and item damage for coilgun ammunition: piercing 50% less damage, exploding 100% more damage (from explosions), physicorium 50% more damage. +- Don't populate the abyss in difficulty levels 0 to 10 in single mission mode. +- Revisited endworm: the armor now breaks less easily, reduced the change of cutting the worm towards the head, adjusted the bleeding speed. + +Fixes: +- Attempt to fix occasional performance drops in the store interface. +- Fixed clients seeing a blank server lobby if they join when a round is running with respawning disabled. +- Fixed arithmetic and trigonometric components not passing the sender of the signal forwards, preventing e.g. helm skill from boosting engines if the signal goes through a component. +- Fixed occasional crashes when transitioning between levels with showperf enabled in multiplayer campaign. +- Fixed wearables not affecting movement speed when godmode is on. +- Fixed disconnected players preventing talents that require everyone to survive from working (e.g. "Field Medic", "Bootcamp"). +- Fixed workshop menu's "popular mods" and "publish" tabs being usable when not connected to Steam (unstable only). +- Fixed text displays' depth being forced to 0.85, causing draw order issues in older subs (unstable only). +- Fixes periscopes not focusing on turrets if there's certain components (e.g. arithmetic components) between them. +- Fixed crashing when trying to use the "dumpeventtexts" command with no arguments or a disallowed path. +- Made smoke detectors a little more sensitive (should fix small fires sometimes not being detected even if they're in the same room). +- Fixed reactor sometimes not catching fire again if you start overheating it again immediately after a fire. +- Fixed broken batteries consuming power (unstable only). +- Fixed sub editor test mode spawning the crew in an inconsistent order, sometimes causing the player to start as someone else than the captain (unstable only). +- Fixed recycling a non-empty SMG magazine dropping the bullet inside it on the floor. +- Fixed "Trusted Captain" and "Esteemed Captain" giving medals even when no missions have been completed. +- Fixed small pump's ballast flora infection sprite (unstable only). +- Fixed text colors being broken in campaign map tooltips (unstable only). +- Fixed text colors not working in texts forced to upper case (e.g. reputation texts in the round summary). Unstable only. +- Fixed EntitySpawnerComponent's SpawnAreaOffset.Y being inverted. +- Fixed gaps generating incorrectly on "Shell A 70 Degrees". +- Fixed items powered by battery cells not working correctly (certain devices like handheld sonar beacons never powering up, and items staying powered indefinitely when you put in a battery and take it out). Unstable only. +- Fixed turret lights starting in an incorrect rotation in the sub editor. +- Fixed "commander" talent still not correctly giving the buff (giving orders that a character already had didn't move the buff). Unstable only. +- Fixed nav terminals sometimes determining which docking port the docking button controls incorrectly (specifically, when the correct docking port is also connected to other ports). +- Fixed campaign saves being considered incompatible if you install some new custom subs (unstable only). +- Fixed an exploit in the depleted fuel SMG magazine recipe. +- Fixed reactor gauges' background sticking out from the gauges when selecting a reactor in the editor. +- Fixed projectiles bouncing off subs without doing any damage (unstable only). +- Fixed crashing when clicking a turret in the sub upgrade menu's submarine preview (unstable only). +- Fixed character variants failing to load animations from the base character (unstable only). +- Fixed deltatime being applied twice to affliction reductions, making meds do practically nothing (unstable only). +- Fixed order of the command menu changing on each launch (unstable only). +- Fixed talent options switching places on each launch (unstable only). +- Fixed submarine upgrades and factions in the round summary switching places on each launch (unstable only). +- Fixed texture compression causing the preview image saving to fail when dragging and dropping the preview image into the save screen via file explorer (unstable only). +- Fixed disguises not working in multiplayer (unstable only). +- Fixed server host getting stuck in a lobby creation loop if their server crashes (unstable only). +- Fixed fractal guardians fleeing to the shelter immediatedly after taking some damage when they have targeted the guardian pod once and have not changed the target yet (e.g. if you shoot a guardian that is returning from the pod and if it has not yet spotted you). +- Fixed Crawler Broodmother regenerating really fast while eating. Broodmother no longer eats her own eggs. +- Fixed monsters not being able to target items (unstable only). +- Fixed spinelings accidentally killing each other with their spikes. +- Fixed "error loading a previously saved order" errors when transitioning between levels (unstable only). +- Fixed burn being ignored in the damagemodifier of Charybdis' head. The jaw worked correctly. Affects pulse laser damage for example. + +AI: +- Adjusted bot behavior around ballast flora: priority of some objectives now drops to 0 when the target's been claimed by ballast flora, items claimed by ballast flora aren't valid targets for some objectives anymore. +- Made bot healing dialog reflect if CPR was performed or not. +- Fixed security from the player's own crew attacking the player in multiplayer when the player attacks someone in an outpost. +- Fix bots not ignoring items marked to be "Hidden In Game". +- Bots prefer not to take diving suits off in rooms marged with the "IsWetRoom" flag. +- Docking ports are now automatically considered as "wet rooms". +- Fixed bots trying to target through doors and walls even though there's no line of sight between the end node and the target. +- Added some dialogue to bots when they get infected with the husk infection. + +Modding: +- Fixed clients not gaining control of the final stage of a husk affliction when "controlhusk" is enabled. +- When using a mod that doesn't set the InitialCount of any job, choose the first 3 jobs as the starting crew. Otherwise the crew customization menu will be empty and starting the campaign will lead to an immediate game over. +- Made it possible for attack StatusEffects to target the character doing the attack instead of the limb by using "Parent" as the target type. +- Using RemoveCharacter on a limb removes the character that limb belongs to. +- Fixed editing human character in the character editor sometimes making the inventory inaccessible. +- Fixed character editor crashing when trying to copy a character (unstable only). + --------------------------------------------------------------------------------------------------------- v0.17.0.0 --------------------------------------------------------------------------------------------------------- From cefac171f5de72a774145809912f8cb9c5548cdf Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 18 Mar 2022 04:20:02 +0900 Subject: [PATCH 3/9] Unstable 0.17.2.0 --- .../ClientSource/Characters/CharacterInfo.cs | 4 +- .../ClientSource/GUI/MedicalClinicUI.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 9 +- .../ClientSource/GameSession/CargoManager.cs | 2 +- .../GameModes/SinglePlayerCampaign.cs | 1 + .../ClientSource/GameSession/RoundSummary.cs | 3 +- .../ClientSource/Map/Submarine.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 7 +- .../CharacterEditor/CharacterEditorScreen.cs | 3 +- .../ClientSource/Screens/EditorScreen.cs | 12 ++ .../Screens/EventEditor/EventEditorScreen.cs | 5 - .../ClientSource/Screens/LevelEditorScreen.cs | 3 +- .../Screens/ParticleEditorScreen.cs | 3 +- .../Screens/SpriteEditorScreen.cs | 3 +- .../ClientSource/Screens/SubEditorScreen.cs | 12 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 4 +- .../SharedSource/Characters/CharacterInfo.cs | 109 ++++++++++++++---- .../Characters/CharacterPrefab.cs | 2 +- .../Characters/Params/CharacterParams.cs | 12 +- .../SharedSource/Events/Missions/Mission.cs | 2 +- .../SharedSource/Items/CharacterInventory.cs | 2 +- .../Items/Components/Machines/Controller.cs | 2 +- .../Items/Components/Signal/LightComponent.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 62 +++++----- .../Map/Levels/LevelGenerationParams.cs | 14 +++ .../SharedSource/Utils/Rand.cs | 34 ------ Barotrauma/BarotraumaShared/changelog.txt | 21 ++++ 33 files changed, 214 insertions(+), 135 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 5d2358563..8ee874c7d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -842,7 +842,7 @@ namespace Barotrauma ContentXElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); ContentXElement headSpriteElement = headElement.GetChildElement("sprite"); - string spritePathWithTags = headSpriteElement.Attribute("texture").Value; + ContentPath spritePathWithTags = headSpriteElement.GetAttributeContentPath("texture"); var characterConfigElement = info.CharacterConfigElement; @@ -853,7 +853,7 @@ namespace Barotrauma itemsInRow = 0; foreach (var head in heads.Where(h => h.TagSet.Contains(selectedCategory))) { - string spritePath = info.Prefab.ReplaceVars(spritePathWithTags, head); + string spritePath = info.Prefab.ReplaceVars(spritePathWithTags.Value, head); if (!File.Exists(spritePath)) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 6c530a3a7..ed32a00b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -521,7 +521,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) { - TextGetter = () => $"{(int)(info.Character?.HealthPercentage ?? 100f)}%", + TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)(info.Character?.HealthPercentage ?? 100f)}"), TextColor = GUIStyle.Green }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index c31514929..201d8d242 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -850,7 +850,7 @@ namespace Barotrauma 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), $"{Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage}%", textAlignment: Alignment.BottomRight); + GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()), textAlignment: Alignment.BottomRight); 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) { @@ -860,7 +860,7 @@ namespace Barotrauma BarSize = 0.1f, OnMoved = (bar, scroll) => { - rewardBlock.Text = $"{Mission.GetRewardShare((int)(scroll * 100f), salaryCrew, Option.None()).Percentage}%"; + rewardBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()); return true; }, OnReleased = (bar, scroll) => @@ -1090,10 +1090,7 @@ namespace Barotrauma GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - static int GetRewardDistributionPercentage(int distribution, ImmutableArray crew) - { - return Mission.GetRewardShare(distribution, crew, Option.None()).Percentage; - } + string GetSharePercentage() => Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage.ToString(); } private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 81e700dc4..3c9ac933b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -184,7 +184,7 @@ namespace Barotrauma } // Exchange money Location.StoreCurrentBalance -= itemValue; - campaign.Wallet.TryDeduct(itemValue); + campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 7f8e0c43f..ffa578204 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -125,6 +125,7 @@ namespace Barotrauma InitUI(); + //backwards compatibility for saves made prior to the addition of personal wallets int oldMoney = element.GetAttributeInt("money", 0); if (oldMoney > 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index a25c7787f..2770fc3c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -315,9 +315,8 @@ namespace Barotrauma int reward = displayedMission.GetReward(Submarine.MainSub); if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && reward > 0) { - LocalizedString rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); - if (Character.Controlled is { } controlled) + if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); if (share > 0) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index b6968b44f..8f48882e1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -677,7 +677,7 @@ namespace Barotrauma if (item.ParentInventory == null) { continue; } disabledItemLightCount += item.GetComponents().Count(); } - return GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; + return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount; } public void ClientReadPosition(IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 4083f49cb..559fe77b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -666,8 +666,11 @@ namespace Barotrauma.Networking if (ChildServerRelay.Process?.HasExited ?? true) { Disconnect(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; + 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; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 3f382e8f7..56032d102 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -243,9 +243,8 @@ namespace Barotrauma.CharacterEditor character.AnimController.ForceSelectAnimationType = AnimationType.NotDefined; } - public override void Deselect() + protected override void DeselectEditorSpecific() { - base.Deselect(); SoundPlayer.OverrideMusicType = Identifier.Empty; GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameSettings.CurrentConfig.Audio.SoundVolume, 0); GUI.ForceMouseOn(null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 9b68e1dbe..be295b3ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -7,6 +7,18 @@ namespace Barotrauma public static Color BackgroundColor = GameSettings.CurrentConfig.SubEditorBackground; public override bool IsEditor => true; + public override sealed void Deselect() + { + DeselectEditorSpecific(); + //reset cheats the player might have used in the editor + GameMain.LightManager.LightingEnabled = true; + GameMain.LightManager.LosEnabled = true; + Hull.EditFire = false; + Hull.EditWater = false; + } + + protected virtual void DeselectEditorSpecific() { } + public void CreateBackgroundColorPicker() { var msgBox = new GUIMessageBox(TextManager.Get("CharacterEditor.EditBackgroundColor"), "", new[] { TextManager.Get("Reset"), TextManager.Get("OK")}, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 890a4b1ce..395391803 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -519,11 +519,6 @@ namespace Barotrauma base.Select(); } - public override void Deselect() - { - base.Deselect(); - } - public override void AddToGUIUpdateList() { GuiFrame.AddToGUIUpdateList(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 71bc8cab5..470977caf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -289,9 +289,8 @@ namespace Barotrauma UpdateLevelObjectsList(); } - public override void Deselect() + protected override void DeselectEditorSpecific() { - base.Deselect(); pointerLightSource?.Remove(); pointerLightSource = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 418ee66ed..81473f6d6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -153,9 +153,8 @@ namespace Barotrauma RefreshPrefabList(); } - public override void Deselect() + protected override void DeselectEditorSpecific() { - base.Deselect(); GameMain.ParticleManager.Camera = GameMain.GameScreen.Cam; filterBox.Text = ""; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index f2d230caf..7e5b5e6ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -850,9 +850,8 @@ namespace Barotrauma spriteList.Select(0, autoScroll: false); } - public override void Deselect() + protected override void DeselectEditorSpecific() { - base.Deselect(); loadedSprites.ForEach(s => s.Remove()); loadedSprites.Clear(); ResetWidgets(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 14d7493a5..2e55fe64b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -22,7 +22,7 @@ namespace Barotrauma public const int MaxStructures = 2000; public const int MaxWalls = 500; public const int MaxItems = 5000; - public const int MaxLights = 300; + public const int MaxLights = 600; public const int MaxShadowCastingLights = 60; private static Submarine MainSub @@ -852,7 +852,7 @@ namespace Barotrauma lightCount += item.GetComponents().Count(); } lightCountText.TextColor = lightCount > MaxLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxLights); - return lightCount.ToString(); + return lightCount.ToString() + "/" + MaxLights; }; var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, wrap: true); @@ -863,10 +863,10 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.ParentInventory != null) { continue; } - lightCount += item.GetComponents().Count(l => l.CastShadows); + lightCount += item.GetComponents().Count(l => l.CastShadows && !l.DrawBehindSubs); } shadowCastingLightCountText.TextColor = lightCount > MaxShadowCastingLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxShadowCastingLights); - return lightCount.ToString(); + return lightCount.ToString() + "/" + MaxShadowCastingLights; }; entityCountPanel.RectTransform.NonScaledSize = new Point( @@ -1508,10 +1508,8 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public override void Deselect() + protected override void DeselectEditorSpecific() { - base.Deselect(); - CloseItem(); autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 5b77eb340..80a620bcc 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.1.0 + 0.17.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 48d9ebdc3..20a82c153 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.1.0 + 0.17.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 7ee9ab78d..35a96ffc0 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.1.0 + 0.17.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index b77b4c435..c599af014 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.1.0 + 0.17.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index d22aef776..036a8d02c 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.1.0 + 0.17.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ecbbeb3e3..989383c24 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.1.0 + 0.17.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 4dae997fb..8a2f1ef0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -162,7 +162,9 @@ namespace Barotrauma CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty, TargetName = Leak.FlowTargetHull?.DisplayName, - requiredCondition = () => Leak.Submarine == character.Submarine, + requiredCondition = () => + Leak.Submarine == character.Submarine && + (Leak.FlowTargetHull != null && character.CurrentHull == Leak.FlowTargetHull || character.CanSeeTarget(Leak)), // 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/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 4142ba190..f2bc10d8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -21,13 +21,25 @@ namespace Barotrauma public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement) { Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray(); - VarTags = varsElement.Elements() - .Select(e => - (e.GetAttributeIdentifier("var", ""), - e.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet())) - .ToImmutableDictionary(); - MenuCategoryVar = menuCategoryElement.GetAttributeIdentifier("var", Identifier.Empty); - Pronouns = pronounsElement.GetAttributeIdentifier("vars", Identifier.Empty); + if (varsElement != null) + { + VarTags = varsElement.Elements() + .Select(e => + (e.GetAttributeIdentifier("var", ""), + e.GetAttributeIdentifierArray("tags", Array.Empty()).ToImmutableHashSet())) + .ToImmutableDictionary(); + } + else + { + VarTags = new[] + { + ("GENDER".ToIdentifier(), + new[] { "female".ToIdentifier(), "male".ToIdentifier() }.ToImmutableHashSet()) + }.ToImmutableDictionary(); + } + + MenuCategoryVar = menuCategoryElement?.GetAttributeIdentifier("var", Identifier.Empty) ?? "GENDER".ToIdentifier(); + Pronouns = pronounsElement?.GetAttributeIdentifier("vars", Identifier.Empty) ?? "GENDER".ToIdentifier(); } public string ReplaceVars(string str, CharacterInfo.HeadPreset headPreset) { @@ -135,7 +147,13 @@ namespace Barotrauma public string Tags { get { return string.Join(",", TagSet); } - private set { TagSet = value.Split(",").Select(s => s.ToIdentifier()).ToImmutableHashSet(); } + private set + { + TagSet = value.Split(",") + .Select(s => s.ToIdentifier()) + .Where(id => !id.IsEmpty) + .ToImmutableHashSet(); + } } [Serialize("0,0", IsPropertySaveable.No)] @@ -149,6 +167,20 @@ namespace Barotrauma { characterInfoPrefab = charInfoPrefab; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + DetermineTagsFromLegacyFormat(element); + } + + private void DetermineTagsFromLegacyFormat(XElement element) + { + void addTag(string tag) + => TagSet = TagSet.Add(tag.ToIdentifier()); + + string headId = element.GetAttributeString("id", ""); + string gender = element.GetAttributeString("gender", ""); + string race = element.GetAttributeString("race", ""); + if (!headId.IsNullOrEmpty()) { addTag($"head{headId}"); } + if (!gender.IsNullOrEmpty()) { addTag(gender); } + if (!race.IsNullOrEmpty()) { addTag(race); } } } @@ -438,12 +470,37 @@ namespace Barotrauma public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors; public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors; - private void GetName(ContentPath namesFile, Rand.RandSync randSync, out string name) + private void GetName(Rand.RandSync randSync, out string name) { - XDocument doc = XMLExtensions.TryLoadXml(namesFile); - name = doc.Root.GetAttributeString("format", ""); + var nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name"); + ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty; + XElement namesXml = null; + if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined + { + XDocument doc = XMLExtensions.TryLoadXml(namesXmlFile); + namesXml = doc.Root; + } + else //the legacy firstnames.txt/lastnames.txt shit is defined + { + namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]")); + var firstNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? ""); + var lastNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? ""); + if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath)) + { + var firstNames = File.ReadAllLines(firstNamesPath); + var lastNames = File.ReadAllLines(lastNamesPath); + namesXml.Add(firstNames.Select(n => new XElement("firstname", new XAttribute("value", n)))); + namesXml.Add(lastNames.Select(n => new XElement("lastname", new XAttribute("value", n)))); + } + else //the files don't exist, just fall back to the vanilla names + { + XDocument doc = XMLExtensions.TryLoadXml("Content/Characters/Human/names.xml"); + namesXml = doc.Root; + } + } + name = namesXml.GetAttributeString("format", ""); Dictionary> entries = new Dictionary>(); - foreach (var subElement in doc.Root.Elements()) + foreach (var subElement in namesXml.Elements()) { Identifier elemName = subElement.NameAsIdentifier(); if (!entries.ContainsKey(elemName)) @@ -477,8 +534,21 @@ namespace Barotrauma // talent-relevant values public int MissionsCompletedSinceDeath = 0; + private static bool ElementHasSpecifierTags(XElement element) + => element.GetAttributeBool("specifiertags", + element.GetAttributeBool("genders", + element.GetAttributeBool("races", false))); + // Used for creating the data - public CharacterInfo(Identifier speciesName, string name = "", string originalName = "", Either jobOrJobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, Identifier npcIdentifier = default) + public CharacterInfo( + Identifier speciesName, + string name = "", + string originalName = "", + Either jobOrJobPrefab = null, + string ragdollFileName = null, + int variant = 0, + Rand.RandSync randSync = Rand.RandSync.Unsynced, + Identifier npcIdentifier = default) { JobPrefab jobPrefab = null; Job job = null; @@ -494,7 +564,7 @@ namespace Barotrauma CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement; if (CharacterConfigElement == null) { return; } // TODO: support for variants - HasSpecifierTags = CharacterConfigElement.GetAttributeBool("specifiertags", false); + HasSpecifierTags = ElementHasSpecifierTags(CharacterConfigElement); if (HasSpecifierTags) { HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); @@ -537,12 +607,7 @@ namespace Barotrauma public string GetRandomName(Rand.RandSync randSync) { - string name = ""; - var nameElement = CharacterConfigElement.GetChildElement("names"); - if (nameElement != null) - { - GetName(nameElement.GetAttributeContentPath("path") ?? ContentPath.Empty, randSync, out name); - } + GetName(randSync, out string name); return name; } @@ -623,7 +688,7 @@ namespace Barotrauma if (element == null) { return; } // TODO: support for variants CharacterConfigElement = element; - HasSpecifierTags = CharacterConfigElement.GetAttributeBool("specifiertags", false); + HasSpecifierTags = ElementHasSpecifierTags(CharacterConfigElement); if (HasSpecifierTags) { RecreateHead( @@ -647,7 +712,7 @@ namespace Barotrauma var nameElement = CharacterConfigElement.GetChildElement("names"); if (nameElement != null) { - GetName(nameElement.GetAttributeContentPath("path") ?? ContentPath.Empty, Rand.RandSync.ServerAndClient, out Name); + GetName(Rand.RandSync.ServerAndClient, out Name); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 6cec0f84c..f41b7720f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -36,7 +36,7 @@ namespace Barotrauma var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory"); var pronounsElement = ConfigElement.GetChildElement("Pronouns"); - if (headsElement != null && varsElement != null && menuCategoryElement != null && pronounsElement != null) + if (headsElement != null) { CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 68d156778..640bf791c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -380,20 +380,24 @@ namespace Barotrauma public string Tags { get { return string.Join(',', TagSet); } - private set { TagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet(); } + private set + { + TagSet = value.Split(',') + .ToIdentifiers() + .Where(id => !id.IsEmpty) + .ToImmutableHashSet(); + } } public ImmutableHashSet TagSet { get; private set; } public SoundParams(ContentXElement element, CharacterParams character) : base(element, character) { - HashSet tags = TagSet.ToHashSet(); Identifier genderFallback = element.GetAttributeIdentifier("gender", ""); if (genderFallback != Identifier.Empty && genderFallback != "None") { - tags.Add(genderFallback); + TagSet = TagSet.Add(genderFallback); } - TagSet = tags.ToImmutableHashSet(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 40c00e220..a525764bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -446,7 +446,7 @@ namespace Barotrauma #elif CLIENT return characters; #endif - static bool IsAlive(Character c) { return c.Info != null && !c.IsDead; } + static bool IsAlive(Character c) { return c?.Info != null && !c.IsDead; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 3cedefab8..cb24eb337 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -58,7 +58,7 @@ namespace Barotrauma IsEquipped = new bool[capacity]; SlotTypes = new InvSlotType[capacity]; - AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", false); + AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", character.Info != null); AccessibleByOwner = element.GetAttributeBool("accessiblebyowner", AccessibleWhenAlive); string[] slotTypeNames = ParseSlotTypes(element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 7a2cb168d..03c061552 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -370,7 +370,7 @@ namespace Barotrauma.Items.Components public Item GetFocusTarget() { - var positionOut = item.Connections.Find(c => c.Name == "position_out"); + var positionOut = item.Connections?.Find(c => c.Name == "position_out"); if (positionOut == null) { return null; } item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index ef5d693af..529490ecc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components } [Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should structures cast shadows when light from this light source hits them. " + - "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)] + "Disabling shadows increases the performance of the game, and is recommended for lights with a short range. Lights that are set to be drawn behind subs don't cast shadows, regardless of this setting.", alwaysUseInstanceValues: true)] public bool CastShadows { get { return castShadows; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index cd5e684d5..42b982c38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -407,8 +407,6 @@ namespace Barotrauma Loaded = this; Generating = true; - Rand.Tracker.Reset(); - Rand.Tracker.Active = true; EqualityCheckValues.Clear(); EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); @@ -1305,8 +1303,6 @@ namespace Barotrauma //assign an ID to make entity events work //ID = FindFreeID(); Generating = false; - Rand.Tracker.Active = false; - File.WriteAllLines(GameMain.NetworkMember is { IsServer: true } ? "serverrng.txt" : "clientrng.txt", Rand.Tracker.LogMsgs); } private List GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance) @@ -2443,7 +2439,6 @@ namespace Barotrauma fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); } } - levelResources.Sort((x, y) => x.commonness.CompareTo(y.commonness)); DebugConsole.Log("Generating level resources..."); var allValidLocations = GetAllValidClusterLocations(); @@ -2473,29 +2468,33 @@ namespace Barotrauma //place some of the least common resources in the abyss AbyssResources.Clear(); - for (int j = 0; j < levelResources.Count && j < 5; j++) - { - for (int i = 0; i < 10; i++) - { - var (itemPrefab, commonness) = levelResources[j]; - var location = allValidLocations.GetRandom(l => - { - if (l.Cell == null || l.Edge == null) { return false; } - if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } - l.InitializeResources(); - return l.Resources.Count <= GetMaxResourcesOnEdge(itemPrefab, l, out _); - }, randSync: Rand.RandSync.ServerAndClient); - if (location.Cell == null || location.Edge == null) { break; } - int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient); - PlaceResources(itemPrefab, clusterSize, location, out var abyssResources); - var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); - abyssClusterLocation.Resources.AddRange(abyssResources); - AbyssResources.Add(abyssClusterLocation); - var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); - allValidLocations.RemoveAt(locationIndex); - } - } + int abyssClusterCount = (int)MathHelper.Lerp(GenerationParams.AbyssResourceClustersMin, GenerationParams.AbyssResourceClustersMax, Difficulty / 100.0f); + + for (int i = 0; i < abyssClusterCount; i++) + { + //use inverse commonness to select the abyss resources (the rarest ones are the most common in the abyss) + var selectedPrefab = ToolBox.SelectWeightedRandom( + levelResources.Select(it => it.itemPrefab).ToList(), + levelResources.Select(it => it.commonness <= 0.0f ? 0.0f : 1.0f / it.commonness).ToList(), + Rand.RandSync.ServerAndClient); + var location = allValidLocations.GetRandom(l => + { + if (l.Cell == null || l.Edge == null) { return false; } + if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } + l.InitializeResources(); + return l.Resources.Count <= GetMaxResourcesOnEdge(selectedPrefab, l, out _); + }, randSync: Rand.RandSync.ServerAndClient); + + if (location.Cell == null || location.Edge == null) { break; } + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y + 1, Rand.RandSync.ServerAndClient); + PlaceResources(selectedPrefab, clusterSize, location, out var abyssResources); + var abyssClusterLocation = new ClusterLocation(location.Cell, location.Edge, initializeResourceList: true); + abyssClusterLocation.Resources.AddRange(abyssResources); + AbyssResources.Add(abyssClusterLocation); + var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); + allValidLocations.RemoveAt(locationIndex); + } PathPoints.Clear(); nextPathPointId = 0; @@ -2603,7 +2602,14 @@ namespace Barotrauma #if DEBUG DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" + - "Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count); + " 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"); + if (AbyssResources.Count > 0) + { + + DebugConsole.NewMessage("Abyss resources spawned: " + AbyssResources.Sum(a => a.Resources.Count) + "\n" + + " Total value: " + AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)) + " mk"); + } #endif DebugConsole.Log("Level resources generated"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 3ee01232f..d1fc85553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -387,6 +387,20 @@ namespace Barotrauma set; } + [Serialize(3, IsPropertySaveable.Yes, description: "Minimum 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 AbyssResourceClustersMin + { + get; + set; + } + + [Serialize(20, 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; + set; + } + [Serialize(-300000, IsPropertySaveable.Yes, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] public int SeaFloorDepth { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs index 55be14eb5..3e739b555 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/Rand.cs @@ -11,38 +11,6 @@ namespace Barotrauma { public static class Rand { - [Obsolete("TODO: remove")] - public static class Tracker - { - private readonly static List logMsgs = new List(); - public static IReadOnlyList LogMsgs => logMsgs; - - public static bool Active = false; - - public static void Reset() - { - logMsgs.Clear(); - Active = false; - } - - public static void RegisterCall(int stDepth=4) - { - if (!Active) { return; } - var st = new StackTrace(skipFrames: 2, fNeedFileInfo: true); - var frames = st.GetFrames(); - string msg = string.Join("; ", - frames.Take(stDepth).Select(f => - $"{Path.GetFileNameWithoutExtension(f.GetFileName())}:{f.GetFileLineNumber()}")); - logMsgs.Add(msg); - } - - public static void Log(string msg) - { - if (!Active) { return; } - logMsgs.Add(msg); - } - } - public enum RandSync { Unsynced, //not synced, used for unimportant details like minor particle properties @@ -81,8 +49,6 @@ namespace Barotrauma public static int ThreadId = 0; private static void CheckRandThreadSafety(RandSync sync) { - if (sync == RandSync.ServerAndClient) { Tracker.RegisterCall(); } - if (ThreadId != 0 && sync == RandSync.Unsynced) { if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId) diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index a24e889aa..f5663e4fd 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,24 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.2.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Adjusted abyss resource spawning: less resources per level, the number of resources is relative to the difficulty, the spawned resources aren't guaranteed to always be the 5 least common alien materials. +- Increased maximum number of lights from 300 to 600 (unstable only). +- Backwards compatibility with human mods made before modding refactor (unstable only). + +Fixes: +- Fixed server crash in GetSalaryEligibleCrew (unstable only). +- Fixed crashing in Controller.GetFocusTarget (unstable only). +- Fixed "Tried to access crew wallets in singleplayer" error at the end of the round (unstable only). +- Fixed selling items taking money instead of giving it (unstable only). +- Fixed lights that are drawn behind subs counting as shadow-casting in the sub editor. +- Fixed server host creating 2 disconnect message boxes if the server crashes. +- Deactivate certain cheats when leaving an editor. Fixes ability to use certain cheat commands in an editor (which is now allowed without having to enable cheats) and then switch back to the game (unstable only). + +AI: +- Fixed bots sometimes getting stuck to doors when they are trying to fix a hull behind it. Happened because the goto objective was completed before the bot could open the door. + --------------------------------------------------------------------------------------------------------- v0.17.1.0 --------------------------------------------------------------------------------------------------------- From 4206f6db42e47e769d5876e6f0cc3bc3677ae29d Mon Sep 17 00:00:00 2001 From: Juan Pablo Arce Date: Tue, 22 Mar 2022 14:44:12 -0300 Subject: [PATCH 4/9] Unstable 0.17.3.0 --- .../ClientSource}/CameraTransition.cs | 0 .../Characters/AI/EnemyAIController.cs | 2 +- .../Characters/CharacterNetworking.cs | 17 +- .../Characters/Health/CharacterHealth.cs | 2 +- .../ContentPackageManager.cs | 24 +- .../ClientSource/GUI/GUIPrefab.cs | 2 +- .../ClientSource/GUI/MedicalClinicUI.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 44 +++- .../GameSession/GameModes/CampaignMode.cs | 4 + .../GameModes/MultiPlayerCampaign.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../ClientSource/Items/CharacterInventory.cs | 2 +- .../Items/Components/ItemContainer.cs | 7 +- .../Items/Components/ItemLabel.cs | 51 +++- .../ClientSource/Items/Components/Rope.cs | 20 +- .../ClientSource/Items/Inventory.cs | 2 +- .../ClientSource/Items/Item.cs | 4 +- .../Map/Creatures/BallastFloraBehavior.cs | 4 +- .../ClientSource/Map/WayPoint.cs | 12 +- .../Networking/Voip/VoipCapture.cs | 13 +- .../Networking/Voip/VoipClient.cs | 12 +- .../CharacterEditor/CharacterEditorScreen.cs | 3 +- .../ClientSource/Screens/GameScreen.cs | 25 +- .../ClientSource/Screens/LevelEditorScreen.cs | 10 +- .../ClientSource/Screens/MainMenuScreen.cs | 149 +++++++----- .../ClientSource/Screens/ModDownloadScreen.cs | 9 + .../ClientSource/Screens/SubEditorScreen.cs | 109 ++++++--- .../Serialization/SerializableEntityEditor.cs | 30 ++- .../ClientSource/Steam/PublishTab.cs | 98 ++++---- .../ClientSource/Steam/Workshop.cs | 5 + .../BarotraumaClient/LinuxClient.csproj | 4 +- Barotrauma/BarotraumaClient/MacClient.csproj | 4 +- .../BarotraumaClient/WindowsClient.csproj | 4 +- .../BarotraumaServer/LinuxServer.csproj | 4 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/Character.cs | 2 +- .../Characters/CharacterNetworking.cs | 2 +- .../ServerSource/GameSession/CargoManager.cs | 2 +- .../GameModes/MultiPlayerCampaign.cs | 8 +- .../ServerSource/Items/Item.cs | 3 +- .../Map/Creatures/BallastFloraBehavior.cs | 32 ++- .../Networking/FileTransfer/ModSender.cs | 6 +- .../ServerSource/Networking/GameServer.cs | 2 +- .../ServerSource/Networking/ServerSettings.cs | 7 +- .../ServerSource/Traitors/Traitor.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 4 +- .../Characters/AI/EnemyAIController.cs | 225 ++++++++++-------- .../SharedSource/Characters/AI/LatchOntoAI.cs | 10 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/FishAnimController.cs | 19 +- .../Animation/HumanoidAnimController.cs | 74 ++++-- .../Characters/Animation/Ragdoll.cs | 2 +- .../SharedSource/Characters/Attack.cs | 13 +- .../SharedSource/Characters/Character.cs | 32 ++- .../Characters/CharacterEventData.cs | 2 +- .../SharedSource/Characters/CharacterInfo.cs | 24 +- .../Characters/Health/CharacterHealth.cs | 16 +- .../SharedSource/Characters/Limb.cs | 2 + .../Params/Animation/HumanoidAnimations.cs | 10 +- .../Characters/Params/CharacterParams.cs | 6 + .../ContentFile/ContentFile.cs | 10 +- .../ContentFile/ServerExecutableFile.cs | 28 +++ .../ContentPackage/ContentPackage.cs | 2 +- .../ContentPackage/CorePackage.cs | 4 +- .../ContentPackageManager.cs | 3 +- .../SharedSource/DebugConsole.cs | 57 +++-- .../Events/Missions/BeaconMission.cs | 7 + .../Events/Missions/CargoMission.cs | 9 +- .../SharedSource/Events/Missions/Mission.cs | 33 +-- .../Events/Missions/MonsterMission.cs | 2 +- .../SharedSource/GameSession/CargoManager.cs | 14 +- .../SharedSource/GameSession/Data/Wallet.cs | 18 +- .../GameSession/GameModes/CampaignMode.cs | 23 +- .../SharedSource/GameSession/GameSession.cs | 17 +- .../GameSession/UpgradeManager.cs | 2 +- .../Items/Components/DockingPort.cs | 2 +- .../Items/Components/ItemContainer.cs | 9 + .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Power/Powered.cs | 31 +-- .../Items/Components/Projectile.cs | 79 +++--- .../SharedSource/Items/Components/Rope.cs | 62 ++++- .../SharedSource/Items/Item.cs | 7 +- .../SharedSource/Items/ItemEventData.cs | 2 +- .../Map/Creatures/BallastFloraBehavior.cs | 57 +++-- .../SharedSource/Map/ItemAssemblyPrefab.cs | 17 +- .../SharedSource/Map/Levels/Level.cs | 4 +- .../SharedSource/Map/Levels/LevelData.cs | 14 +- .../SharedSource/Map/Map/Map.cs | 3 +- .../Map/Outposts/OutpostGenerator.cs | 166 +++++++++---- .../Map/Outposts/OutpostModuleInfo.cs | 3 + .../SharedSource/Map/RoundEndCinematic.cs | 4 +- .../SharedSource/Map/StructurePrefab.cs | 2 +- .../SharedSource/Networking/NetworkMember.cs | 2 +- .../Serialization/XMLExtensions.cs | 8 +- .../SharedSource/Settings/GameSettings.cs | 25 +- .../StatusEffects/StatusEffect.cs | 24 +- .../SharedSource/Utils/SafeIO.cs | 26 +- Barotrauma/BarotraumaShared/changelog.txt | 59 +++++ .../LICENSE_webm_mem_playback.txt | 2 +- README.md | 2 +- 100 files changed, 1380 insertions(+), 655 deletions(-) rename Barotrauma/{BarotraumaShared/SharedSource => BarotraumaClient/ClientSource}/CameraTransition.cs (100%) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs rename to Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 9b31bf9bf..bfb8b7202 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -39,7 +39,7 @@ namespace Barotrauma targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); - if (wallTarget != null) + if (wallTarget != null && !IsCoolDownRunning) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 22d741e27..28c3230ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -152,7 +152,7 @@ namespace Barotrauma case TreatmentEventData _: msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; - case StatusEventData _: + case CharacterStatusEventData _: //do nothing break; case UpdateTalentsEventData _: @@ -343,8 +343,12 @@ namespace Barotrauma if (controlled == this) { Controlled = null; - IsRemotePlayer = ownerID > 0; } + if (GameMain.Client?.Character == this) + { + GameMain.Client.Character = null; + } + IsRemotePlayer = ownerID > 0; } break; case EventType.Status: @@ -371,7 +375,9 @@ namespace Barotrauma if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; @@ -380,13 +386,16 @@ namespace Barotrauma if (targetEntity == null && eventType == EventType.SetAttackTarget) { DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetNotFound", GameAnalyticsManager.ErrorSeverity.Error, "Received invalid SetAttackTarget message. Target entity not found."); break; } - if (targetEntity is Character targetCharacter) + if (targetEntity is Character targetCharacter && targetLimbIndex != 255) { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + string errorMsgWithoutName = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.SpeciesName}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"; + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsgWithoutName); break; } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index b61c76669..436e47141 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -400,7 +400,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.StatusEventData()); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.CharacterStatusEventData()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 3ae094982..86a971bf5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -12,22 +12,30 @@ namespace Barotrauma { public sealed partial class PackageSource : ICollection { - public ContentPackage SaveAndEnableRegularMod(ModProject modProject) + public string SaveRegularMod(ModProject modProject) { if (modProject.IsCore) { throw new ArgumentException("ModProject must not be a core package"); } - - //save the content package + string fileListPath = Path.Combine(directory, ToolBox.RemoveInvalidFileNameChars(modProject.Name), ContentPackage.FileListFileName) .CleanUpPathCrossPlatform(correctFilenameCase: false); - Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)!); modProject.Save(fileListPath); Refresh(); EnabledPackages.DisableRemovedMods(); - var newPackage = Regular.First(p => p.Path == fileListPath); - //enable it - EnabledPackages.EnableRegular(newPackage); + return fileListPath; + } - return newPackage; + public RegularPackage GetRegularModByPath(string fileListPath) + { + return Regular.First(p => p.Path == fileListPath); + } + + public RegularPackage SaveAndEnableRegularMod(ModProject modProject) + { + string fileListPath = SaveRegularMod(modProject); + var package = GetRegularModByPath(fileListPath); + EnabledPackages.EnableRegular(package); + + return package; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index ccef64092..1487d6943 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -65,7 +65,7 @@ namespace Barotrauma LoadFont(); } - private void LoadFont() + public void LoadFont() { string fontPath = GetFontFilePath(element); uint size = GetFontSize(element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index ed32a00b9..1ea2828e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -931,7 +931,7 @@ namespace Barotrauma }); } - private static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) + 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/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 201d8d242..62b0364d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -218,7 +218,7 @@ namespace Barotrauma public void AddToGUIUpdateList() { - infoFrame?.AddToGUIUpdateList(); + infoFrame?.AddToGUIUpdateList(order: 1); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); } @@ -379,8 +379,7 @@ namespace Barotrauma private void CreateCrewListFrame(GUIFrame crewFrame) { - // FIXME remove TestScreen stuff - crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new []{ TestScreen.dummyCharacter }; + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? Array.Empty(); teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -817,7 +816,7 @@ namespace Barotrauma else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); - if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewClient(client, preview); } + GameMain.Client?.SelectCrewClient(client, preview); CreateWalletFrame(background, client.Character); } @@ -850,17 +849,18 @@ namespace Barotrauma 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), TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()), textAlignment: Alignment.BottomRight); + GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight); 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) { - Range = Vector2.UnitY, + ToolTip = TextManager.Get("crewwallet.salary.tooltip"), + Range = new Vector2(0, 1), BarScrollValue = targetWallet.RewardDistribution / 100f, Step = 0.01f, BarSize = 0.1f, OnMoved = (bar, scroll) => { - rewardBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()); + SetRewardText((int)(scroll * 100), rewardBlock); return true; }, OnReleased = (bar, scroll) => @@ -871,6 +871,9 @@ namespace Barotrauma return true; } }; + + SetRewardText(targetWallet.RewardDistribution, rewardBlock); + // @formatter:off GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter)) { @@ -902,8 +905,11 @@ namespace Barotrauma GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; // @formatter:on + ImmutableArray layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout); + MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups); transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical") { + ToolTip = TextManager.Get("crewwallet.transfer.tooltip"), OnClicked = (button, o) => { isTransferMenuOpen = !isTransferMenuOpen; @@ -951,6 +957,8 @@ namespace Barotrauma break; } + MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); + if (!hasPermissions) { centerButton.Enabled = centerButton.CanBeFocused = false; @@ -1073,14 +1081,14 @@ namespace Barotrauma Receiver = to.Select(option => option.ID), Amount = amount }; - IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.MONEY); + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.TRANSFER_MONEY); transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } static void SetRewardDistribution(Character character, int newValue) { - INetSerializableStruct transfer = new NetWalletSalaryUpdate + INetSerializableStruct transfer = new NetWalletSetSalaryUpdate { Target = character.ID, NewRewardDistribution = newValue @@ -1090,7 +1098,23 @@ namespace Barotrauma GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - string GetSharePercentage() => Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage.ToString(); + 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 7186c3bd9..824d1c178 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -63,6 +63,10 @@ namespace Barotrauma } } + /// + /// Gets the current personal wallet + /// In singleplayer this is the campaign bank and in multiplayer this is the personal wallet + /// public virtual Wallet Wallet => GetWallet(); public override void ShowStartMessage() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 597939e79..2cf62767f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -914,7 +914,7 @@ namespace Barotrauma WalletInfo info = transaction.Info; switch (transaction.CharacterID) { - case Some { Value: var charID}: + case Some { Value: var charID }: { Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); if (targetCharacter is null) { break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 2770fc3c0..abe401df3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -318,7 +318,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew().Where(c => c != controlled), Option.Some(reward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index e19ace1a9..4746f0e9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -966,7 +966,7 @@ namespace Barotrauma } else if (character.HeldItems.Any(i => i.OwnInventory != null && - (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + ((i.OwnInventory.CanBePut(item) && allowInventorySwap) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 597c90daa..5fe3adde1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,8 +1,7 @@ -using System; -using System.Linq; -using System.Xml.Linq; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 7c7821844..01742a2d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -2,12 +2,14 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class ItemLabel : ItemComponent, IDrawableComponent + partial class ItemLabel : ItemComponent, IDrawableComponent, IHasExtraTextPickerEntries { private GUITextBlock textBlock; @@ -106,7 +108,7 @@ namespace Barotrauma.Items.Components set { scrollable = value; - IsActive = value; + IsActive = value || parseSpecialTextTagOnStart; TextBlock.Wrap = !scrollable; TextBlock.TextAlignment = scrollable ? Alignment.CenterLeft : Alignment.Center; } @@ -136,6 +138,11 @@ namespace Barotrauma.Items.Components { } + public IEnumerable GetExtraTextPickerEntries() + { + return SpecialTextTags; + } + private void SetScrollingText() { if (!scrollable) { return; } @@ -174,9 +181,18 @@ namespace Barotrauma.Items.Components scrollIndex = MathHelper.Clamp(scrollIndex, 0, DisplayText.Length); } + private static readonly string[] SpecialTextTags = new string[] { "[CurrentLocationName]", "[CurrentBiomeName]", "[CurrentSubName]" }; + private bool parseSpecialTextTagOnStart; private void SetDisplayText(string value) { + if (SpecialTextTags.Contains(value)) + { + parseSpecialTextTagOnStart = true; + IsActive = true; + } + DisplayText = IgnoreLocalization ? value : TextManager.Get(value).Fallback(value); + TextBlock.Text = DisplayText; if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) { @@ -198,9 +214,37 @@ namespace Barotrauma.Items.Components }; } + private void ParseSpecialTextTag() + { + switch (text) + { + case "[CurrentLocationName]": + SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); + break; + case "[CurrentBiomeName]": + SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); + break; + case "[CurrentSubName]": + SetDisplayText(item.Submarine?.Info?.DisplayName.Value ?? string.Empty); + break; + default: + break; + } + } + public override void Update(float deltaTime, Camera cam) { - if (!scrollable) { return; } + if (parseSpecialTextTagOnStart) + { + ParseSpecialTextTag(); + parseSpecialTextTagOnStart = false; + } + + if (!scrollable) + { + IsActive = false; + return; + } if (scrollingText == null) { @@ -286,5 +330,6 @@ namespace Barotrauma.Items.Components { Text = msg.ReadString(); } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 2dc8ec191..36713eab5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -34,6 +34,13 @@ namespace Barotrauma.Items.Components [Serialize("0.5,0.5)", IsPropertySaveable.No)] public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); + [Serialize(true, IsPropertySaveable.No, description: "")] + public bool BreakFromMiddle + { + get; + set; + } + public Vector2 DrawSize { get @@ -124,9 +131,14 @@ namespace Barotrauma.Items.Components int width = (int)(SpriteWidth * snapState); if (width > 0.0f) - { - DrawRope(spriteBatch, endPos - diff * snapState * 0.5f, endPos, width); - DrawRope(spriteBatch, startPos, startPos + diff * snapState * 0.5f, width); + { + float positionMultiplier = snapState; + if (BreakFromMiddle) + { + positionMultiplier /= 2; + DrawRope(spriteBatch, endPos - diff * positionMultiplier, endPos, width); + } + DrawRope(spriteBatch, startPos, startPos + diff * positionMultiplier, width); } } else @@ -143,7 +155,7 @@ namespace Barotrauma.Items.Components float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); } - if (endSprite != null) + if (endSprite != null && (!Snapped || BreakFromMiddle)) { float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 1c27aaf3c..6224c628c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1846,7 +1846,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private void ApplyReceivedState() + public void ApplyReceivedState() { if (receivedItemIDs == null || (Owner != null && Owner.Removed)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 8cb513864..3cc3610e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1198,9 +1198,9 @@ namespace Barotrauma Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { - if (ic is Repairable) + if (ic is Repairable r) { - if (!IsFullCondition) { color = Color.Cyan; } + if (r.IsBelowRepairThreshold) { color = Color.Cyan; } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 80d814e5f..264b5b2e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -370,6 +370,7 @@ namespace Barotrauma.MapCreatures.Behavior private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); + bool isRootGrowth = msg.ReadBoolean(); byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); @@ -385,7 +386,8 @@ namespace Barotrauma.MapCreatures.Behavior { ID = id, MaxHealth = maxHealth, - Sides = (TileSide) sides + Sides = (TileSide) sides, + IsRootGrowth = isRootGrowth }; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 8cd2acaaf..e69d0bbb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -272,11 +272,7 @@ namespace Barotrauma private GUIComponent CreateEditingHUD() { - int width = 500; - int height = spawnType == SpawnType.Path ? 80 : 200; - int x = GameMain.GraphicsWidth / 2 - width / 2, y = 30; - - editingHUD = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas) { ScreenSpaceOffset = new Point(x, y) }) + editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; @@ -284,7 +280,7 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) { Stretch = true, - RelativeSpacing = 0.05f + AbsoluteSpacing = (int)(GUI.Scale * 5) }; if (spawnType == SpawnType.Path) @@ -418,6 +414,10 @@ namespace Barotrauma }; } + editingHUD.RectTransform.Resize(new Point( + editingHUD.Rect.Width, + (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y))); + PositionEditingHUD(); return editingHUD; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 98073f188..cf2caabed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -163,16 +163,11 @@ namespace Barotrauma.Networking public static void ChangeCaptureDevice(string deviceName) { - var config = GameSettings.CurrentConfig; - config.Audio.VoiceCaptureDevice = deviceName; - GameSettings.SetCurrentConfig(config); + if (Instance == null) { return; } - if (Instance != null) - { - UInt16 storedBufferID = Instance.LatestBufferID; - Instance.Dispose(); - Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); - } + UInt16 storedBufferID = Instance.LatestBufferID; + Instance.Dispose(); + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } IntPtr nativeBuffer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 85570fcbd..8916fc81e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -54,7 +54,17 @@ namespace Barotrauma.Networking } else { - if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + try + { + if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"VoipCature.Create failed: {e.Message} {e.StackTrace.CleanupStackTrace()}"); + var config = GameSettings.CurrentConfig; + config.Audio.VoiceSetting = VoiceMode.Disabled; + GameSettings.SetCurrentConfig(config); + } if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 56032d102..e9de25b54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2805,7 +2805,8 @@ namespace Barotrauma.CharacterEditor return false; } #endif - if (!character.IsHuman && !string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) + ContentPath texturePath = ContentPath.FromRaw(character.Prefab.ContentPackage, RagdollParams.Texture); + if (!character.IsHuman && (texturePath.IsNullOrWhiteSpace() || !File.Exists(texturePath.Value))) { DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}"); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index e58af763e..2b01b3ff2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -248,17 +248,24 @@ namespace Barotrauma } spriteBatch.End(); - //draw characters with deformable limbs last, because they can't be batched into SpriteBatch - //pretty hacky way of preventing draw order issues between normal and deformable sprites - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) - for (int i = Character.CharacterList.Count - 1; i >= 0; i--) + DrawDeformed(firstPass: true); + DrawDeformed(firstPass: false); + + void DrawDeformed(bool firstPass) { - Character c = Character.CharacterList[i]; - if (!c.IsVisible || c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } - c.Draw(spriteBatch, Cam); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) + for (int i = Character.CharacterList.Count - 1; i >= 0; i--) + { + Character c = Character.CharacterList[i]; + if (!c.IsVisible) { continue; } + if (c.Params.DrawLast == firstPass) { continue; } + if (c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } + c.Draw(spriteBatch, Cam); + } + spriteBatch.End(); } - spriteBatch.End(); + Level.Loaded?.DrawFront(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 470977caf..a1bbea8ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -32,7 +32,7 @@ namespace Barotrauma private readonly GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled, mirrorLevel; + private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; private Sprite editingSprite; @@ -126,6 +126,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { SerializeAll(); + GUI.AddMessage(TextManager.Get("leveleditor.allsaved"), GUIStyle.Green); return true; } }; @@ -169,6 +170,12 @@ namespace Barotrauma 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), + TextManager.Get("leveleditor.allowinvalidoutpost")) + { + ToolTip = TextManager.Get("leveleditor.allowinvalidoutpost.tooltip") + }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -179,6 +186,7 @@ namespace Barotrauma 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); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0ff737f3c..5f574156c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,6 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; + private GUIDropDown serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -557,6 +558,35 @@ namespace Barotrauma GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); return true; } + + serverExecutableDropdown.ListBox.Content.Children.ToArray() + .Where(c => c.UserData is ServerExecutableFile f && !ContentPackageManager.EnabledPackages.All.Contains(f.ContentPackage)) + .ForEach(serverExecutableDropdown.ListBox.RemoveChild); + var newServerExes + = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()) + .Where(f => serverExecutableDropdown.ListBox.Content.Children.None(c => c.UserData == f)) + .ToArray(); + foreach (var newServerExe in newServerExes) + { + serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + } + serverExecutableDropdown.ListBox.Content.Children.ForEach(c => + { + c.RectTransform.RelativeSize = (1.0f, c.RectTransform.RelativeSize.Y); + c.ForceLayoutRecalculation(); + }); + bool serverExePickable = serverExecutableDropdown.ListBox.Content.CountChildren > 1; + serverExecutableDropdown.Parent.Visible + = serverExePickable; + serverExecutableDropdown.Parent.RectTransform.RelativeSize + = (1.0f, serverExePickable ? 0.1f : 0.0f); + serverExecutableDropdown.Parent.ForceLayoutRecalculation(); + (serverExecutableDropdown.Parent.Parent as GUILayoutGroup)?.Recalculate(); + if (serverExecutableDropdown.SelectedComponent is null) + { + serverExecutableDropdown.Select(0); + } + break; case Tab.Tutorials: if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) @@ -784,7 +814,7 @@ namespace Barotrauma GameMain.ResetNetLobbyScreen(); try { - string exeName = "DedicatedServer.exe"; + string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -814,15 +844,20 @@ namespace Barotrauma arguments += " -ownerkey " + ownerKey; } - string filename = exeName; -#if LINUX || OSX - filename = "./" + Path.GetFileNameWithoutExtension(exeName); - //arguments = ToolBox.EscapeCharacters(arguments); + string filename = Path.Combine( + Path.GetDirectoryName(exeName), + Path.GetFileNameWithoutExtension(exeName)); +#if WINDOWS + filename += ".exe"; +#else + filename = "./" + exeName; #endif + var processInfo = new ProcessStartInfo { FileName = filename, Arguments = arguments, + WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG CreateNoWindow = true, UseShellExecute = false, @@ -1184,12 +1219,12 @@ namespace Barotrauma label.RectTransform.MaxSize = serverNameBox.RectTransform.MaxSize; var maxPlayersLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("MaxPlayers"), textAlignment: textAlignment); - var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true) + var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.1f }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, OnClicked = ChangeMaxPlayers @@ -1209,7 +1244,7 @@ namespace Barotrauma currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers, 1, NetConfig.MaxPlayers); maxPlayersBox.Text = currMaxPlayers.ToString(); }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, OnClicked = ChangeMaxPlayers @@ -1223,6 +1258,41 @@ namespace Barotrauma }; label.RectTransform.MaxSize = passwordBox.RectTransform.MaxSize; + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("ServerExecutable"), textAlignment: textAlignment); + const string vanillaServerOption = "Vanilla"; + serverExecutableDropdown + = new GUIDropDown(new RectTransform(textFieldSize, serverExecutableLabel.RectTransform, Anchor.CenterRight), + vanillaServerOption); + var listBoxSize = serverExecutableDropdown.ListBox.RectTransform.RelativeSize; + serverExecutableDropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.5f, listBoxSize.Y); + serverExecutableDropdown.AddItem(vanillaServerOption, userData: null); + serverExecutableDropdown.OnSelected = (selected, userData) => + { + if (userData != null) + { + var warningBox = new GUIMessageBox(headerText: TextManager.Get("Warning"), + text: TextManager.GetWithVariable("ModServerExesAtYourOwnRisk", "[exename]", serverExecutableDropdown.Text), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + warningBox.Buttons[0].OnClicked = (_, __) => + { + warningBox.Close(); + return false; + }; + warningBox.Buttons[1].OnClicked = (_, __) => + { + serverExecutableDropdown.Select(0); + warningBox.Close(); + return false; + }; + } + + serverExecutableDropdown.Text = ToolBox.LimitString(serverExecutableDropdown.Text, + serverExecutableDropdown.Font, serverExecutableDropdown.Rect.Width * 8 / 10); + + return true; + }; + // tickbox upper --------------- var tickboxAreaUpper = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, tickBoxSize.Y), parent.RectTransform), isHorizontal: true); @@ -1312,8 +1382,8 @@ namespace Barotrauma { var client = new RestClient(RemoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); - client.ExecuteAsync(request, RemoteContentReceived); - CoroutineManager.StartCoroutine(WairForRemoteContentReceived()); + TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), + RemoteContentReceived); } catch (Exception e) @@ -1327,58 +1397,31 @@ namespace Barotrauma } } - private IEnumerable WairForRemoteContentReceived() + private void RemoteContentReceived(Task t) { - while (true) + try { - lock (remoteContentLock) + if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + string xml = remoteContentResponse.Content; + int index = xml.IndexOf('<'); + if (index > 0) { xml = xml.Substring(index, xml.Length - index); } + if (!string.IsNullOrWhiteSpace(xml)) { - if (remoteContentResponse != null) { break; } - } - yield return new WaitForSeconds(0.1f); - } - lock (remoteContentLock) - { - if (remoteContentResponse.ResponseStatus != ResponseStatus.Completed || remoteContentResponse.StatusCode != HttpStatusCode.OK) - { - yield return CoroutineStatus.Success; - } - - try - { - string xml = remoteContentResponse.Content; - int index = xml.IndexOf('<'); - if (index > 0) { xml = xml.Substring(index, xml.Length - index); } - if (!string.IsNullOrWhiteSpace(xml)) + remoteContentDoc = XDocument.Parse(xml); + foreach (var subElement in remoteContentDoc?.Root.Elements()) { - remoteContentDoc = XDocument.Parse(xml); - foreach (var subElement in remoteContentDoc?.Root.Elements()) - { - GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); - } + 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.WairForRemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, - "Reading received remote main menu content failed. " + e.Message); - } } - yield return CoroutineStatus.Success; - } - private readonly object remoteContentLock = new object(); - private IRestResponse remoteContentResponse; - - private void RemoteContentReceived(IRestResponse response, RestRequestAsyncHandle handle) - { - lock (remoteContentLock) + catch (Exception e) { - remoteContentResponse = response; +#if DEBUG + DebugConsole.ThrowError("Reading received remote main menu content failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.RemoteContentReceived: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 478571b01..71646a819 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -29,10 +29,19 @@ namespace Barotrauma currentDownload = null; confirmDownload = false; } + + private void DeletePrevDownloads() + { + if (Directory.Exists(ModReceiver.DownloadFolder)) + { + Directory.Delete(ModReceiver.DownloadFolder, recursive: true); + } + } public override void Select() { base.Select(); + DeletePrevDownloads(); Reset(); Frame.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 2e55fe64b..886cf512b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -9,11 +9,7 @@ using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; -#if DEBUG -using System.IO; -#else using Barotrauma.IO; -#endif namespace Barotrauma { @@ -1262,7 +1258,9 @@ namespace Barotrauma } textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - if (ep.Category == MapEntityCategory.ItemAssembly) + if (ep.Category == MapEntityCategory.ItemAssembly + && ep.ContentPackage?.Files.Length == 1 + && ContentPackageManager.LocalPackages.Contains(ep.ContentPackage)) { var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, TextManager.Get("Delete"), style: "GUIButtonSmall") @@ -2225,9 +2223,7 @@ namespace Barotrauma // gap positions --------------------- var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), TextManager.Get("outpostmodulegappositions"), textAlignment: Alignment.CenterLeft); - var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); @@ -2271,6 +2267,49 @@ namespace Barotrauma }; gapPositionGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), TextManager.Get("canattachtoprevious"), textAlignment: Alignment.CenterLeft) + { + ToolTip = TextManager.Get("canattachtoprevious.tooltip") + }; + var canAttachToPrevDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), + text: "", selectMultiple: true); + if (outpostModuleInfo != null) + { + foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + { + if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + canAttachToPrevDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); + if (outpostModuleInfo.CanAttachToPrevious.HasFlag((OutpostModuleInfo.GapPosition)gapPos)) + { + canAttachToPrevDropDown.SelectItem(gapPos); + } + } + } + + canAttachToPrevDropDown.OnSelected += (_, __) => + { + if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } + Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious = OutpostModuleInfo.GapPosition.None; + if (canAttachToPrevDropDown.SelectedDataMultiple.Any()) + { + List gapPosTexts = new List(); + foreach (OutpostModuleInfo.GapPosition gapPos in canAttachToPrevDropDown.SelectedDataMultiple) + { + Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious |= gapPos; + gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString()).Value); + } + canAttachToPrevDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); + } + else + { + canAttachToPrevDropDown.Text = ToolBox.LimitString("None", canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); + } + return true; + }; + canAttachToPrevGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + // ------------------- var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) @@ -2583,7 +2622,18 @@ namespace Barotrauma //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) if (contentPack.Files.All(f => f is SubmarineFile)) { continue; } - if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } + + if (!contentPacks.Contains(contentPack.Name)) + { + string altName = contentPack.AltNames.FirstOrDefault(n => contentPacks.Contains(n)); + if (!string.IsNullOrEmpty(altName)) + { + MainSub.Info.RequiredContentPackages.Remove(altName); + MainSub.Info.RequiredContentPackages.Add(contentPack.Name); + contentPacks.Remove(altName); + } + contentPacks.Add(contentPack.Name); + } } foreach (string contentPackageName in contentPacks) @@ -2749,11 +2799,7 @@ namespace Barotrauma } bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; -#if DEBUG - string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; -#else string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); -#endif string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); if (File.Exists(filePath)) { @@ -2782,26 +2828,26 @@ namespace Barotrauma void Save() { - XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); -#if DEBUG - doc.Save(filePath); -#else - doc.SaveSafe(filePath); -#endif - ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); + ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.Regular.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); if (existingContentPackage == null) { //content package doesn't exist, create one ModProject modProject = new ModProject() { Name = nameBox.Text }; - var newFile = ModProject.File.FromPath(filePath); + var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); modProject.AddFile(newFile); - ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); - } - else - { - EnqueueForReload(existingContentPackage); + string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); + existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); + doc.SaveSafe(filePath); + + var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; + if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) + { + ContentPackageManager.EnabledPackages.EnableRegular(resultPackage); + } + UpdateEntityList(); } @@ -2884,11 +2930,7 @@ namespace Barotrauma { if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { -#if DEBUG - deleteBtn.Enabled = true; -#else - deleteBtn.Enabled = userData is SubmarineInfo subInfo && !subInfo.IsVanillaSubmarine(); -#endif + deleteBtn.Enabled = userData is SubmarineInfo subInfo && GetContentPackageIntrinsicallyTiedToSub(subInfo) != null; } return true; } @@ -3141,7 +3183,7 @@ namespace Barotrauma ReconstructLayers(); } - private RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) + private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) { foreach (RegularPackage regularPackage in ContentPackageManager.RegularPackages) { @@ -3171,10 +3213,11 @@ namespace Barotrauma { try { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), true); + Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); sub.Dispose(); - File.Delete(sub.FilePath); SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index f8186ff57..af4eba036 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -390,13 +390,13 @@ namespace Barotrauma } GUIComponent propertyField = null; - if (value is bool) + if (value is bool boolVal) { - propertyField = CreateBoolField(entity, property, (bool)value, displayName, toolTip); + propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); } - else if (value is string) + else if (value is string stringVal) { - propertyField = CreateStringField(entity, property, (string)value, displayName, toolTip); + propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip); } else if (value.GetType().IsEnum) { @@ -1277,7 +1277,7 @@ namespace Barotrauma public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { - var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -1307,6 +1307,18 @@ namespace Barotrauma UserData = tagTextPair.Key.ToString() }; } + + if (entity is IHasExtraTextPickerEntries hasExtraTextPickerEntries) + { + foreach (string extraEntry in hasExtraTextPickerEntries.GetExtraTextPickerEntries()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, + ToolBox.LimitString(extraEntry, GUIStyle.Font, textList.Content.Rect.Width), GUIStyle.Green) + { + UserData = extraEntry + }; + } + } } private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) @@ -1445,4 +1457,12 @@ namespace Barotrauma } } } + + /// + /// Implement this interface to insert extra entires to the text pickers created for the SerializableEntityEditors of the entity + /// + interface IHasExtraTextPickerEntries + { + public IEnumerable GetExtraTextPickerEntries(); + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs index 45fa95f50..978e6d971 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -89,23 +89,22 @@ namespace Barotrauma.Steam return (fileCount, byteCount); } + + private void DeselectPublishedItem() + { + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + deselectAction?.Invoke(); + SelectTab(Tab.Publish); + } private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); ContentPackageManager.WorkshopPackages.Refresh(); - var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); - Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } - ? action - : null; - - void deselectItem() - { - deselectAction?.Invoke(); - SelectTab(Tab.Publish); - } - parentFrame.ClearChildren(); GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform), childAnchor: Anchor.TopCenter); @@ -146,7 +145,7 @@ namespace Barotrauma.Steam { OnClicked = (button, o) => { - deselectItem(); + DeselectPublishedItem(); return false; } }; @@ -334,7 +333,7 @@ namespace Barotrauma.Steam t => { confirmDeletion.Close(); - deselectItem(); + DeselectPublishedItem(); }); return false; }; @@ -364,24 +363,10 @@ namespace Barotrauma.Steam return false; }; - var coroutineEval = subcoroutine(messageBox.Text, messageBox); + var coroutineEval = subcoroutine(messageBox.Text, messageBox).GetEnumerator(); while (true) { - bool moveNext = true; - try - { - moveNext = coroutineEval.GetEnumerator().MoveNext(); - } - catch (Exception e) - { - DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); - messageBox.Close(); - } - if (!moveNext) - { - messageBox.Close(); - } - var status = coroutineEval.GetEnumerator().Current; + var status = coroutineEval.Current; if (messageBox.Closed) { yield return CoroutineStatus.Success; @@ -397,6 +382,20 @@ namespace Barotrauma.Steam { yield return status; } + bool moveNext = true; + try + { + moveNext = coroutineEval.MoveNext(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); + messageBox.Close(); + } + if (!moveNext) + { + messageBox.Close(); + } } } @@ -408,26 +407,9 @@ namespace Barotrauma.Steam { if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) { - //Must download! - while (!SteamManager.Workshop.CanBeInstalled(workshopItem)) - { - bool shouldForceInstall = workshopItem.IsInstalled - && Directory.Exists(workshopItem.Directory) - && !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem); - shouldForceInstall |= workshopItem is - { IsDownloading: false, IsDownloadPending: false, IsInstalled: false }; - if (shouldForceInstall) - { - SteamManager.Workshop.ForceRedownload(workshopItem); - } - currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); - yield return new WaitForSeconds(0.5f); - } - } - else - { - SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); + SteamManager.Workshop.NukeDownload(workshopItem); } + SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); TaskPool.Add($"Install {workshopItem.Title}", SteamManager.Workshop.WaitForInstall(workshopItem), (t) => @@ -436,7 +418,9 @@ namespace Barotrauma.Steam }); while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) { - currentStepText.Text = TextManager.Get("PublishPopupInstall"); + currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem) + ? TextManager.Get("PublishPopupInstall") + : TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } @@ -457,7 +441,6 @@ namespace Barotrauma.Steam currentStepText.Text = TextManager.Get("PublishPopupCreateLocal"); yield return new WaitForSeconds(0.5f); } - PopulatePublishTab(workshopItem, parentFrame); yield return CoroutineStatus.Success; @@ -500,7 +483,10 @@ namespace Barotrauma.Steam editor.SubmitAsync(), t => { - t.TryGetResult(out result); + if (t.TryGetResult(out Steamworks.Ugc.PublishResult publishResult)) + { + result = publishResult; + } resultException = t.Exception?.GetInnermost(); }); currentStepText.Text = TextManager.Get("PublishPopupSubmit"); @@ -523,6 +509,14 @@ namespace Barotrauma.Steam $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); } + ContentPackage? pkgToNuke + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); + if (pkgToNuke != null) + { + Directory.Delete(pkgToNuke.Dir, recursive: true); + ContentPackageManager.WorkshopPackages.Refresh(); + } + bool installed = false; TaskPool.Add( "InstallNewlyPublished", @@ -541,9 +535,11 @@ namespace Barotrauma.Steam { SteamWorkshopId = resultId }; + localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); ContentPackageManager.ReloadContentPackage(localPackage); ContentPackageManager.WorkshopPackages.Refresh(); + DeselectPublishedItem(); if (result.Value.NeedsWorkshopAgreement) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index fe499a4d9..f865ca3f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -207,6 +207,11 @@ namespace Barotrauma.Steam } string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; + if (File.Exists(newPath) || Directory.Exists(newPath)) + { + newPath += $"_{contentPackage.SteamWorkshopId}"; + } + if (File.Exists(newPath) || Directory.Exists(newPath)) { throw new Exception($"{newPath} already exists"); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 80a620bcc..6a04f3da2 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.3.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 20a82c153..6d3ace4d5 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.3.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 35a96ffc0..dc69d29ec 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.3.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c599af014..cf1901a55 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.3.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 036a8d02c..4546b9a91 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.3.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 49405004e..2e5acdab7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -5,7 +5,7 @@ namespace Barotrauma { partial class Character { - public static Character Controlled = null; + public static Character Controlled => null; partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7d8e7791e..10e4452f9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -424,7 +424,7 @@ namespace Barotrauma msg.Write(owner == c && owner.Character == this); msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; - case StatusEventData _: + case CharacterStatusEventData _: WriteStatus(msg); break; case UpdateSkillsEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 00190b2fd..b84aa42af 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -21,7 +21,7 @@ namespace Barotrauma } } - public void BuyBackSoldItems(List itemsToBuy, Client client) + public void BuyBackSoldItems(List itemsToBuy) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index fbd7d030e..d64ecfb71 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -859,19 +859,19 @@ namespace Barotrauma { // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all // sold items that are removed so they should be discarded on the next message - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems), sender); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems)); CargoManager.SellItems(soldItems, sender); } else if (allowedToSellInventoryItems || allowedToSellSubItems) { if (allowedToSellInventoryItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character)), sender); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character))); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); } else { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine)), sender); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine))); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); } CargoManager.SellItems(soldItems, sender); @@ -960,7 +960,7 @@ namespace Barotrauma public void ServerReadRewardDistribution(IReadMessage msg, Client sender) { - NetWalletSalaryUpdate update = INetSerializableStruct.Read(msg); + NetWalletSetSalaryUpdate update = INetSerializableStruct.Read(msg); if (!AllowedToManageCampaign(sender)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 8f1783707..f7db7648e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -56,7 +56,6 @@ namespace Barotrauma if (containerIndex < 0) { throw error($"container index out of range ({containerIndex})"); - break; } if (!(components[containerIndex] is ItemContainer itemContainer)) { @@ -66,7 +65,7 @@ namespace Barotrauma msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case StatusEventData _: + case ItemStatusEventData _: msg.Write(condition); break; case AssignCampaignInteractionEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 0707bd526..9645311ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -1,12 +1,13 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using System; -using System.Xml.Linq; namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { + const float DamageUpdateInterval = 1.0f; + private float damageUpdateTimer; partial void LoadPrefab(ContentXElement element) @@ -31,16 +32,38 @@ namespace Barotrauma.MapCreatures.Behavior } } + partial void UpdateDamage(float deltaTime) + { + damageUpdateTimer -= deltaTime; + if (damageUpdateTimer > 0.0f) { return; } + + const int maxMessagesPerSecond = 10; + int messages = 0; + foreach (BallastFloraBranch branch in Branches) + { + //don't notify about minuscule amounts of damage (<= 1.0f) + if (branch.AccumulatedDamage > 1.0f) + { + CreateNetworkMessage(new BranchDamageEventData(branch)); + branch.AccumulatedDamage = 0.0f; + messages++; + //throttle a bit: if a large ballast flora is withering, it can lead to a very large number of events otherwise + if (messages > maxMessagesPerSecond) { break; } + } + } + damageUpdateTimer = DamageUpdateInterval; + } + public void ServerWrite(IWriteMessage msg, IEventData eventData) { msg.Write((byte)eventData.NetworkHeader); switch (eventData) { - case SpawnEventData spawnEventData: + case SpawnEventData _: ServerWriteSpawn(msg); break; - case KillEventData killEventData: + case KillEventData _: //do nothing break; case BranchCreateEventData branchCreateEventData: @@ -72,6 +95,7 @@ namespace Barotrauma.MapCreatures.Behavior var (x, y) = branch.Position; msg.Write(parentId); msg.Write((int)branch.ID); + msg.Write(branch.IsRootGrowth); msg.WriteRangedInteger((byte)branch.Type, 0b0000, 0b1111); msg.WriteRangedInteger((byte)branch.Sides, 0b0000, 0b1111); msg.WriteRangedInteger(branch.FlowerConfig.Serialize(), 0, 0xFFF); @@ -103,7 +127,7 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.ID); } - public void SendNetworkMessage(IEventData extraData) + public void CreateNetworkMessage(IEventData extraData) { GameMain.Server.CreateEntityEvent(Parent, new Hull.BallastFloraEventData(this, extraData)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 7a8d742eb..93a36099e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -28,7 +28,11 @@ namespace Barotrauma.Networking public static string GetCompressedModPath(ContentPackage mod) { string dir = mod.Dir; - string resultFileName = dir.Replace('\\', '_').Replace('/', '_'); + string resultFileName + = dir.StartsWith(ContentPackage.LocalModsDir) + ? $"Local_{mod.Name}" + : $"Workshop_{mod.Name}"; + resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 804c11d40..4373de52d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -814,7 +814,7 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; - case ClientPacketHeader.MONEY: + case ClientPacketHeader.TRANSFER_MONEY: ReadMoneyMessage(inc, connectedClient); break; case ClientPacketHeader.REWARD_DISTRIBUTION: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 334e62414..5580a12fe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -321,11 +321,16 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); + if (HiddenSubs.Any()) + { + UpdateFlag(NetFlags.HiddenSubs); + } SelectedSubmarine = SelectNonHiddenSubmarine(SelectedSubmarine); string[] defaultAllowedClientNameChars = - new string[] { + new string[] + { "32-33", "38-46", "48-57", diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index 715ef5a3a..ba77838e6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -16,7 +16,7 @@ namespace Barotrauma Role = role; Character = character; Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData()); } public delegate void MessageSender(string message); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 989383c24..0b4d31d6b 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.3.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 48ef25dce..25e64fa6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -56,7 +56,7 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; - private readonly float attackLimbResetInterval = 2; + private readonly float attackLimbSelectionInterval = 3; // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. private const float minPriority = 10; @@ -65,10 +65,10 @@ namespace Barotrauma private float updateTargetsTimer; private float updateMemoriesTimer; - private float attackLimbResetTimer; + private float attackLimbSelectionTimer; - private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; - private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0; + private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning; + private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; private float Hearing => AIParams.Hearing; @@ -77,25 +77,25 @@ namespace Barotrauma private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - private Limb _attackingLimb; - private Limb _previousAttackingLimb; - public Limb AttackingLimb + private Limb _attackLimb; + private Limb _previousAttackLimb; + public Limb AttackLimb { - get { return _attackingLimb; } + get { return _attackLimb; } private set { - attackLimbResetTimer = 0; - if (_attackingLimb != value) + if (_attackLimb != value) { - _previousAttackingLimb = _attackingLimb; + _previousAttackLimb = _attackLimb; + _previousAttackLimb?.AttachedRope?.Snap(); } - if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0) + else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0) { - SetAimTimer(); + _attackLimb.AttachedRope?.Snap(); } - _attackingLimb = value; + _attackLimb = value; attackVector = null; - Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; + Reverse = _attackLimb != null && _attackLimb.attack.Reverse; } } @@ -425,7 +425,8 @@ namespace Barotrauma private void ReleaseDragTargets() { - if (Character.Inventory != null) + AttackLimb?.AttachedRope?.Snap(); + if (Character.Params.CanInteract && Character.Inventory != null) { Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); } @@ -600,7 +601,7 @@ namespace Barotrauma UpdatePatrol(deltaTime); break; case AIState.Attack: - run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; + run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); break; case AIState.Eat: @@ -620,7 +621,7 @@ namespace Barotrauma return; } float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); + var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; @@ -875,7 +876,10 @@ namespace Barotrauma if (followLastTarget) { var target = SelectedAiTarget ?? _lastAiTarget; - if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) + if (target?.Entity != null && !target.Entity.Removed && + PreviousState == AIState.Attack && Character.CurrentHull == null && + (_previousAttackLimb?.attack == null || + _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) { // Keep heading to the last known position of the target var memory = GetTargetMemory(target, false); @@ -1126,31 +1130,42 @@ namespace Barotrauma return; } } + + attackLimbSelectionTimer -= deltaTime; + if (AttackLimb == null || attackLimbSelectionTimer <= 0) + { + attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f); + if (!IsAttackRunning && !IsCoolDownRunning) + { + AttackLimb = GetAttackLimb(attackWorldPos); + } + } bool canAttack = true; bool pursue = false; - if (IsCoolDownRunning) + if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) { - var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb; + var currentAttackLimb = AttackLimb ?? _previousAttackLimb; if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - switch (currentAttackLimb.attack.AfterAttack) + AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack; + switch (activeBehavior) { case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (activeBehavior == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } } @@ -1162,13 +1177,13 @@ namespace Barotrauma if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) + if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } - AttackingLimb = null; + AttackLimb = null; } else { @@ -1177,19 +1192,19 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { // No new limb was found. - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (activeBehavior == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } } @@ -1204,10 +1219,15 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: + case AIBehaviorAfterAttack.ReverseUntilCanAttack: + if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack) + { + Reverse = true; + } if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1217,7 +1237,7 @@ namespace Barotrauma // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1227,12 +1247,12 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1240,7 +1260,7 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1269,7 +1289,7 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { @@ -1291,7 +1311,12 @@ namespace Barotrauma UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; case AIBehaviorAfterAttack.FallBack: + case AIBehaviorAfterAttack.Reverse: default: + if (activeBehavior == AIBehaviorAfterAttack.Reverse) + { + Reverse = true; + } UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); return; } @@ -1303,12 +1328,13 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable)) + if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity)) { - AttackingLimb = GetAttackLimb(attackWorldPos); + AttackLimb = GetAttackLimb(attackWorldPos); } - canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; + canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0; } + if (!AIParams.CanOpenDoors) { if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) @@ -1347,8 +1373,8 @@ namespace Barotrauma // Target a specific limb instead of the target center position if (wallTarget == null && targetCharacter != null) { - var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; - attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); + var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType; + attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType); if (attackTargetLimb == null) { State = AIState.Idle; @@ -1361,7 +1387,7 @@ namespace Barotrauma } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) @@ -1389,23 +1415,23 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } - float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; } + float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; + if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } - float distanceOffset = diff * AttackingLimb.attack.Duration; + float distanceOffset = diff * AttackLimb.attack.Duration; // Intentionally omit the unit conversion because we use distanceOffset as a multiplier. return targetVelocity * distanceOffset * dot; } // Check that we can reach the target distance = toTarget.Length(); - canAttack = distance < AttackingLimb.attack.Range; + canAttack = distance < AttackLimb.attack.Range; // Crouch if the target is down (only humanoids), so that we can reach it. - if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2) + if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2) { - if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range) + if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) { humanoidAnimController.Crouching = true; } @@ -1413,14 +1439,14 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb.attack.Ranged) + if (AttackLimb.attack.Ranged) { // Check that is facing the target - float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset * Character.AnimController.Dir); float angle = VectorExtensions.Angle(forward, toTarget); - canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle); - if (canAttack && AttackingLimb.attack.AvoidFriendlyFire) + canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); + if (canAttack && AttackLimb.attack.AvoidFriendlyFire) { float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; @@ -1434,11 +1460,11 @@ namespace Barotrauma } if (canAttack) { - canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range)); + canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range)); bool IsBlocked(Vector2 targetPosition) { - foreach (var body in Submarine.PickBodies(AttackingLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) { Character hitTarget = null; if (body.UserData is Character c) @@ -1460,22 +1486,8 @@ namespace Barotrauma } } } - else if (!IsAttackRunning && !IsCoolDownRunning) - { - // If not, reset the attacking limb, if the cooldown is not running - // Don't use the property, because we don't want cancel reversing, if we are reversing. - if (attackLimbResetTimer > attackLimbResetInterval) - { - _attackingLimb = null; - attackLimbResetTimer = 0; - } - else - { - attackLimbResetTimer += deltaTime; - } - } } - Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null; + Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. @@ -1490,9 +1502,9 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackingLimb != null && AttackingLimb.attack.Retreat) + if (AttackLimb != null && AttackLimb.attack.Retreat) { - UpdateFallBack(attackWorldPos, deltaTime, false); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); } else { @@ -1527,7 +1539,7 @@ namespace Barotrauma } // When pursuing, we don't want to pursue too close float max = 300; - float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max; + float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; if (!canAttack || distance > margin) { // Steer towards the target if in the same room and swimming @@ -1558,10 +1570,10 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.Ranged) + if (AttackLimb.attack.Ranged) { float dir = Character.AnimController.Dir; - if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin) + if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin) { SteeringManager.Reset(); } @@ -1658,9 +1670,9 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) { - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) @@ -1703,10 +1715,10 @@ namespace Barotrauma // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; } else { @@ -1741,9 +1753,9 @@ namespace Barotrauma } } } - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) { - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } canAttack = false; @@ -1800,7 +1812,7 @@ namespace Barotrauma } } - if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100)) + if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1811,7 +1823,7 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else if (AttackingLimb.attack.Ranged) + else if (AttackLimb.attack.Ranged) { // Too close UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1824,18 +1836,18 @@ namespace Barotrauma } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) + if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); } } else if (IsAttackRunning) { - AttackingLimb.attack.ResetAttackTimer(); + AttackLimb.attack.ResetAttackTimer(); } } - private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, IDamageable target) + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) { if (attackingLimb == null) { return false; } if (target == null) { return false; } @@ -1854,10 +1866,11 @@ namespace Barotrauma // Check that is approximately facing the target Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; + if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) { return false; } float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); - float angle = VectorExtensions.Angle(forward, toTarget); - if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; } + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); + if (angle > attack.RequiredAngle) { return false; } } return true; } @@ -1867,7 +1880,7 @@ namespace Barotrauma private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); - IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; + Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; @@ -1901,12 +1914,13 @@ namespace Barotrauma float CalculatePriority(Limb limb, Vector2 attackPos) { - if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; } + float prio = 1 + limb.attack.Priority; + if (Character.AnimController.SimplePhysicsEnabled) { return prio; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. // We also need a max value that is more than the actual range. float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); - return (1 + limb.attack.Priority) * distanceFactor; + return prio * distanceFactor; } } @@ -1919,7 +1933,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } - if (Character.Params.CanInteract && attackResult.Damage > 10) + if (attackResult.Damage >= AIParams.DamageThreshold) { ReleaseDragTargets(); } @@ -2007,9 +2021,11 @@ namespace Barotrauma if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning)) { - // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; - avoidGunFire = false; + if (IsAttackRunning) + { + avoidGunFire = false; + } } if (retaliate) { @@ -2022,7 +2038,7 @@ namespace Barotrauma } } } - else if (avoidGunFire) + else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold) { State = AIState.Escape; avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); @@ -2114,7 +2130,7 @@ namespace Barotrauma { if (attackingLimb.attack.CoolDownTimer > 0) { - SetAimTimer(); + SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f)); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; if (!(damageTarget is Character)) @@ -2245,19 +2261,19 @@ namespace Barotrauma // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } - Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); - if (!MathUtils.IsValid(attackDir)) + Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); + if (!MathUtils.IsValid(dir)) { - attackDir = Vector2.UnitY; + dir = Vector2.UnitY; } - steeringManager.SteeringManual(deltaTime, attackDir); - if (Character.AnimController.InWater) + steeringManager.SteeringManual(deltaTime, dir); + if (Character.AnimController.InWater && !Reverse) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } if (checkBlocking) { - return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2)); + return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2)); } return true; } @@ -2990,7 +3006,7 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; - Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition; + Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { Vector2 rayEnd = SelectedAiTarget.SimPosition; @@ -3303,6 +3319,7 @@ namespace Barotrauma foreach (var triggerObject in activeTriggers) { AITrigger trigger = triggerObject.Key; + if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) { @@ -3471,7 +3488,7 @@ namespace Barotrauma disableTailCoroutine = null; } Character.AnimController.ReleaseStuckLimbs(); - AttackingLimb = null; + AttackLimb = null; movementMargin = 0; ResetEscape(); if (isStateChanged && to == AIState.Idle && from != to) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index a98cef44d..5dc813a8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -148,14 +148,14 @@ namespace Barotrauma } if (TargetCharacter != null) { - if (enemyAI.AttackingLimb?.attack == null) + if (enemyAI.AttackLimb?.attack == null) { DeattachFromBody(reset: true, cooldown: 1); } else { - float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; - if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) + float range = enemyAI.AttackLimb.attack.DamageRange * 2f; + if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range) { DeattachFromBody(reset: true, cooldown: 1); } @@ -265,11 +265,11 @@ namespace Barotrauma if (enemyAI.IsSteeringThroughGap) { break; } if (_attachPos == Vector2.Zero) { break; } if (!AttachToSub && !AttachToCharacters) { break; } - if (enemyAI.AttackingLimb == null) { break; } + if (enemyAI.AttackLimb == null) { break; } if (targetBody == null) { break; } if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); - if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) + if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 65597452e..e282061e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -99,8 +99,7 @@ namespace Barotrauma { if (InWater || !CanWalk) { - float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; - return TargetMovement.LengthSquared() > avg * avg; + return TargetMovement.LengthSquared() > SwimSlowParams.MovementSpeed; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 699f94e37..f898ac8d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -448,21 +448,18 @@ namespace Barotrauma movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; var mainLimb = MainLimb; - if (isMoving) + float t = 0.5f; + if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { - float t = 0.5f; - if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) { - Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); - float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); - if (dot < 0) - { - // Reduce the linear movement speed when not facing the movement direction - t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); - } + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index b386fdd83..30fc791e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -443,8 +443,6 @@ namespace Barotrauma if (CurrentGroundedParams == null) { return; } Vector2 handPos; - //if you're allergic to magic numbers, stop reading now - Limb leftFoot = GetLimb(LimbType.LeftFoot); Limb rightFoot = GetLimb(LimbType.RightFoot); Limb head = GetLimb(LimbType.Head); @@ -599,16 +597,20 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } + if (Crouching && !movingHorizontally && !aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (HeadAngle.HasValue) + if (!aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); } + else + { + RotateHead(head); + } if (!onGround) { @@ -883,23 +885,17 @@ namespace Barotrauma } } float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f) + if (aiming) { - if (!aiming) - { - float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - torso.SimPosition) * Dir; + float newRotation = MathUtils.VectorToAngle(diff); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } - else + else if (targetSpeed > 0.1f) { - if (aiming) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - torso.SimPosition) * Dir; - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } + float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); @@ -914,13 +910,14 @@ namespace Barotrauma { torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } - if (HeadAngle.HasValue) + + if (!aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue) { head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } else { - head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque); + RotateHead(head); } //dont try to move upwards if head is already out of water @@ -951,7 +948,18 @@ namespace Barotrauma if (isNotRemote) { - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp); + float t = movementLerp; + if (targetSpeed > 0.00001f && !SimplePhysicsEnabled) + { + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) + { + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + } + } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } WalkPos += movement.Length(); @@ -1227,7 +1235,11 @@ namespace Barotrauma //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); - if (!aiming) + if (aiming) + { + RotateHead(head); + } + else { float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); @@ -1710,6 +1722,24 @@ namespace Barotrauma } } + private void RotateHead(Limb head) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 dir = (mousePos - head.SimPosition) * Dir; + float rot = MathUtils.VectorToAngle(dir); + var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso); + if (neckJoint != null) + { + float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation); + float lowerLimit = neckJoint.LowerLimit + offset; + float upperLimit = neckJoint.UpperLimit + offset; + float min = Math.Min(lowerLimit, upperLimit); + float max = Math.Max(lowerLimit, upperLimit); + rot = Math.Clamp(rot, min, max); + } + head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque); + } + private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) { if (!MathUtils.IsValid(pos)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 874e6e000..9277a79ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -805,7 +805,7 @@ namespace Barotrauma SeverLimbJointProjSpecific(limbJoint, playSound: true); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData()); } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index ff3af7ccf..53b7ee230 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -37,7 +37,9 @@ namespace Barotrauma Pursue, FollowThrough, FollowThroughUntilCanAttack, - IdleUntilCanAttack + IdleUntilCanAttack, + Reverse, + ReverseUntilCanAttack } struct AttackResult @@ -102,7 +104,7 @@ namespace Barotrauma public bool Retreat { get; private set; } private float _range; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float Range { get => _range * RangeMultiplier; @@ -110,13 +112,16 @@ namespace Barotrauma } private float _damageRange; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float DamageRange { get => _damageRange * RangeMultiplier; set => _damageRange = value; } + [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + public float MinRange { get; private set; } + [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } @@ -686,7 +691,7 @@ namespace Barotrauma public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; - public bool IsValidTarget(IDamageable target) + public bool IsValidTarget(Entity target) { return TargetType switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 3f6245bc7..4978d0f49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1885,7 +1885,7 @@ namespace Barotrauma if (!attack.IsValidContext(currentContexts)) { return false; } if (attackTarget != null) { - if (!attack.IsValidTarget(attackTarget)) { return false; } + if (!attack.IsValidTarget(attackTarget as Entity)) { return false; } if (attackTarget is ISerializableEntity se && attackTarget is Character) { if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } @@ -3148,7 +3148,8 @@ namespace Barotrauma var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } - foreach (Item inventoryItem in Inventory.AllItemsMod) + List inventoryItems = new List(Inventory.AllItemsMod); + foreach (Item inventoryItem in inventoryItems) { if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents)) { @@ -3156,17 +3157,25 @@ namespace Barotrauma inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents); } } + //this needs to happen after the items have been dropped (we can no longer sync dropping the items if the character has been removed) + Spawner.AddEntityToRemoveQueue(this); } } - - Spawner.AddEntityToRemoveQueue(this); + else + { + Spawner.AddEntityToRemoveQueue(this); + } } public void DespawnNow(bool createNetworkEvents = true) { despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); - Spawner.Update(createNetworkEvents); + //update twice: first to spawn the duffel bag and move the items into it, then to remove the character + for (int i = 0; i < 2; i++) + { + Spawner.Update(createNetworkEvents); + } } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -4012,7 +4021,7 @@ namespace Barotrauma if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); } isDead = true; @@ -4168,6 +4177,11 @@ namespace Barotrauma } DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")"); +#if CLIENT + //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns + Inventory?.ApplyReceivedState(); +#endif + base.Remove(); foreach (Item heldItem in HeldItems.ToList()) @@ -4179,12 +4193,12 @@ namespace Barotrauma #if CLIENT GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false); + + if (Controlled == this) { Controlled = null; } #endif CharacterList.Remove(this); - if (Controlled == this) { Controlled = null; } - if (Inventory != null) { foreach (Item item in Inventory.AllItems) @@ -4266,7 +4280,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData()); + newItem.CreateStatusEvent(); } #if SERVER newItem.GetComponent()?.SyncHistory(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 11eab9eba..731461475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -51,7 +51,7 @@ namespace Barotrauma } } - public struct StatusEventData : IEventData + public struct CharacterStatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index f2bc10d8b..df1cede68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -852,6 +852,19 @@ namespace Barotrauma #if CLIENT public void RecreateHead(MultiplayerPreferences characterSettings) { + if (characterSettings.HairIndex == -1 && + characterSettings.BeardIndex == -1 && + characterSettings.MoustacheIndex == -1 && + characterSettings.FaceAttachmentIndex == -1) + { + //randomize if nothing is set + SetAttachments(Rand.RandSync.Unsynced); + characterSettings.HairIndex = Head.HairIndex; + characterSettings.BeardIndex = Head.BeardIndex; + characterSettings.MoustacheIndex = Head.MoustacheIndex; + characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex; + } + RecreateHead( characterSettings.TagSet.ToImmutableHashSet(), characterSettings.HairIndex, @@ -859,9 +872,14 @@ namespace Barotrauma characterSettings.MoustacheIndex, characterSettings.FaceAttachmentIndex); - Head.SkinColor = characterSettings.SkinColor; - Head.HairColor = characterSettings.HairColor; - Head.FacialHairColor = characterSettings.FacialHairColor; + Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor); + Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor); + Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor); + + Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor) + { + return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced); + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 320d295d0..a83def334 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -52,17 +52,19 @@ namespace Barotrauma DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names."); continue; } - - Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); - Identifier afflictionType = subElement.GetAttributeIdentifier("type", ""); - float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - if (!afflictionIdentifier.IsEmpty) + var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); + if (vitalityMultipliers == null) { - VitalityMultipliers.Add(afflictionIdentifier, multiplier); + vitalityMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); + } + if (vitalityMultipliers != null) + { + float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); + vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier)); } else { - VitalityTypeMultipliers.Add(afflictionType, multiplier); + DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b08cafe51..c430090d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -540,6 +540,8 @@ namespace Barotrauma private set; } + public Items.Components.Rope AttachedRope { get; set; } + public string Name => Params.Name; // These properties are exposed for status effects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 2fdb19b98..a1920a820 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -103,6 +103,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] + public bool FixedHeadAngle { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation @@ -131,7 +134,7 @@ namespace Barotrauma get => MathHelper.ToDegrees(FootAngleInRadians); set { - FootAngleInRadians = MathHelper.ToRadians(value); + FootAngleInRadians = MathHelper.ToRadians(value); } } public float FootAngleInRadians { get; private set; } @@ -156,6 +159,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] + public bool FixedHeadAngle { get; set; } } public interface IHumanAnimation @@ -166,5 +172,7 @@ namespace Barotrauma float ArmMoveStrength { get; set; } float HandMoveStrength { get; set; } + + bool FixedHeadAngle { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 640bf791c..08cc0f106 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -104,6 +104,9 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool DrawLast { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -574,6 +577,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "How much damage is required for single attack to trigger avoiding/releasing targets."), Editable(minValue: 0f, maxValue: 1000f)] + public float DamageThreshold { get; private set; } + [Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] public float AvoidTime { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 73e07dcf1..35865bb05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -30,6 +30,7 @@ namespace Barotrauma public readonly bool NotSyncedInMultiplayer; public readonly ImmutableHashSet? AlternativeTypes; public readonly ImmutableHashSet Names; + private readonly MethodInfo? contentPathMutator; public TypeInfo(Type type) { @@ -40,9 +41,11 @@ namespace Barotrauma var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; + contentPathMutator + = Type.GetMethod(nameof(MutateContentPath), BindingFlags.Static | BindingFlags.Public); HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; - if (type.GetCustomAttribute()?.Names is { } altNames) + if (type.GetCustomAttribute(inherit: false)?.Names is { } altNames) { names.UnionWith(altNames); } @@ -50,6 +53,10 @@ namespace Barotrauma Names = names.ToImmutableHashSet(); } + public ContentPath MutateContentPath(ContentPath path) + => (ContentPath?)contentPathMutator?.Invoke(null, new object[] { path }) + ?? path; + public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); } @@ -80,6 +87,7 @@ namespace Barotrauma } try { + filePath = type.MutateContentPath(filePath); if (!File.Exists(filePath.FullPath)) { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs new file mode 100644 index 000000000..85f6a6fa1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs @@ -0,0 +1,28 @@ +using System; +using Barotrauma.IO; + +namespace Barotrauma +{ + sealed class ServerExecutableFile : OtherFile + { + //This content type doesn't do very much on its own, it's handled manually by the Host Server menu + public ServerExecutableFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public static ContentPath MutateContentPath(ContentPath path) + { + if (File.Exists(path.FullPath)) { return path; } + + string rawValueWithoutExtension() + => Barotrauma.IO.Path.Combine( + Barotrauma.IO.Path.GetDirectoryName(path.RawValue ?? ""), + Barotrauma.IO.Path.GetFileNameWithoutExtension(path.RawValue ?? "")).CleanUpPath(); + + path = ContentPath.FromRaw(path.ContentPackage, rawValueWithoutExtension()); + if (File.Exists(path.FullPath)) { return path; } + + path = ContentPath.FromRaw(path.ContentPackage, + rawValueWithoutExtension() + ".exe"); + return path; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index dae572835..586da6857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -110,7 +110,7 @@ namespace Barotrauma !expectedHash.IsNullOrWhiteSpace() && !expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase); - public IEnumerable GetFiles() where T : ContentFile => Files.Where(f => f is T).Cast(); + public IEnumerable GetFiles() where T : ContentFile => Files.OfType(); public IEnumerable GetFiles(Type type) => !type.IsSubclassOf(typeof(ContentFile)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs index 33daaec39..02a6f4401 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class RequiredByCorePackage : Attribute { public readonly ImmutableHashSet AlternativeTypes; @@ -18,7 +18,7 @@ namespace Barotrauma } } - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class AlternativeContentTypeNames : Attribute { public readonly ImmutableHashSet Names; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index bf2faed95..a437d78cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -327,7 +327,8 @@ namespace Barotrauma => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); public static IEnumerable AllPackages - => LocalPackages.CollectionConcat(WorkshopPackages); + => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages) + .OfType(); public static void UpdateContentPackageList() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 8562d6800..5274ff2e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -168,25 +168,34 @@ namespace Barotrauma }; })); + void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab + { + NewMessage("***************", Color.Cyan); + foreach (T prefab in prefabs) + { + if (prefab.Name.IsNullOrEmpty()) { continue; } + string text = $"- {prefab.Name}"; + if (prefab.Tags.Any()) + { + text += $" ({string.Join(", ", prefab.Tags)})"; + } + if (prefab.AllowedLinks?.Any() ?? false) + { + text += $", Links: {string.Join(", ", prefab.AllowedLinks)}"; + } + NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple); + } + NewMessage("***************", Color.Cyan); + } commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) => { - NewMessage("***************", Color.Cyan); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) - { - if (itemPrefab.Name.IsNullOrEmpty()) { continue; } - string text = $"- {itemPrefab.Name}"; - if (itemPrefab.Tags.Any()) - { - text += $" ({string.Join(", ", itemPrefab.Tags)})"; - } - if (itemPrefab.AllowedLinks.Any()) - { - text += $", Links: {string.Join(", ", itemPrefab.AllowedLinks)}"; - } - NewMessage(text, Color.Cyan); - } - NewMessage("***************", Color.Cyan); + printMapEntityPrefabs(ItemPrefab.Prefabs); + })); + + commands.Add(new Command("itemassemblies", "itemassemblies: List all the item assemblies available for spawning.", (string[] args) => + { + printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs); })); @@ -202,6 +211,7 @@ namespace Barotrauma string[] creatureAndJobNames = CharacterPrefab.Prefabs.Select(p => p.Identifier.Value) .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value)) + .OrderBy(s => s) .ToArray(); return new string[][] @@ -732,9 +742,16 @@ namespace Barotrauma { #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { return; } - Character.Controlled = null; - GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; - GameMain.Client?.SendConsoleCommand("freecam"); + + if (GameMain.Client == null) + { + Character.Controlled = null; + GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; + } + else + { + GameMain.Client?.SendConsoleCommand("freecam"); + } #endif }, isCheat: true)); @@ -2382,7 +2399,7 @@ namespace Barotrauma public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { - ThrowError(error.Value); + ThrowError(error.Value, e, createMessageBox, appendStackTrace); } public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 6b09ce371..d7d042774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -153,6 +153,13 @@ namespace Barotrauma swarmSpawned = true; } +#if DEBUG || UNSTABLE + if (State == 1 && !level.CheckBeaconActive()) + { + DebugConsole.ThrowError("Beacon became inactive!"); + State = 2; + } +#endif } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 934b9df66..8a02928df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -248,7 +248,7 @@ namespace Barotrauma { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); + int deliveredItemCount = items.Count(it => IsItemDelivered(it)); if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { GiveReward(); @@ -267,5 +267,12 @@ namespace Barotrauma items.Clear(); failed = !completed; } + + private bool IsItemDelivered(Item item) + { + if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } + var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine; + return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index a525764bd..413e835e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -419,17 +419,17 @@ namespace Barotrauma public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) { int remainingRewards = totalReward; - HashSet nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet(); - float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution); - if (sum == 0) { return remainingRewards; } - foreach (Character character in nonBotCrew) + float sum = GetRewardDistibutionSum(crew); + if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } + foreach (Character character in crew) { - float rewardWeight = character.Wallet.RewardDistribution / sum; - int reward = (int)Math.Floor(totalReward * rewardWeight); - reward = Math.Max(remainingRewards, reward); + int rewardDistribution = character.Wallet.RewardDistribution; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; + int reward = (int)(totalReward * rewardWeight); + reward = Math.Min(remainingRewards, reward); character.Wallet.Give(reward); remainingRewards -= reward; - if (0 >= remainingRewards) { break; } + if (remainingRewards <= 0) { break; } } return remainingRewards; @@ -442,26 +442,27 @@ namespace Barotrauma IEnumerable characters = crewManager.GetCharacters(); #if SERVER - return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters); + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters); #elif CLIENT return characters; #endif - static bool IsAlive(Character c) { return c?.Info != null && !c.IsDead; } } + public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) + + public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) { - float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - if (sum == 0) { return (0, 0); } + float sum = GetRewardDistibutionSum(crew, rewardDistribution); + if (MathUtils.NearlyEqual(sum, 0)) { return (0, 0, sum); } - float rewardWeight = rewardDistribution / sum; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); return reward switch { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage), - None _ => (0, rewardPercentage), + Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), + None _ => (0, rewardPercentage, sum), _ => throw new ArgumentOutOfRangeException() }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index ec2aea487..c51ad855c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -228,7 +228,7 @@ namespace Barotrauma } GiveReward(); completed = true; - if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))) + if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))) { level.LevelData.HasHuntingGrounds = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 585de85c8..fcd724d2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -30,22 +30,16 @@ namespace Barotrauma } #if CLIENT - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null) + public PurchasedItem(ItemPrefab itemPrefab, int quantity) + : this(itemPrefab, quantity, buyer: null) { } +#endif + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) { ItemPrefab = itemPrefab; Quantity = quantity; IsStoreComponentEnabled = null; BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; } -#elif SERVER - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) - { - ItemPrefab = itemPrefab; - Quantity = quantity; - IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0; - } -#endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index d32c25575..abd84ef03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -25,12 +25,18 @@ namespace Barotrauma public int Balance; } + /// + /// Network message for the server to update wallet values to clients + /// internal struct NetWalletUpdate : INetSerializableStruct { [NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)] public NetWalletTransaction[] Transactions; } + /// + /// Network message for the client to transfer money between wallets + /// [NetworkSerialize] internal struct NetWalletTransfer : INetSerializableStruct { @@ -39,7 +45,10 @@ namespace Barotrauma public int Amount; } - internal struct NetWalletSalaryUpdate : INetSerializableStruct + /// + /// Network message for the client to set the salary of someone + /// + internal struct NetWalletSetSalaryUpdate : INetSerializableStruct { [NetworkSerialize] public ushort Target; @@ -48,6 +57,10 @@ namespace Barotrauma public int NewRewardDistribution; } + /// + /// Represents the difference in balance and salary when a wallet gets updated + /// Not really used right now but could be used for notifications when receiving funds similar to how talents do it + /// [NetworkSerialize] internal struct WalletChangedData : INetSerializableStruct { @@ -82,6 +95,9 @@ namespace Barotrauma } } + /// + /// Represents an update that changed the amount of money or salary of the wallet + /// [NetworkSerialize] internal struct NetWalletTransaction : INetSerializableStruct { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index dbde33069..8f1f97323 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -321,15 +321,32 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))); if (!huntingGroundsMissionPrefabs.Any()) { - DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); + DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); } else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand); + // Adjust the prefab commonness based on the difficulty tag + var prefabs = huntingGroundsMissionPrefabs.ToList(); + var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList(); + for (int i = 0; i < prefabs.Count; i++) + { + var prefab = prefabs[i]; + var weight = weights[i]; + if (prefab.Tags.Contains("easy")) + { + weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty)); + } + else if (prefab.Tags.Contains("hard")) + { + weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty)); + } + weights[i] = weight; + } + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 0da770a21..e6fc1f3e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -754,9 +754,9 @@ namespace Barotrauma try { - IEnumerable crewCharacters = GetSessionCrewCharacters(); + ImmutableArray crewCharacters = GetSessionCrewCharacters().ToImmutableArray(); - int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution + int prevMoney = GetAmountOfMoney(crewCharacters); foreach (Mission mission in missions) { @@ -828,7 +828,7 @@ namespace Barotrauma LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { - GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); campaignMode.TotalPlayTime += roundDuration; } #if CLIENT @@ -840,6 +840,17 @@ namespace Barotrauma { RoundEnding = false; } + + int GetAmountOfMoney(IEnumerable crew) + { + if (!(GameMode is CampaignMode campaign)) { return 0; } + + return GameMain.NetworkMember switch + { + null => campaign.Bank.Balance, + _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance + }; + } } public void LogEndRoundStats(string eventId) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 6a8df4f5e..f85b6bafe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -216,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets + if (Campaign.GetWallet(client).TryDeduct(price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index e34ef8f1b..f8249369e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] public bool ApplyEffectsOnDocking { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 40bd8a5b0..891fc3fce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -424,6 +424,15 @@ namespace Barotrauma.Items.Components } } + public override void UpdateBroken(float deltaTime, Camera cam) + { + //update when the item is broken too to get OnContaining effects to execute and contained item positions to update + if (IsActive) + { + Update(deltaTime, cam); + } + } + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index b375eb205..0ee0d9d50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER - hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData()); + hull.BallastFlora.CreateNetworkMessage(new BallastFloraBehavior.SpawnEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index be83e2267..0a0d2af0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; using Microsoft.Xna.Framework; using System.Collections.Generic; #if CLIENT @@ -151,20 +150,6 @@ namespace Barotrauma.Items.Components } set { - if (powerIn != null) - { - if (powerIn.Grid != null) - { - powerIn.Grid.Voltage = Math.Max(0.0f, value); - } - } - else if (powerOut != null) - { - if (powerOut.Grid != null) - { - powerOut.Grid.Voltage = Math.Max(0.0f, value); - } - } voltage = Math.Max(0.0f, value); } } @@ -213,11 +198,6 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = false; } #endif - if (powerIn == null) - { - //power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic) - Voltage -= deltaTime; - } } public override void Update(float deltaTime, Camera cam) @@ -470,6 +450,11 @@ namespace Barotrauma.Items.Components //Determine if devices are adding a load or providing power, also resolve solo nodes foreach (Powered powered in poweredList) { + //Make voltage decay to ensure the device powers down. + //This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery) + //or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead. + powered.Voltage -= deltaTime; + //Handle the device if it's got a power connection if (powered.powerIn != null && powered.powerOut != powered.powerIn) { @@ -500,7 +485,7 @@ namespace Barotrauma.Items.Components } else { - powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); + powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); powered.GridResolved(powered.powerIn); } } @@ -541,7 +526,7 @@ namespace Barotrauma.Items.Components else { //Perform power calculations for the singular connection - float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); + float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); if (powered is PowerTransfer pt2) { pt2.PowerLoad = loadOut; @@ -667,7 +652,7 @@ namespace Barotrauma.Items.Components public static bool ValidPowerConnection(Connection conn1, Connection conn2) { - return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock"))); + return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fcddb442b..37c5e5533 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -49,8 +49,6 @@ namespace Barotrauma.Items.Components //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; - //a duration during which the projectile won't drop from the body it's stuck to - private const float PersistentStickJointDuration = 1.0f; private PrismaticJoint stickJoint; public Attack Attack { get; private set; } @@ -86,8 +84,6 @@ namespace Barotrauma.Items.Components get { return hits; } } - private float persistentStickJointTimer; - [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } @@ -116,13 +112,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] - public bool StickPermanently - { - get; - set; - } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { @@ -151,6 +140,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "")] + public bool StickToLightTargets + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan @@ -212,16 +208,29 @@ namespace Barotrauma.Items.Components set; } + private float stickTimer; + [Serialize(0f, IsPropertySaveable.No)] + public float StickDuration + { + get; + set; + } + + [Serialize(-1f, IsPropertySaveable.No)] + public float MaxJointTranslation + { + get; + set; + } + private float _maxJointTranslation = -1; + public Body StickTarget { get; private set; } - public bool IsStuckToTarget - { - get { return StickTarget != null; } - } + public bool IsStuckToTarget => StickTarget != null; private Category originalCollisionCategories; private Category originalCollisionTargets; @@ -660,23 +669,22 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - if (persistentStickJointTimer > 0.0f && !StickPermanently) + if (StickDuration > 0 && stickTimer > 0) { - persistentStickJointTimer -= deltaTime; + stickTimer -= deltaTime; return; } + float absoluteMaxTranslation = 100; // Update the item's transform to make sure it's inside the same sub as the target (or outside) - if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f) + if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > absoluteMaxTranslation) { item.UpdateTransform(); } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (StickTargetRemoved() || - (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) || - Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far + if (StickTargetRemoved() || Math.Abs(stickJoint.JointTranslation) > _maxJointTranslation) { Unstick(); #if SERVER @@ -936,14 +944,12 @@ namespace Barotrauma.Items.Components { item.body.LinearVelocity *= deflectedSpeedMultiplier; } - else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick - //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && - hits.Count() >= MaxTargetsToHit && - target.Body.Mass > item.body.Mass * 0.5f && + else if ( stickJoint == null && StickTarget == null && + StickToStructures && target.Body.UserData is Structure || + ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || - (StickToStructures && target.Body.UserData is Structure) || - (StickToItems && target.Body.UserData is Item))) + (StickToItems && target.Body.UserData is Item)))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), @@ -1026,6 +1032,8 @@ namespace Barotrauma.Items.Components { if (stickJoint != null) { return; } + item.body.ResetDynamics(); + stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true) { MotorEnabled = true, @@ -1034,18 +1042,17 @@ namespace Barotrauma.Items.Components Breakpoint = 1000.0f }; - if (StickPermanently) + if (_maxJointTranslation == -1) { - stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; - item.body.ResetDynamics(); - } - else if (item.Sprite != null) - { - stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale); - stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale); + if (item.Sprite != null && MaxJointTranslation < 0) + { + MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale; + } + MaxJointTranslation = Math.Min(MaxJointTranslation, 1000); + _maxJointTranslation = ConvertUnits.ToSimUnits(MaxJointTranslation); } - persistentStickJointTimer = PersistentStickJointDuration; + stickTimer = StickDuration; StickTarget = targetBody; GameMain.World.Add(stickJoint); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 7c5b320ba..d77939f02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -1,9 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -12,8 +12,36 @@ namespace Barotrauma.Items.Components private ISpatialEntity source; private Item target; + private Vector2? launchDir; + + private void SetSource(ISpatialEntity source) + { + this.source = source; + if (source is Limb sourceLimb) + { + sourceLimb.AttachedRope = this; + float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir); + } + } + + private void ResetSource() + { + if (source is Limb sourceLimb && sourceLimb.AttachedRope == this) + { + sourceLimb.AttachedRope = null; + } + source = null; + } + private float snapTimer; - private const float SnapAnimDuration = 1.0f; + + [Serialize(1.0f, IsPropertySaveable.No, description: "")] + public float SnapAnimDuration + { + get; + set; + } private float raycastTimer; private const float RayCastInterval = 0.2f; @@ -46,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(360.0f, IsPropertySaveable.No, description: "How far the source item can be from the projectile until the rope breaks.")] + public float MaxAngle + { + get; + set; + } + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] public bool SnapOnCollision { @@ -115,8 +150,8 @@ namespace Barotrauma.Items.Components { System.Diagnostics.Debug.Assert(source != null); System.Diagnostics.Debug.Assert(target != null); - this.source = source; this.target = target; + SetSource(source); Snapped = false; ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; @@ -127,7 +162,7 @@ namespace Barotrauma.Items.Components if (source == null || target == null || target.Removed || (source is Entity sourceEntity && sourceEntity.Removed)) { - source = null; + ResetSource(); target = null; IsActive = false; return; @@ -144,12 +179,27 @@ namespace Barotrauma.Items.Components } Vector2 diff = target.WorldPosition - source.WorldPosition; - if (diff.LengthSquared() > MaxLength * MaxLength) + float lengthSqr = diff.LengthSquared(); + if (lengthSqr > MaxLength * MaxLength) { Snap(); return; } + if (MaxAngle < 180 && lengthSqr > 2500) + { + if (launchDir == null) + { + launchDir = diff; + } + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff)); + if (angle > MaxAngle) + { + Snap(); + return; + } + } + #if CLIENT item.ResetCachedVisibleSize(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 1e2638509..094d5a973 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1677,12 +1677,17 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + CreateStatusEvent(); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } + public void CreateStatusEvent() + { + GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); + } + private bool isActive = true; public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index a4a939944..e0c65a747 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -63,7 +63,7 @@ namespace Barotrauma } } - private readonly struct StatusEventData : IEventData + private readonly struct ItemStatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 9708fd2e4..2be6ddb93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -520,8 +520,9 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; } float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount; if (branchHealAmount <= 0.0f) { continue; } + float prevHealth = branch.Health; branch.Health += branchHealAmount; - branch.AccumulatedDamage -= branchHealAmount; + branch.AccumulatedDamage += (prevHealth - branch.Health); } } StateMachine.Update(deltaTime); @@ -633,7 +634,8 @@ namespace Barotrauma.MapCreatures.Behavior { if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) { - DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot); + float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth); + DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); } if (branch.Health <= 0.0f) { @@ -836,7 +838,7 @@ namespace Barotrauma.MapCreatures.Behavior } #if SERVER - SendNetworkMessage(new BranchCreateEventData(newBranch, parent)); + CreateNetworkMessage(new BranchCreateEventData(newBranch, parent)); #endif return true; } @@ -878,7 +880,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); + CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); } #endif } @@ -955,8 +957,6 @@ namespace Barotrauma.MapCreatures.Behavior /// private void CreateBody(BallastFloraBranch branch) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - Rectangle rect = branch.Rect; Vector2 pos = Parent.Position + Offset + branch.Position; @@ -975,6 +975,14 @@ 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) { @@ -983,8 +991,28 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } - // damage is handled server side currently - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (type != AttackType.Other && type != AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + Anger += damage * 0.001f; + } + + if (GameMain.NetworkMember != null) + { + // damage is handled server side + if (GameMain.NetworkMember.IsClient) + { + return; + } + else + { + //accumulate damage on the server's side to ensure clients get notified + if (type == AttackType.Other || type == AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + } + } + } if (attacker != null && toxinsCooldown <= 0) { @@ -1014,11 +1042,6 @@ namespace Barotrauma.MapCreatures.Behavior } branch.Health -= damage; - if (type != AttackType.Other && type != AttackType.CutFromRoot) - { - branch.AccumulatedDamage += damage; - Anger += damage * 0.001f; - } #if SERVER GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); @@ -1110,7 +1133,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!wasRemoved) { - SendNetworkMessage(new BranchRemoveEventData(branch)); + CreateNetworkMessage(new BranchRemoveEventData(branch)); } #endif } @@ -1141,7 +1164,7 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); #endif } @@ -1159,7 +1182,7 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - SendNetworkMessage(new KillEventData()); + CreateNetworkMessage(new KillEventData()); #endif } @@ -1181,7 +1204,7 @@ namespace Barotrauma.MapCreatures.Behavior _entityList.Remove(this); #if SERVER - SendNetworkMessage(new KillEventData()); + CreateNetworkMessage(new KillEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 13d72bc6a..05539c6e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -160,17 +160,20 @@ namespace Barotrauma public void Delete() { Dispose(); - if (File.Exists(ContentFile.Path)) + try { - try + if (ContentPackage is { Files: { Length: 1 } } + && ContentPackageManager.LocalPackages.Contains(ContentPackage)) { - File.Delete(ContentFile.Path); - } - catch (Exception e) - { - DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + Directory.Delete(ContentPackage.Dir, recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); } } + catch (Exception e) + { + DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + } } public override void Dispose() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 42b982c38..f184f08f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -3826,12 +3826,12 @@ namespace Barotrauma if (location != null) { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } else { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } foreach (string categoryToHide in locationType.HideEntitySubcategories) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 4fd6d7166..cc0a5bd5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -31,8 +31,16 @@ namespace Barotrauma public bool HasHuntingGrounds, OriginallyHadHuntingGrounds; + //minimum difficulty of the level before hunting grounds can appear + public const float HuntingGroundsDifficultyThreshold = 25; + + //probability of hunting grounds appearing in 100% difficulty levels + public const float MaxHuntingGroundsProbability = 0.3f; + public OutpostGenerationParams ForceOutpostGenerationParams; + public bool AllowInvalidOutpost; + public readonly Point Size; public readonly int InitialDepth; @@ -150,11 +158,7 @@ namespace Barotrauma } else { - //minimum difficulty of the level before hunting grounds can appear - float huntingGroundsDifficultyThreshold = 25; - //probability of hunting grounds appearing in 100% difficulty levels - float maxHuntingGroundsProbability = 0.3f; - HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; + HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability; HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); } IsBeaconActive = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index f7b648463..2a1bbd3bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -182,8 +182,7 @@ namespace Barotrauma { for (int i = 0; i < Connections.Count; i++) { - float maxHuntingGroundsProbability = 0.3f; - Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability; + Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability; connectionElements[i].SetAttributeValue("hashuntinggrounds", true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 8a92048f3..336462b62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -57,17 +57,17 @@ namespace Barotrauma } } - public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false) + public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false) { - return Generate(generationParams, locationType, location: null, onlyEntrance); + return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost); } - public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false) + public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) { - return Generate(generationParams, location.Type, location, onlyEntrance); + return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost); } - private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) + private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) { var outpostModuleFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -197,12 +197,19 @@ namespace Barotrauma AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); if (pendingModuleFlags.Any(flag => flag != "none")) { - remainingTries--; - if (remainingTries <= 0) + if (!allowInvalidOutpost) { - DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); + remainingTries--; + if (remainingTries <= 0) + { + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); + } + continue; + } + else + { + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed."); } - continue; } var outpostInfo = new SubmarineInfo() @@ -328,7 +335,10 @@ namespace Barotrauma selectedModule.Offset = (selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) - selectedModule.ThisGap.WorldPosition; - selectedModule.Offset += moveDir * generationParams.MinHallwayLength; + if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null) + { + selectedModule.Offset += moveDir * generationParams.MinHallwayLength; + } } entities[selectedModule] = moduleEntities; } @@ -464,6 +474,16 @@ namespace Barotrauma pendingModuleFlags.Remove(initialModuleFlag); pendingModuleFlags.Insert(0, initialModuleFlag); + if (pendingModuleFlags.Count > totalModuleCount) + { + DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules..."); + int removeCount = pendingModuleFlags.Count - totalModuleCount; + for (int i = 0; i < removeCount; i++) + { + pendingModuleFlags.Remove(pendingModuleFlags.Last()); + } + } + return pendingModuleFlags; } @@ -486,46 +506,71 @@ namespace Barotrauma if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) + for (int i = 0; i < 2; i++) { - if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) + //try placing a module meant for this location type first, and if that fails, try choosing whatever fits + bool allowDifferentLocationType = i > 0; + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) - { - var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType); - if (newModule != null) { placedModules.Add(newModule); } - if (pendingModuleFlags.Count == 0) { return true; } + if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + } + + PlacedModule newModule = null; + //try appending to the current module if possible + if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) + { + newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + } + + if (newModule != null) + { + placedModules.Add(newModule); + } + else + { + //couldn't append to current module, try one of the other placed modules + foreach (PlacedModule otherModule in selectedModules) + { + if (otherModule == currentModule) { continue; } + foreach (OutpostModuleInfo.GapPosition otherGapPosition in + GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) + { + newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + if (newModule != null) + { + placedModules.Add(newModule); + break; + } + } + if (newModule != null) { break; } + } + } + if (pendingModuleFlags.Count == 0) { return true; } } } - //couldn't place anything, retry - if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule)) + //couldn't place a module anywhere, we're probably fucked! + if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) { - //try to append to some other module first - foreach (PlacedModule otherModule in selectedModules) - { - if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) - { - return true; - } - } //try to replace the previously placed module with something else that we can append to - var failedModule = currentModule; for (int i = 0; i < 10; i++) { selectedModules.Remove(currentModule); + assertAllPreviousModulesPresent(); //readd the module types that the previous module was supposed to fulfill to the pending module types pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes); if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); } //retry - currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType); + currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true); + assertAllPreviousModulesPresent(); if (currentModule == null) { break; } if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { + assertAllPreviousModulesPresent(); return true; } } @@ -534,9 +579,14 @@ namespace Barotrauma foreach (PlacedModule placedModule in placedModules) { - AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType); + AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule); } return placedModules.Count > 0; + + void assertAllPreviousModulesPresent() + { + System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); + } } /// @@ -553,7 +603,8 @@ namespace Barotrauma List availableModules, List pendingModuleFlags, List selectedModules, - LocationType locationType) + LocationType locationType, + bool allowDifferentLocationType) { if (pendingModuleFlags.Count == 0) { return null; } @@ -562,7 +613,7 @@ namespace Barotrauma foreach (Identifier moduleFlag in pendingModuleFlags) { flagToPlace = moduleFlag; - nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType); + nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType); if (nextModule != null) { break; } } @@ -603,6 +654,7 @@ namespace Barotrauma foreach (PlacedModule otherModule in modules2) { if (module == otherModule) { continue; } + if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; } if (ModulesOverlap(module, otherModule)) { module1 = module; @@ -775,22 +827,25 @@ namespace Barotrauma } } - private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) + private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) { IEnumerable availableModules = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { availableModules = modules - .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); } else { availableModules = modules - .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); } + + availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); + if (prevModule != null) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo)); + availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); } if (availableModules.Count() == 0) { return null; } @@ -800,15 +855,22 @@ namespace Barotrauma availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type - if (!modulesSuitableForLocationType.Any()) + if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) { modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); } if (!modulesSuitableForLocationType.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); + if (allowDifferentLocationType) + { + 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); + } + else + { + return null; + } } else { @@ -1039,16 +1101,20 @@ namespace Barotrauma if (hallwayLength <= 1.0f) { continue; } - var suitableModules = availableModules.Where(m => - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); - if (suitableModules.Count() == 0) + Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(); + var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); + + var suitableHallwayModules = hallwayModules.Where(m => + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); + if (suitableHallwayModules.Count() == 0) { - suitableModules = availableModules.Where(m => + suitableHallwayModules = hallwayModules.Where(m => !m.OutpostModuleInfo.AllowAttachToModules.Any() || m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")); } - var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType); + + var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType); if (hallwayInfo == null) { DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\"."); @@ -1170,7 +1236,7 @@ namespace Barotrauma var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap); if (startWaypoint == null) { - DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); + DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); continue; } var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index c79a10a64..794a9671b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -45,6 +45,9 @@ namespace Barotrauma [Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] public GapPosition GapPositions { get; set; } + [Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")] + public GapPosition CanAttachToPrevious { get; set; } + public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index ae5e402d5..1fb8afbae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -48,11 +48,11 @@ namespace Barotrauma { if (!subs.Any()) yield return CoroutineStatus.Success; - Character.Controlled = null; - cam.TargetPos = Vector2.Zero; #if CLIENT + Character.Controlled = null; GameMain.LightManager.LosEnabled = false; #endif + cam.TargetPos = Vector2.Zero; Level.Loaded.TopBarrier.Enabled = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 84bf74588..acdee62e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -322,7 +322,7 @@ namespace Barotrauma #endif Tags = tags.ToImmutableHashSet(); - AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); + AllowedLinks = ImmutableHashSet.Empty; } protected override void CreateInstance(Rectangle rect) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 90f123c95..e405ecb4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic - MONEY, //wallet updates + TRANSFER_MONEY, // wallet transfers REWARD_DISTRIBUTION, // wallet reward distribution READY_CHECK, READY_TO_SPAWN diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index dacc2c8a7..89c8bc1bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -172,11 +172,11 @@ namespace Barotrauma return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase); } - public static ContentPath GetAttributeContentPath(this XElement element, string name, - ContentPackage contentPackage) + public static ContentPath GetAttributeContentPath(this XElement element, string name, ContentPackage contentPackage) { - if (element?.GetAttribute(name) == null) { return null; } - return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null)); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return null; } + return ContentPath.FromRaw(contentPackage, GetAttributeString(attribute, null)); } public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index b917b844a..551b48fdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -10,6 +10,7 @@ using System.Xml.Linq; using Barotrauma.IO; #if CLIENT using Barotrauma.ClientSource.Settings; +using Barotrauma.Networking; using Microsoft.Xna.Framework.Input; #endif @@ -453,8 +454,12 @@ namespace Barotrauma 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); + currentConfig = newConfig; -#warning TODO: Implement program state updates; #if CLIENT if (setGraphicsMode) @@ -462,6 +467,24 @@ namespace Barotrauma GameMain.Instance.ApplyGraphicsSettings(); } + if (audioOutputChanged) + { + GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice); + } + + if (voiceCaptureChanged) + { + VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); + } + + if (textScaleChanged) + { + foreach (var font in GUIStyle.Fonts.Values) + { + font.Prefabs.ForEach(p => p.LoadFont()); + } + } + GameMain.SoundManager?.ApplySettings(); #endif if (languageChanged) { TextManager.ClearCache(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 688ebbfbb..2bdef6f7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -68,15 +68,21 @@ namespace Barotrauma public bool IsTriggered { get; private set; } - public float Timer { get; private set; } = -1; + public float Timer { get; private set; } public bool IsActive { get; private set; } + public bool IsPermanent { get; private set; } + public void Launch() { IsTriggered = true; IsActive = true; - Timer = Duration; + IsPermanent = Duration <= 0; + if (!IsPermanent) + { + Timer = Duration; + } } public void Reset() @@ -88,6 +94,7 @@ namespace Barotrauma public void UpdateTimer(float deltaTime) { + if (IsPermanent) { return; } Timer -= deltaTime; if (Timer < 0) { @@ -1243,24 +1250,27 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) + var target = targets[i]; + Limb targetLimb = target as Limb; + if (targetLimb == null && target is Character character) { foreach (Limb limb in character.AnimController.Limbs) { if (limb.body == sourceBody) { + targetLimb = limb; if (breakLimb) { character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); } - else - { - limb.HideAndDisable(hideLimbTimer); - } break; } } } + if (hideLimb) + { + targetLimb?.HideAndDisable(hideLimbTimer); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index fefee9d61..40f135e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -2,19 +2,19 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma.IO { static class Validation { - private static readonly string[] unwritableDirs = new string[] { "Content" }; - private static readonly string[] unwritableExtensions = new string[] + private static readonly ImmutableArray unwritableDirs = new[] { "Content".ToIdentifier() }.ToImmutableArray(); + private static readonly ImmutableArray unwritableExtensions = new[] { - ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite) + ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe, .dll and .json handled separately in CanWrite) ".bat", ".sh", //shell scripts - ".json" //deps.json - }; + }.ToIdentifiers().ToImmutableArray(); /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. @@ -24,25 +24,27 @@ namespace Barotrauma.IO public static bool CanWrite(string path, bool isDirectory) { path = System.IO.Path.GetFullPath(path).CleanUpPath(); + string localModsDir = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); + string workshopModsDir = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); if (!isDirectory) { - string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); - if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + Identifier extension = System.IO.Path.GetExtension(path).Replace(" ", "").ToIdentifier(); + if (unwritableExtensions.Any(e => e == extension)) { return false; } - if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) - && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) + if (!path.StartsWith(workshopModsDir, StringComparison.OrdinalIgnoreCase) + && !path.StartsWith(localModsDir, StringComparison.OrdinalIgnoreCase) + && (extension == ".dll" || extension == ".exe" || extension == ".json")) { return false; } } - foreach (string unwritableDir in unwritableDirs) + foreach (var unwritableDir in unwritableDirs) { - string dir = System.IO.Path.GetFullPath(unwritableDir).CleanUpPath(); + string dir = System.IO.Path.GetFullPath(unwritableDir.Value).CleanUpPath(); if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index f5663e4fd..32a3fb494 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,62 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.3.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- A new abyss monster (placeholder art and sounds, name pending) +- Overhauled colonies: completely new modules, improved layouts, new structures and items, new events. +- Adjustments to reactors and supercapacitors to prevent the increased supercapacitor loads from crippling the subs on default recharge rates: slightly increased Humpback reactor output and decreased the default recharge rate of the capacitors, reduced recharge rates in the 3 new subs and set the supercapacitor efficiency to match the rest of the subs. +- Throw an error in debug and unstable builds if beacon station becomes inactive after it's been activated (hopefully helps us diagnose why beacon missions sometimes fail after the beacon's been activated). +- Reimplemented ServerExecutable to be usable in non-core packages. Now, players must select a server executable from a dropdown in the "Host server" menu if multiple are available. +- Animation adjustment: The head now rotates towards the mouse cursor while aiming or swimming. +- Swim animation adjustment: The body now rotates towards the aim target also when the character is moving, and not only while staying still. Moving while not facing the movement direction results in reduced movement speed. +- Set the bottom hole probability to 0 in Cold Caverns, which reduces the size and the frequency of holes in the level bottom. +- Adjusted the probabilities for spawning the Thalamus in the wrecks. +- Added a (WIP) difficulty hierarchy for the abyss monsters. Easier monsters should spawn more frequently on an easier difficulty level, the harder should spawn more frequently on higher difficulty levels. Currently the new abyss monster is defined as the easiest, and Endworm the hardest. Charybdis is in between. + +Fixes: +- Re-filled Typhon 2 oxygen tank shelves. +- Fixed spawnpoint editing panel being too small on large resolutions. +- Fixed inability to equip one-handed items when there's a suitable container in the other hand (e.g. flashlight when there's a storage container in the other hand). +- Cargo missions don't require the cargo to be inside a hull: being in the sub is enough. Fixes inability to complete cargo missions with unconventional subs where the cargo is stored outside hulls. +- Fixed non-equipped items that can't be put into a duffel bag disappearing when a character despawns. +- Fixed incorrect animation parameters being used for swimming while wearing a regular diving suit. +- Fixed projectiles sometimes staying attached to the target even when they are far from it. +- Fixed monsters sometimes trying to follow targets after losing the track of them even when they should be falling back from them (according to the after attack behavior). +- Fixed monsters sometimes using the after attack behavior of the current attack even when the cooldown of that attack is not active. +- Fixed monsters sometimes being unable to target the submarine, because their attack was incorrectly considered invalid. +- Fixed fractal guardians fleeing to a shelter immediatedly after taking some damage when they have targeted the guardian pod once and have not changed the target yet (e.g. if you shoot a guardian that is returning from the pod and if it has not yet spotted you). + +Fixes (unstable only): +- Fixed text scale slider not working. +- Fixed audio capture and output settings changes not being applied until the game was restarted. +- Fixed numerous circumstances where the Publish tab could cause a crash or softlock. +- Fixed creating and deleting item assemblies in the submarine editor. +- Fixed deleting submarines in the submarine editor. +- Trimmed down the filenames of mods transferred from the server to the clients. +- Fixed ballast flora's damage visualizations (particles, branches shaking, healthbar) not working in multiplayer (unstable only). +- Fixed occasional "invalid SetAttackTarget/ExecuteAttack" errors in multiplayer (unstable only). +- Fixed clients not taking control off the previous character properly when using freecam, preventing the character from moving if another player or AI takes control of it (unstable only). +- Fixed "event data was of the wrong type" error when characters spawn with items with depleted condition in their inventory (unstable only). +- Fixed incorrect power displayed on the reactor when unwired (unstable only). +- Fixed devices not powering down if they're disconnected from the grid when their voltage has been set above 0. Could be reproduced by powering up the sub in mp campaign, entering a new level and disconnecting e.g. the oxygen generator from the grid (unstable). +- Fixed hidden subs resetting client-side when restarting a server (unstable only). +- Randomize character appearance in server lobby if it hasn't been set (= when launching the game or v0.17 for the first time). Unstable only. + +Modding: +- ItemContainers apply the OnContaining effects even when the item is broken. Doesn't affect any vanilla items. +- Ropes attached to limbs now automatically snap when another attack is chosen. +- Ropes can now be set to break from the end instead of always breaking from the middle (see the new abyss monster for an example). +- Ropes can be set to break if they are in too steep angle to the target. +- Projectiles always stick permanently unless a stick duration is defined. +- Characters (with deformable sprites) can be set to be drawn after (on top of) other characters. Normally characters are drawn in the order of spawning. +- AI Triggers can now be permanent. +- Added a generic damage threshold that currently defines how much damage the character needs to take from a single hit to hit the avoiding and releasing captured targets. +- Added a support for multiple identifiers and types in the limb health definitions. +- Added a support for min range for ranged attacks. +- Fixed monsters not being able to shoot faster than every ~1.5 second if they change the attacking limb. +- Added new after attack behaviors: Reverse and ReverseUntilCanAttack. + --------------------------------------------------------------------------------------------------------- v0.17.2.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt index f29df7c55..a2c5918d6 100644 --- a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt +++ b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt @@ -1,4 +1,4 @@ -Copyright (c) 2019 FakeFish Games +Copyright (c) 2019 FakeFish Ltd. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/README.md b/README.md index 99597222e..7943f78d2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,6 @@ If you're interested in working on the code, either to develop mods or to contri ### Windows - [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 8.0 support (VS 2019 or later recommended) ### Linux -- [.NET Core 3.0 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) +- [.NET Core 3.1 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) ### macOS - [Visual Studio 2019 for Mac](https://visualstudio.microsoft.com/vs/mac/) From 2968e23ae8a1e061106e13a41a71009cde7ed554 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Wed, 30 Mar 2022 00:06:59 +0900 Subject: [PATCH 5/9] Unstable 0.17.2.0 --- .../Characters/AI/EnemyAIController.cs | 2 +- .../Characters/CharacterNetworking.cs | 17 +- .../Characters/Health/CharacterHealth.cs | 2 +- .../ContentPackageManager.cs | 24 +- .../ClientSource/GUI/GUIPrefab.cs | 2 +- .../ClientSource/GUI/MedicalClinicUI.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 44 +--- .../GameSession/GameModes/CampaignMode.cs | 4 - .../GameModes/MultiPlayerCampaign.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../ClientSource/Items/CharacterInventory.cs | 2 +- .../Items/Components/ItemContainer.cs | 7 +- .../Items/Components/ItemLabel.cs | 51 +--- .../ClientSource/Items/Components/Rope.cs | 20 +- .../ClientSource/Items/Inventory.cs | 2 +- .../ClientSource/Items/Item.cs | 4 +- .../Map/Creatures/BallastFloraBehavior.cs | 4 +- .../ClientSource/Map/WayPoint.cs | 12 +- .../Networking/Voip/VoipCapture.cs | 13 +- .../Networking/Voip/VoipClient.cs | 12 +- .../CharacterEditor/CharacterEditorScreen.cs | 3 +- .../ClientSource/Screens/GameScreen.cs | 25 +- .../ClientSource/Screens/LevelEditorScreen.cs | 10 +- .../ClientSource/Screens/MainMenuScreen.cs | 149 +++++------- .../ClientSource/Screens/ModDownloadScreen.cs | 9 - .../ClientSource/Screens/SubEditorScreen.cs | 107 +++------ .../Serialization/SerializableEntityEditor.cs | 30 +-- .../ClientSource/Steam/PublishTab.cs | 98 ++++---- .../ClientSource/Steam/Workshop.cs | 5 - .../BarotraumaClient/LinuxClient.csproj | 4 +- Barotrauma/BarotraumaClient/MacClient.csproj | 4 +- .../BarotraumaClient/WindowsClient.csproj | 4 +- .../BarotraumaServer/LinuxServer.csproj | 4 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/Character.cs | 2 +- .../Characters/CharacterNetworking.cs | 2 +- .../ServerSource/GameSession/CargoManager.cs | 2 +- .../GameModes/MultiPlayerCampaign.cs | 8 +- .../ServerSource/Items/Item.cs | 3 +- .../Map/Creatures/BallastFloraBehavior.cs | 32 +-- .../Networking/FileTransfer/ModSender.cs | 6 +- .../ServerSource/Networking/GameServer.cs | 2 +- .../ServerSource/Networking/ServerSettings.cs | 7 +- .../ServerSource/Traitors/Traitor.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 4 +- .../SharedSource}/CameraTransition.cs | 0 .../Characters/AI/EnemyAIController.cs | 225 ++++++++---------- .../SharedSource/Characters/AI/LatchOntoAI.cs | 10 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/FishAnimController.cs | 19 +- .../Animation/HumanoidAnimController.cs | 74 ++---- .../Characters/Animation/Ragdoll.cs | 2 +- .../SharedSource/Characters/Attack.cs | 13 +- .../SharedSource/Characters/Character.cs | 32 +-- .../Characters/CharacterEventData.cs | 2 +- .../SharedSource/Characters/CharacterInfo.cs | 24 +- .../Characters/Health/CharacterHealth.cs | 16 +- .../SharedSource/Characters/Limb.cs | 2 - .../Params/Animation/HumanoidAnimations.cs | 10 +- .../Characters/Params/CharacterParams.cs | 6 - .../ContentFile/ContentFile.cs | 10 +- .../ContentFile/ServerExecutableFile.cs | 28 --- .../ContentPackage/ContentPackage.cs | 2 +- .../ContentPackage/CorePackage.cs | 4 +- .../ContentPackageManager.cs | 3 +- .../SharedSource/DebugConsole.cs | 57 ++--- .../Events/Missions/BeaconMission.cs | 7 - .../Events/Missions/CargoMission.cs | 9 +- .../SharedSource/Events/Missions/Mission.cs | 33 ++- .../Events/Missions/MonsterMission.cs | 2 +- .../SharedSource/GameSession/CargoManager.cs | 14 +- .../SharedSource/GameSession/Data/Wallet.cs | 18 +- .../GameSession/GameModes/CampaignMode.cs | 23 +- .../SharedSource/GameSession/GameSession.cs | 17 +- .../GameSession/UpgradeManager.cs | 2 +- .../Items/Components/DockingPort.cs | 2 +- .../Items/Components/ItemContainer.cs | 9 - .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Power/Powered.cs | 31 ++- .../Items/Components/Projectile.cs | 79 +++--- .../SharedSource/Items/Components/Rope.cs | 62 +---- .../SharedSource/Items/Item.cs | 7 +- .../SharedSource/Items/ItemEventData.cs | 2 +- .../Map/Creatures/BallastFloraBehavior.cs | 57 ++--- .../SharedSource/Map/ItemAssemblyPrefab.cs | 17 +- .../SharedSource/Map/Levels/Level.cs | 4 +- .../SharedSource/Map/Levels/LevelData.cs | 14 +- .../SharedSource/Map/Map/Map.cs | 3 +- .../Map/Outposts/OutpostGenerator.cs | 166 ++++--------- .../Map/Outposts/OutpostModuleInfo.cs | 3 - .../SharedSource/Map/RoundEndCinematic.cs | 4 +- .../SharedSource/Map/StructurePrefab.cs | 2 +- .../SharedSource/Networking/NetworkMember.cs | 2 +- .../Serialization/XMLExtensions.cs | 8 +- .../SharedSource/Settings/GameSettings.cs | 25 +- .../StatusEffects/StatusEffect.cs | 24 +- .../SharedSource/Utils/SafeIO.cs | 26 +- Barotrauma/BarotraumaShared/changelog.txt | 59 ----- .../LICENSE_webm_mem_playback.txt | 2 +- README.md | 2 +- 100 files changed, 654 insertions(+), 1379 deletions(-) rename Barotrauma/{BarotraumaClient/ClientSource => BarotraumaShared/SharedSource}/CameraTransition.cs (100%) delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index bfb8b7202..9b31bf9bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -39,7 +39,7 @@ namespace Barotrauma targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); - if (wallTarget != null && !IsCoolDownRunning) + if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 28c3230ac..22d741e27 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -152,7 +152,7 @@ namespace Barotrauma case TreatmentEventData _: msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; - case CharacterStatusEventData _: + case StatusEventData _: //do nothing break; case UpdateTalentsEventData _: @@ -343,12 +343,8 @@ namespace Barotrauma if (controlled == this) { Controlled = null; + IsRemotePlayer = ownerID > 0; } - if (GameMain.Client?.Character == this) - { - GameMain.Client.Character = null; - } - IsRemotePlayer = ownerID > 0; } break; case EventType.Status: @@ -375,9 +371,7 @@ namespace Barotrauma if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; @@ -386,16 +380,13 @@ namespace Barotrauma if (targetEntity == null && eventType == EventType.SetAttackTarget) { DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); - GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetNotFound", GameAnalyticsManager.ErrorSeverity.Error, "Received invalid SetAttackTarget message. Target entity not found."); break; } - if (targetEntity is Character targetCharacter && targetLimbIndex != 255) + if (targetEntity is Character targetCharacter) { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); - string errorMsgWithoutName = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.SpeciesName}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"; - GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsgWithoutName); break; } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 436e47141..b61c76669 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -400,7 +400,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.CharacterStatusEventData()); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.StatusEventData()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 86a971bf5..3ae094982 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -12,30 +12,22 @@ namespace Barotrauma { public sealed partial class PackageSource : ICollection { - public string SaveRegularMod(ModProject modProject) + public ContentPackage SaveAndEnableRegularMod(ModProject modProject) { if (modProject.IsCore) { throw new ArgumentException("ModProject must not be a core package"); } - + + //save the content package string fileListPath = Path.Combine(directory, ToolBox.RemoveInvalidFileNameChars(modProject.Name), ContentPackage.FileListFileName) .CleanUpPathCrossPlatform(correctFilenameCase: false); + Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)!); modProject.Save(fileListPath); Refresh(); EnabledPackages.DisableRemovedMods(); + var newPackage = Regular.First(p => p.Path == fileListPath); - return fileListPath; - } + //enable it + EnabledPackages.EnableRegular(newPackage); - public RegularPackage GetRegularModByPath(string fileListPath) - { - return Regular.First(p => p.Path == fileListPath); - } - - public RegularPackage SaveAndEnableRegularMod(ModProject modProject) - { - string fileListPath = SaveRegularMod(modProject); - var package = GetRegularModByPath(fileListPath); - EnabledPackages.EnableRegular(package); - - return package; + return newPackage; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index 1487d6943..ccef64092 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -65,7 +65,7 @@ namespace Barotrauma LoadFont(); } - public void LoadFont() + private void LoadFont() { string fontPath = GetFontFilePath(element); uint size = GetFontSize(element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index 1ea2828e2..ed32a00b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -931,7 +931,7 @@ namespace Barotrauma }); } - public static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) + private static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) { if (string.IsNullOrWhiteSpace(text)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 62b0364d2..201d8d242 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -218,7 +218,7 @@ namespace Barotrauma public void AddToGUIUpdateList() { - infoFrame?.AddToGUIUpdateList(order: 1); + infoFrame?.AddToGUIUpdateList(); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); } @@ -379,7 +379,8 @@ namespace Barotrauma private void CreateCrewListFrame(GUIFrame crewFrame) { - crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? Array.Empty(); + // FIXME remove TestScreen stuff + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new []{ TestScreen.dummyCharacter }; teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -816,7 +817,7 @@ namespace Barotrauma else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); - GameMain.Client?.SelectCrewClient(client, preview); + if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewClient(client, preview); } CreateWalletFrame(background, client.Character); } @@ -849,18 +850,17 @@ namespace Barotrauma 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); + GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()), textAlignment: Alignment.BottomRight); 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), + Range = Vector2.UnitY, BarScrollValue = targetWallet.RewardDistribution / 100f, Step = 0.01f, BarSize = 0.1f, OnMoved = (bar, scroll) => { - SetRewardText((int)(scroll * 100), rewardBlock); + rewardBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()); return true; }, OnReleased = (bar, scroll) => @@ -871,9 +871,6 @@ namespace Barotrauma return true; } }; - - SetRewardText(targetWallet.RewardDistribution, rewardBlock); - // @formatter:off GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter)) { @@ -905,11 +902,8 @@ namespace Barotrauma GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; // @formatter:on - ImmutableArray layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout); - MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups); transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical") { - ToolTip = TextManager.Get("crewwallet.transfer.tooltip"), OnClicked = (button, o) => { isTransferMenuOpen = !isTransferMenuOpen; @@ -957,8 +951,6 @@ namespace Barotrauma break; } - MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); - if (!hasPermissions) { centerButton.Enabled = centerButton.CanBeFocused = false; @@ -1081,14 +1073,14 @@ namespace Barotrauma Receiver = to.Select(option => option.ID), Amount = amount }; - IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.TRANSFER_MONEY); + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.MONEY); transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } static void SetRewardDistribution(Character character, int newValue) { - INetSerializableStruct transfer = new NetWalletSetSalaryUpdate + INetSerializableStruct transfer = new NetWalletSalaryUpdate { Target = character.ID, NewRewardDistribution = newValue @@ -1098,23 +1090,7 @@ namespace Barotrauma 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); - } + string GetSharePercentage() => Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage.ToString(); } private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 824d1c178..7186c3bd9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -63,10 +63,6 @@ namespace Barotrauma } } - /// - /// Gets the current personal wallet - /// In singleplayer this is the campaign bank and in multiplayer this is the personal wallet - /// public virtual Wallet Wallet => GetWallet(); public override void ShowStartMessage() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 2cf62767f..597939e79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -914,7 +914,7 @@ namespace Barotrauma WalletInfo info = transaction.Info; switch (transaction.CharacterID) { - case Some { Value: var charID }: + case Some { Value: var charID}: { Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); if (targetCharacter is null) { break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index abe401df3..2770fc3c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -318,7 +318,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew().Where(c => c != controlled), Option.Some(reward)); + var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 4746f0e9f..e19ace1a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -966,7 +966,7 @@ namespace Barotrauma } 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.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 5fe3adde1..597c90daa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,7 +1,8 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; +using System; using System.Linq; +using System.Xml.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 01742a2d0..7c7821844 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -2,14 +2,12 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class ItemLabel : ItemComponent, IDrawableComponent, IHasExtraTextPickerEntries + partial class ItemLabel : ItemComponent, IDrawableComponent { private GUITextBlock textBlock; @@ -108,7 +106,7 @@ namespace Barotrauma.Items.Components set { scrollable = value; - IsActive = value || parseSpecialTextTagOnStart; + IsActive = value; TextBlock.Wrap = !scrollable; TextBlock.TextAlignment = scrollable ? Alignment.CenterLeft : Alignment.Center; } @@ -138,11 +136,6 @@ namespace Barotrauma.Items.Components { } - public IEnumerable GetExtraTextPickerEntries() - { - return SpecialTextTags; - } - private void SetScrollingText() { if (!scrollable) { return; } @@ -181,18 +174,9 @@ namespace Barotrauma.Items.Components scrollIndex = MathHelper.Clamp(scrollIndex, 0, DisplayText.Length); } - private static readonly string[] SpecialTextTags = new string[] { "[CurrentLocationName]", "[CurrentBiomeName]", "[CurrentSubName]" }; - private bool parseSpecialTextTagOnStart; private void SetDisplayText(string value) { - if (SpecialTextTags.Contains(value)) - { - parseSpecialTextTagOnStart = true; - IsActive = true; - } - DisplayText = IgnoreLocalization ? value : TextManager.Get(value).Fallback(value); - TextBlock.Text = DisplayText; if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) { @@ -214,37 +198,9 @@ namespace Barotrauma.Items.Components }; } - private void ParseSpecialTextTag() - { - switch (text) - { - case "[CurrentLocationName]": - SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); - break; - case "[CurrentBiomeName]": - SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); - break; - case "[CurrentSubName]": - SetDisplayText(item.Submarine?.Info?.DisplayName.Value ?? string.Empty); - break; - default: - break; - } - } - public override void Update(float deltaTime, Camera cam) { - if (parseSpecialTextTagOnStart) - { - ParseSpecialTextTag(); - parseSpecialTextTagOnStart = false; - } - - if (!scrollable) - { - IsActive = false; - return; - } + if (!scrollable) { return; } if (scrollingText == null) { @@ -330,6 +286,5 @@ namespace Barotrauma.Items.Components { Text = msg.ReadString(); } - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 36713eab5..2dc8ec191 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -34,13 +34,6 @@ namespace Barotrauma.Items.Components [Serialize("0.5,0.5)", IsPropertySaveable.No)] public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); - [Serialize(true, IsPropertySaveable.No, description: "")] - public bool BreakFromMiddle - { - get; - set; - } - public Vector2 DrawSize { get @@ -131,14 +124,9 @@ namespace Barotrauma.Items.Components int width = (int)(SpriteWidth * snapState); if (width > 0.0f) - { - float positionMultiplier = snapState; - if (BreakFromMiddle) - { - positionMultiplier /= 2; - DrawRope(spriteBatch, endPos - diff * positionMultiplier, endPos, width); - } - DrawRope(spriteBatch, startPos, startPos + diff * positionMultiplier, width); + { + DrawRope(spriteBatch, endPos - diff * snapState * 0.5f, endPos, width); + DrawRope(spriteBatch, startPos, startPos + diff * snapState * 0.5f, width); } } else @@ -155,7 +143,7 @@ namespace Barotrauma.Items.Components float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); } - if (endSprite != null && (!Snapped || BreakFromMiddle)) + if (endSprite != null) { float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 6224c628c..1c27aaf3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1846,7 +1846,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public void ApplyReceivedState() + private void ApplyReceivedState() { if (receivedItemIDs == null || (Owner != null && Owner.Removed)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 3cc3610e0..8cb513864 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1198,9 +1198,9 @@ namespace Barotrauma Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { - if (ic is Repairable r) + if (ic is Repairable) { - if (r.IsBelowRepairThreshold) { color = Color.Cyan; } + if (!IsFullCondition) { color = Color.Cyan; } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 264b5b2e7..80d814e5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -370,7 +370,6 @@ namespace Barotrauma.MapCreatures.Behavior private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); - bool isRootGrowth = msg.ReadBoolean(); byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); @@ -386,8 +385,7 @@ namespace Barotrauma.MapCreatures.Behavior { ID = id, MaxHealth = maxHealth, - Sides = (TileSide) sides, - IsRootGrowth = isRootGrowth + Sides = (TileSide) sides }; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index e69d0bbb0..8cd2acaaf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -272,7 +272,11 @@ namespace Barotrauma private GUIComponent CreateEditingHUD() { - editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) + int width = 500; + int height = spawnType == SpawnType.Path ? 80 : 200; + int x = GameMain.GraphicsWidth / 2 - width / 2, y = 30; + + editingHUD = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas) { ScreenSpaceOffset = new Point(x, y) }) { UserData = this }; @@ -280,7 +284,7 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) { Stretch = true, - AbsoluteSpacing = (int)(GUI.Scale * 5) + RelativeSpacing = 0.05f }; if (spawnType == SpawnType.Path) @@ -414,10 +418,6 @@ namespace Barotrauma }; } - editingHUD.RectTransform.Resize(new Point( - editingHUD.Rect.Width, - (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y))); - PositionEditingHUD(); return editingHUD; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index cf2caabed..98073f188 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -163,11 +163,16 @@ namespace Barotrauma.Networking public static void ChangeCaptureDevice(string deviceName) { - if (Instance == null) { return; } + var config = GameSettings.CurrentConfig; + config.Audio.VoiceCaptureDevice = deviceName; + GameSettings.SetCurrentConfig(config); - UInt16 storedBufferID = Instance.LatestBufferID; - Instance.Dispose(); - Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); + if (Instance != null) + { + UInt16 storedBufferID = Instance.LatestBufferID; + Instance.Dispose(); + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); + } } IntPtr nativeBuffer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 8916fc81e..85570fcbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -54,17 +54,7 @@ namespace Barotrauma.Networking } else { - try - { - if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } - } - catch (Exception e) - { - DebugConsole.ThrowError($"VoipCature.Create failed: {e.Message} {e.StackTrace.CleanupStackTrace()}"); - var config = GameSettings.CurrentConfig; - config.Audio.VoiceSetting = VoiceMode.Disabled; - GameSettings.SetCurrentConfig(config); - } + if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index e9de25b54..56032d102 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2805,8 +2805,7 @@ namespace Barotrauma.CharacterEditor return false; } #endif - ContentPath texturePath = ContentPath.FromRaw(character.Prefab.ContentPackage, RagdollParams.Texture); - if (!character.IsHuman && (texturePath.IsNullOrWhiteSpace() || !File.Exists(texturePath.Value))) + if (!character.IsHuman && !string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) { DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}"); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 2b01b3ff2..e58af763e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -248,24 +248,17 @@ namespace Barotrauma } spriteBatch.End(); - DrawDeformed(firstPass: true); - DrawDeformed(firstPass: false); - - void DrawDeformed(bool firstPass) + //draw characters with deformable limbs last, because they can't be batched into SpriteBatch + //pretty hacky way of preventing draw order issues between normal and deformable sprites + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); + //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) + for (int i = Character.CharacterList.Count - 1; i >= 0; i--) { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) - for (int i = Character.CharacterList.Count - 1; i >= 0; i--) - { - Character c = Character.CharacterList[i]; - if (!c.IsVisible) { continue; } - if (c.Params.DrawLast == firstPass) { continue; } - if (c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } - c.Draw(spriteBatch, Cam); - } - spriteBatch.End(); + Character c = Character.CharacterList[i]; + if (!c.IsVisible || c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } + c.Draw(spriteBatch, Cam); } - + spriteBatch.End(); Level.Loaded?.DrawFront(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index a1bbea8ab..470977caf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -32,7 +32,7 @@ namespace Barotrauma private readonly GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; + private readonly GUITickBox lightingEnabled, cursorLightEnabled, mirrorLevel; private Sprite editingSprite; @@ -126,7 +126,6 @@ namespace Barotrauma OnClicked = (btn, obj) => { SerializeAll(); - GUI.AddMessage(TextManager.Get("leveleditor.allsaved"), GUIStyle.Green); return true; } }; @@ -170,12 +169,6 @@ namespace Barotrauma 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), - TextManager.Get("leveleditor.allowinvalidoutpost")) - { - ToolTip = TextManager.Get("leveleditor.allowinvalidoutpost.tooltip") - }; - new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -186,7 +179,6 @@ namespace Barotrauma 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); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 5f574156c..0ff737f3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,7 +46,6 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; - private GUIDropDown serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -558,35 +557,6 @@ namespace Barotrauma GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); return true; } - - serverExecutableDropdown.ListBox.Content.Children.ToArray() - .Where(c => c.UserData is ServerExecutableFile f && !ContentPackageManager.EnabledPackages.All.Contains(f.ContentPackage)) - .ForEach(serverExecutableDropdown.ListBox.RemoveChild); - var newServerExes - = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()) - .Where(f => serverExecutableDropdown.ListBox.Content.Children.None(c => c.UserData == f)) - .ToArray(); - foreach (var newServerExe in newServerExes) - { - serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); - } - serverExecutableDropdown.ListBox.Content.Children.ForEach(c => - { - c.RectTransform.RelativeSize = (1.0f, c.RectTransform.RelativeSize.Y); - c.ForceLayoutRecalculation(); - }); - bool serverExePickable = serverExecutableDropdown.ListBox.Content.CountChildren > 1; - serverExecutableDropdown.Parent.Visible - = serverExePickable; - serverExecutableDropdown.Parent.RectTransform.RelativeSize - = (1.0f, serverExePickable ? 0.1f : 0.0f); - serverExecutableDropdown.Parent.ForceLayoutRecalculation(); - (serverExecutableDropdown.Parent.Parent as GUILayoutGroup)?.Recalculate(); - if (serverExecutableDropdown.SelectedComponent is null) - { - serverExecutableDropdown.Select(0); - } - break; case Tab.Tutorials: if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) @@ -814,7 +784,7 @@ namespace Barotrauma GameMain.ResetNetLobbyScreen(); try { - string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; + string exeName = "DedicatedServer.exe"; string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -844,20 +814,15 @@ namespace Barotrauma arguments += " -ownerkey " + ownerKey; } - string filename = Path.Combine( - Path.GetDirectoryName(exeName), - Path.GetFileNameWithoutExtension(exeName)); -#if WINDOWS - filename += ".exe"; -#else - filename = "./" + exeName; + string filename = exeName; +#if LINUX || OSX + filename = "./" + Path.GetFileNameWithoutExtension(exeName); + //arguments = ToolBox.EscapeCharacters(arguments); #endif - var processInfo = new ProcessStartInfo { FileName = filename, Arguments = arguments, - WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG CreateNoWindow = true, UseShellExecute = false, @@ -1219,12 +1184,12 @@ namespace Barotrauma label.RectTransform.MaxSize = serverNameBox.RectTransform.MaxSize; var maxPlayersLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("MaxPlayers"), textAlignment: textAlignment); - var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.1f }; - new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, OnClicked = ChangeMaxPlayers @@ -1244,7 +1209,7 @@ namespace Barotrauma currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers, 1, NetConfig.MaxPlayers); maxPlayersBox.Text = currMaxPlayers.ToString(); }; - new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, OnClicked = ChangeMaxPlayers @@ -1258,41 +1223,6 @@ namespace Barotrauma }; label.RectTransform.MaxSize = passwordBox.RectTransform.MaxSize; - var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), - TextManager.Get("ServerExecutable"), textAlignment: textAlignment); - const string vanillaServerOption = "Vanilla"; - serverExecutableDropdown - = new GUIDropDown(new RectTransform(textFieldSize, serverExecutableLabel.RectTransform, Anchor.CenterRight), - vanillaServerOption); - var listBoxSize = serverExecutableDropdown.ListBox.RectTransform.RelativeSize; - serverExecutableDropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.5f, listBoxSize.Y); - serverExecutableDropdown.AddItem(vanillaServerOption, userData: null); - serverExecutableDropdown.OnSelected = (selected, userData) => - { - if (userData != null) - { - var warningBox = new GUIMessageBox(headerText: TextManager.Get("Warning"), - text: TextManager.GetWithVariable("ModServerExesAtYourOwnRisk", "[exename]", serverExecutableDropdown.Text), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - warningBox.Buttons[0].OnClicked = (_, __) => - { - warningBox.Close(); - return false; - }; - warningBox.Buttons[1].OnClicked = (_, __) => - { - serverExecutableDropdown.Select(0); - warningBox.Close(); - return false; - }; - } - - serverExecutableDropdown.Text = ToolBox.LimitString(serverExecutableDropdown.Text, - serverExecutableDropdown.Font, serverExecutableDropdown.Rect.Width * 8 / 10); - - return true; - }; - // tickbox upper --------------- var tickboxAreaUpper = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, tickBoxSize.Y), parent.RectTransform), isHorizontal: true); @@ -1382,8 +1312,8 @@ namespace Barotrauma { var client = new RestClient(RemoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); - TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), - RemoteContentReceived); + client.ExecuteAsync(request, RemoteContentReceived); + CoroutineManager.StartCoroutine(WairForRemoteContentReceived()); } catch (Exception e) @@ -1397,31 +1327,58 @@ namespace Barotrauma } } - private void RemoteContentReceived(Task t) + private IEnumerable WairForRemoteContentReceived() { - try + while (true) { - if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } - string xml = remoteContentResponse.Content; - int index = xml.IndexOf('<'); - if (index > 0) { xml = xml.Substring(index, xml.Length - index); } - if (!string.IsNullOrWhiteSpace(xml)) + lock (remoteContentLock) { - remoteContentDoc = XDocument.Parse(xml); - foreach (var subElement in remoteContentDoc?.Root.Elements()) + if (remoteContentResponse != null) { break; } + } + yield return new WaitForSeconds(0.1f); + } + lock (remoteContentLock) + { + if (remoteContentResponse.ResponseStatus != ResponseStatus.Completed || remoteContentResponse.StatusCode != HttpStatusCode.OK) + { + yield return CoroutineStatus.Success; + } + + try + { + string xml = remoteContentResponse.Content; + int index = xml.IndexOf('<'); + if (index > 0) { xml = xml.Substring(index, xml.Length - index); } + if (!string.IsNullOrWhiteSpace(xml)) { - GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); + remoteContentDoc = XDocument.Parse(xml); + foreach (var subElement in remoteContentDoc?.Root.Elements()) + { + GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); + } } } - } - catch (Exception e) - { + catch (Exception e) + { #if DEBUG - DebugConsole.ThrowError("Reading received remote main menu content failed.", e); + DebugConsole.ThrowError("Reading received remote main menu content failed.", e); #endif - GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.RemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, - "Reading received remote main menu content failed. " + e.Message); + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.WairForRemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, + "Reading received remote main menu content failed. " + e.Message); + } + } + yield return CoroutineStatus.Success; + } + + private readonly object remoteContentLock = new object(); + private IRestResponse remoteContentResponse; + + private void RemoteContentReceived(IRestResponse response, RestRequestAsyncHandle handle) + { + lock (remoteContentLock) + { + remoteContentResponse = response; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 71646a819..478571b01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -29,19 +29,10 @@ namespace Barotrauma currentDownload = null; confirmDownload = false; } - - private void DeletePrevDownloads() - { - if (Directory.Exists(ModReceiver.DownloadFolder)) - { - Directory.Delete(ModReceiver.DownloadFolder, recursive: true); - } - } public override void Select() { base.Select(); - DeletePrevDownloads(); Reset(); Frame.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 886cf512b..2e55fe64b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -9,7 +9,11 @@ using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; +#if DEBUG +using System.IO; +#else using Barotrauma.IO; +#endif namespace Barotrauma { @@ -1258,9 +1262,7 @@ namespace Barotrauma } textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - if (ep.Category == MapEntityCategory.ItemAssembly - && ep.ContentPackage?.Files.Length == 1 - && ContentPackageManager.LocalPackages.Contains(ep.ContentPackage)) + if (ep.Category == MapEntityCategory.ItemAssembly) { var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, TextManager.Get("Delete"), style: "GUIButtonSmall") @@ -2223,7 +2225,9 @@ namespace Barotrauma // gap positions --------------------- var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), TextManager.Get("outpostmodulegappositions"), textAlignment: Alignment.CenterLeft); + var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); @@ -2267,49 +2271,6 @@ namespace Barotrauma }; gapPositionGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), TextManager.Get("canattachtoprevious"), textAlignment: Alignment.CenterLeft) - { - ToolTip = TextManager.Get("canattachtoprevious.tooltip") - }; - var canAttachToPrevDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), - text: "", selectMultiple: true); - if (outpostModuleInfo != null) - { - foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) - { - if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } - canAttachToPrevDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); - if (outpostModuleInfo.CanAttachToPrevious.HasFlag((OutpostModuleInfo.GapPosition)gapPos)) - { - canAttachToPrevDropDown.SelectItem(gapPos); - } - } - } - - canAttachToPrevDropDown.OnSelected += (_, __) => - { - if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious = OutpostModuleInfo.GapPosition.None; - if (canAttachToPrevDropDown.SelectedDataMultiple.Any()) - { - List gapPosTexts = new List(); - foreach (OutpostModuleInfo.GapPosition gapPos in canAttachToPrevDropDown.SelectedDataMultiple) - { - Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious |= gapPos; - gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString()).Value); - } - canAttachToPrevDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); - } - else - { - canAttachToPrevDropDown.Text = ToolBox.LimitString("None", canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); - } - return true; - }; - canAttachToPrevGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - - // ------------------- var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) @@ -2622,18 +2583,7 @@ namespace Barotrauma //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) if (contentPack.Files.All(f => f is SubmarineFile)) { continue; } - - if (!contentPacks.Contains(contentPack.Name)) - { - string altName = contentPack.AltNames.FirstOrDefault(n => contentPacks.Contains(n)); - if (!string.IsNullOrEmpty(altName)) - { - MainSub.Info.RequiredContentPackages.Remove(altName); - MainSub.Info.RequiredContentPackages.Add(contentPack.Name); - contentPacks.Remove(altName); - } - contentPacks.Add(contentPack.Name); - } + if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } } foreach (string contentPackageName in contentPacks) @@ -2799,7 +2749,11 @@ namespace Barotrauma } bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; +#if DEBUG + string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; +#else string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); +#endif string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); if (File.Exists(filePath)) { @@ -2828,26 +2782,26 @@ namespace Barotrauma void Save() { - ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.Regular.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); +#if DEBUG + doc.Save(filePath); +#else + doc.SaveSafe(filePath); +#endif + ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); if (existingContentPackage == null) { //content package doesn't exist, create one ModProject modProject = new ModProject() { Name = nameBox.Text }; - var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); + var newFile = ModProject.File.FromPath(filePath); modProject.AddFile(newFile); - string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); - existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); + ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); } - - XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); - doc.SaveSafe(filePath); - - var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; - if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) + else { - ContentPackageManager.EnabledPackages.EnableRegular(resultPackage); + EnqueueForReload(existingContentPackage); } - + UpdateEntityList(); } @@ -2930,7 +2884,11 @@ namespace Barotrauma { if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { - deleteBtn.Enabled = userData is SubmarineInfo subInfo && GetContentPackageIntrinsicallyTiedToSub(subInfo) != null; +#if DEBUG + deleteBtn.Enabled = true; +#else + deleteBtn.Enabled = userData is SubmarineInfo subInfo && !subInfo.IsVanillaSubmarine(); +#endif } return true; } @@ -3183,7 +3141,7 @@ namespace Barotrauma ReconstructLayers(); } - private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) + private RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) { foreach (RegularPackage regularPackage in ContentPackageManager.RegularPackages) { @@ -3213,11 +3171,10 @@ namespace Barotrauma { try { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); - ContentPackageManager.LocalPackages.Refresh(); - ContentPackageManager.EnabledPackages.DisableRemovedMods(); + Directory.Delete(Path.GetDirectoryName(subPackage.Path), true); sub.Dispose(); + File.Delete(sub.FilePath); SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index af4eba036..f8186ff57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -390,13 +390,13 @@ namespace Barotrauma } GUIComponent propertyField = null; - if (value is bool boolVal) + if (value is bool) { - propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); + propertyField = CreateBoolField(entity, property, (bool)value, displayName, toolTip); } - else if (value is string stringVal) + else if (value is string) { - propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip); + propertyField = CreateStringField(entity, property, (string)value, displayName, toolTip); } else if (value.GetType().IsEnum) { @@ -1277,7 +1277,7 @@ namespace Barotrauma public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { - var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -1307,18 +1307,6 @@ namespace Barotrauma UserData = tagTextPair.Key.ToString() }; } - - if (entity is IHasExtraTextPickerEntries hasExtraTextPickerEntries) - { - foreach (string extraEntry in hasExtraTextPickerEntries.GetExtraTextPickerEntries()) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, - ToolBox.LimitString(extraEntry, GUIStyle.Font, textList.Content.Rect.Width), GUIStyle.Green) - { - UserData = extraEntry - }; - } - } } private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) @@ -1457,12 +1445,4 @@ namespace Barotrauma } } } - - /// - /// Implement this interface to insert extra entires to the text pickers created for the SerializableEntityEditors of the entity - /// - interface IHasExtraTextPickerEntries - { - public IEnumerable GetExtraTextPickerEntries(); - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs index 978e6d971..45fa95f50 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -89,22 +89,23 @@ namespace Barotrauma.Steam return (fileCount, byteCount); } - - private void DeselectPublishedItem() - { - var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); - Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } - ? action - : null; - deselectAction?.Invoke(); - SelectTab(Tab.Publish); - } private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); ContentPackageManager.WorkshopPackages.Refresh(); + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + + void deselectItem() + { + deselectAction?.Invoke(); + SelectTab(Tab.Publish); + } + parentFrame.ClearChildren(); GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform), childAnchor: Anchor.TopCenter); @@ -145,7 +146,7 @@ namespace Barotrauma.Steam { OnClicked = (button, o) => { - DeselectPublishedItem(); + deselectItem(); return false; } }; @@ -333,7 +334,7 @@ namespace Barotrauma.Steam t => { confirmDeletion.Close(); - DeselectPublishedItem(); + deselectItem(); }); return false; }; @@ -363,10 +364,24 @@ namespace Barotrauma.Steam return false; }; - var coroutineEval = subcoroutine(messageBox.Text, messageBox).GetEnumerator(); + var coroutineEval = subcoroutine(messageBox.Text, messageBox); while (true) { - var status = coroutineEval.Current; + bool moveNext = true; + try + { + moveNext = coroutineEval.GetEnumerator().MoveNext(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); + messageBox.Close(); + } + if (!moveNext) + { + messageBox.Close(); + } + var status = coroutineEval.GetEnumerator().Current; if (messageBox.Closed) { yield return CoroutineStatus.Success; @@ -382,20 +397,6 @@ namespace Barotrauma.Steam { yield return status; } - bool moveNext = true; - try - { - moveNext = coroutineEval.MoveNext(); - } - catch (Exception e) - { - DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); - messageBox.Close(); - } - if (!moveNext) - { - messageBox.Close(); - } } } @@ -407,9 +408,26 @@ namespace Barotrauma.Steam { if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) { - SteamManager.Workshop.NukeDownload(workshopItem); + //Must download! + while (!SteamManager.Workshop.CanBeInstalled(workshopItem)) + { + bool shouldForceInstall = workshopItem.IsInstalled + && Directory.Exists(workshopItem.Directory) + && !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem); + shouldForceInstall |= workshopItem is + { IsDownloading: false, IsDownloadPending: false, IsInstalled: false }; + if (shouldForceInstall) + { + SteamManager.Workshop.ForceRedownload(workshopItem); + } + currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); + yield return new WaitForSeconds(0.5f); + } + } + else + { + SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); } - SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); TaskPool.Add($"Install {workshopItem.Title}", SteamManager.Workshop.WaitForInstall(workshopItem), (t) => @@ -418,9 +436,7 @@ namespace Barotrauma.Steam }); while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) { - currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem) - ? TextManager.Get("PublishPopupInstall") - : TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); + currentStepText.Text = TextManager.Get("PublishPopupInstall"); yield return new WaitForSeconds(0.5f); } @@ -441,6 +457,7 @@ namespace Barotrauma.Steam currentStepText.Text = TextManager.Get("PublishPopupCreateLocal"); yield return new WaitForSeconds(0.5f); } + PopulatePublishTab(workshopItem, parentFrame); yield return CoroutineStatus.Success; @@ -483,10 +500,7 @@ namespace Barotrauma.Steam editor.SubmitAsync(), t => { - if (t.TryGetResult(out Steamworks.Ugc.PublishResult publishResult)) - { - result = publishResult; - } + t.TryGetResult(out result); resultException = t.Exception?.GetInnermost(); }); currentStepText.Text = TextManager.Get("PublishPopupSubmit"); @@ -509,14 +523,6 @@ namespace Barotrauma.Steam $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); } - ContentPackage? pkgToNuke - = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); - if (pkgToNuke != null) - { - Directory.Delete(pkgToNuke.Dir, recursive: true); - ContentPackageManager.WorkshopPackages.Refresh(); - } - bool installed = false; TaskPool.Add( "InstallNewlyPublished", @@ -535,11 +541,9 @@ namespace Barotrauma.Steam { SteamWorkshopId = resultId }; - localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); ContentPackageManager.ReloadContentPackage(localPackage); ContentPackageManager.WorkshopPackages.Refresh(); - DeselectPublishedItem(); if (result.Value.NeedsWorkshopAgreement) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index f865ca3f0..fe499a4d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -207,11 +207,6 @@ namespace Barotrauma.Steam } string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; - if (File.Exists(newPath) || Directory.Exists(newPath)) - { - newPath += $"_{contentPackage.SteamWorkshopId}"; - } - if (File.Exists(newPath) || Directory.Exists(newPath)) { throw new Exception($"{newPath} already exists"); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 6a04f3da2..80a620bcc 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.3.0 - Copyright © FakeFish 2018-2022 + 0.17.2.0 + Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6d3ace4d5..20a82c153 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.3.0 - Copyright © FakeFish 2018-2022 + 0.17.2.0 + Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index dc69d29ec..35a96ffc0 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.3.0 - Copyright © FakeFish 2018-2022 + 0.17.2.0 + Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index cf1901a55..c599af014 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.3.0 - Copyright © FakeFish 2018-2022 + 0.17.2.0 + Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 4546b9a91..036a8d02c 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.3.0 - Copyright © FakeFish 2018-2022 + 0.17.2.0 + Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 2e5acdab7..49405004e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -5,7 +5,7 @@ namespace Barotrauma { partial class Character { - public static Character Controlled => null; + public static Character Controlled = null; partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 10e4452f9..7d8e7791e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -424,7 +424,7 @@ namespace Barotrauma msg.Write(owner == c && owner.Character == this); msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; - case CharacterStatusEventData _: + case StatusEventData _: WriteStatus(msg); break; case UpdateSkillsEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index b84aa42af..00190b2fd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -21,7 +21,7 @@ namespace Barotrauma } } - public void BuyBackSoldItems(List itemsToBuy) + public void BuyBackSoldItems(List itemsToBuy, Client client) { // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index d64ecfb71..fbd7d030e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -859,19 +859,19 @@ namespace Barotrauma { // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all // sold items that are removed so they should be discarded on the next message - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems)); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems), sender); CargoManager.SellItems(soldItems, sender); } else if (allowedToSellInventoryItems || allowedToSellSubItems) { if (allowedToSellInventoryItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character))); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character)), sender); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); } else { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine))); + CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine)), sender); soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); } CargoManager.SellItems(soldItems, sender); @@ -960,7 +960,7 @@ namespace Barotrauma public void ServerReadRewardDistribution(IReadMessage msg, Client sender) { - NetWalletSetSalaryUpdate update = INetSerializableStruct.Read(msg); + NetWalletSalaryUpdate update = INetSerializableStruct.Read(msg); if (!AllowedToManageCampaign(sender)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index f7db7648e..8f1783707 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -56,6 +56,7 @@ namespace Barotrauma if (containerIndex < 0) { throw error($"container index out of range ({containerIndex})"); + break; } if (!(components[containerIndex] is ItemContainer itemContainer)) { @@ -65,7 +66,7 @@ namespace Barotrauma msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case ItemStatusEventData _: + case StatusEventData _: msg.Write(condition); break; case AssignCampaignInteractionEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 9645311ce..0707bd526 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -1,13 +1,12 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using System; +using System.Xml.Linq; namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { - const float DamageUpdateInterval = 1.0f; - private float damageUpdateTimer; partial void LoadPrefab(ContentXElement element) @@ -32,38 +31,16 @@ namespace Barotrauma.MapCreatures.Behavior } } - partial void UpdateDamage(float deltaTime) - { - damageUpdateTimer -= deltaTime; - if (damageUpdateTimer > 0.0f) { return; } - - const int maxMessagesPerSecond = 10; - int messages = 0; - foreach (BallastFloraBranch branch in Branches) - { - //don't notify about minuscule amounts of damage (<= 1.0f) - if (branch.AccumulatedDamage > 1.0f) - { - CreateNetworkMessage(new BranchDamageEventData(branch)); - branch.AccumulatedDamage = 0.0f; - messages++; - //throttle a bit: if a large ballast flora is withering, it can lead to a very large number of events otherwise - if (messages > maxMessagesPerSecond) { break; } - } - } - damageUpdateTimer = DamageUpdateInterval; - } - public void ServerWrite(IWriteMessage msg, IEventData eventData) { msg.Write((byte)eventData.NetworkHeader); switch (eventData) { - case SpawnEventData _: + case SpawnEventData spawnEventData: ServerWriteSpawn(msg); break; - case KillEventData _: + case KillEventData killEventData: //do nothing break; case BranchCreateEventData branchCreateEventData: @@ -95,7 +72,6 @@ namespace Barotrauma.MapCreatures.Behavior var (x, y) = branch.Position; msg.Write(parentId); msg.Write((int)branch.ID); - msg.Write(branch.IsRootGrowth); msg.WriteRangedInteger((byte)branch.Type, 0b0000, 0b1111); msg.WriteRangedInteger((byte)branch.Sides, 0b0000, 0b1111); msg.WriteRangedInteger(branch.FlowerConfig.Serialize(), 0, 0xFFF); @@ -127,7 +103,7 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.ID); } - public void CreateNetworkMessage(IEventData extraData) + public void SendNetworkMessage(IEventData extraData) { GameMain.Server.CreateEntityEvent(Parent, new Hull.BallastFloraEventData(this, extraData)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 93a36099e..7a8d742eb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -28,11 +28,7 @@ namespace Barotrauma.Networking public static string GetCompressedModPath(ContentPackage mod) { string dir = mod.Dir; - string resultFileName - = dir.StartsWith(ContentPackage.LocalModsDir) - ? $"Local_{mod.Name}" - : $"Workshop_{mod.Name}"; - resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); + string resultFileName = dir.Replace('\\', '_').Replace('/', '_'); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 4373de52d..804c11d40 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -814,7 +814,7 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; - case ClientPacketHeader.TRANSFER_MONEY: + case ClientPacketHeader.MONEY: ReadMoneyMessage(inc, connectedClient); break; case ClientPacketHeader.REWARD_DISTRIBUTION: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 5580a12fe..334e62414 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -321,16 +321,11 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); - if (HiddenSubs.Any()) - { - UpdateFlag(NetFlags.HiddenSubs); - } SelectedSubmarine = SelectNonHiddenSubmarine(SelectedSubmarine); string[] defaultAllowedClientNameChars = - new string[] - { + new string[] { "32-33", "38-46", "48-57", diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index ba77838e6..715ef5a3a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -16,7 +16,7 @@ namespace Barotrauma Role = role; Character = character; Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.StatusEventData()); } public delegate void MessageSender(string message); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 0b4d31d6b..989383c24 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.3.0 - Copyright © FakeFish 2018-2022 + 0.17.2.0 + Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs b/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs similarity index 100% rename from Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs rename to Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 25e64fa6b..48ef25dce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -56,7 +56,7 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; - private readonly float attackLimbSelectionInterval = 3; + private readonly float attackLimbResetInterval = 2; // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. private const float minPriority = 10; @@ -65,10 +65,10 @@ namespace Barotrauma private float updateTargetsTimer; private float updateMemoriesTimer; - private float attackLimbSelectionTimer; + private float attackLimbResetTimer; - private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning; - private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0; + private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; + private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; private float Hearing => AIParams.Hearing; @@ -77,25 +77,25 @@ namespace Barotrauma private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - private Limb _attackLimb; - private Limb _previousAttackLimb; - public Limb AttackLimb + private Limb _attackingLimb; + private Limb _previousAttackingLimb; + public Limb AttackingLimb { - get { return _attackLimb; } + get { return _attackingLimb; } private set { - if (_attackLimb != value) + attackLimbResetTimer = 0; + if (_attackingLimb != value) { - _previousAttackLimb = _attackLimb; - _previousAttackLimb?.AttachedRope?.Snap(); + _previousAttackingLimb = _attackingLimb; } - else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0) + if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0) { - _attackLimb.AttachedRope?.Snap(); + SetAimTimer(); } - _attackLimb = value; + _attackingLimb = value; attackVector = null; - Reverse = _attackLimb != null && _attackLimb.attack.Reverse; + Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; } } @@ -425,8 +425,7 @@ namespace Barotrauma private void ReleaseDragTargets() { - AttackLimb?.AttachedRope?.Snap(); - if (Character.Params.CanInteract && Character.Inventory != null) + if (Character.Inventory != null) { Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); } @@ -601,7 +600,7 @@ namespace Barotrauma UpdatePatrol(deltaTime); break; case AIState.Attack: - run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack; + run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); break; case AIState.Eat: @@ -621,7 +620,7 @@ namespace Barotrauma return; } float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); + var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; @@ -876,10 +875,7 @@ namespace Barotrauma if (followLastTarget) { var target = SelectedAiTarget ?? _lastAiTarget; - if (target?.Entity != null && !target.Entity.Removed && - PreviousState == AIState.Attack && Character.CurrentHull == null && - (_previousAttackLimb?.attack == null || - _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) + if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) { // Keep heading to the last known position of the target var memory = GetTargetMemory(target, false); @@ -1130,42 +1126,31 @@ namespace Barotrauma return; } } - - attackLimbSelectionTimer -= deltaTime; - if (AttackLimb == null || attackLimbSelectionTimer <= 0) - { - attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f); - if (!IsAttackRunning && !IsCoolDownRunning) - { - AttackLimb = GetAttackLimb(attackWorldPos); - } - } bool canAttack = true; bool pursue = false; - if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) + if (IsCoolDownRunning) { - var currentAttackLimb = AttackLimb ?? _previousAttackLimb; + var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb; if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack; - switch (activeBehavior) + switch (currentAttackLimb.attack.AfterAttack) { case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - if (activeBehavior == AIBehaviorAfterAttack.Pursue) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); + UpdateFallBack(attackWorldPos, deltaTime, true); return; } } @@ -1177,13 +1162,13 @@ namespace Barotrauma if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; - if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. - UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); + UpdateFallBack(attackWorldPos, deltaTime, true); return; } - AttackLimb = null; + AttackingLimb = null; } else { @@ -1192,19 +1177,19 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackLimb = newLimb; + AttackingLimb = newLimb; } else { // No new limb was found. - if (activeBehavior == AIBehaviorAfterAttack.Pursue) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); + UpdateFallBack(attackWorldPos, deltaTime, true); return; } } @@ -1219,15 +1204,10 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: - case AIBehaviorAfterAttack.ReverseUntilCanAttack: - if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack) - { - Reverse = true; - } if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1237,7 +1217,7 @@ namespace Barotrauma // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1247,12 +1227,12 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackLimb = newLimb; + AttackingLimb = newLimb; } else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1260,7 +1240,7 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1289,7 +1269,7 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackLimb = newLimb; + AttackingLimb = newLimb; } else { @@ -1311,12 +1291,7 @@ namespace Barotrauma UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; case AIBehaviorAfterAttack.FallBack: - case AIBehaviorAfterAttack.Reverse: default: - if (activeBehavior == AIBehaviorAfterAttack.Reverse) - { - Reverse = true; - } UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); return; } @@ -1328,13 +1303,12 @@ namespace Barotrauma if (canAttack) { - if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity)) + if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable)) { - AttackLimb = GetAttackLimb(attackWorldPos); + AttackingLimb = GetAttackLimb(attackWorldPos); } - canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0; + canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; } - if (!AIParams.CanOpenDoors) { if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) @@ -1373,8 +1347,8 @@ namespace Barotrauma // Target a specific limb instead of the target center position if (wallTarget == null && targetCharacter != null) { - var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType; - attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType); + var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; + attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); if (attackTargetLimb == null) { State = AIState.Idle; @@ -1387,7 +1361,7 @@ namespace Barotrauma } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) @@ -1415,23 +1389,23 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } - float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } + float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange; + if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } - float distanceOffset = diff * AttackLimb.attack.Duration; + float distanceOffset = diff * AttackingLimb.attack.Duration; // Intentionally omit the unit conversion because we use distanceOffset as a multiplier. return targetVelocity * distanceOffset * dot; } // Check that we can reach the target distance = toTarget.Length(); - canAttack = distance < AttackLimb.attack.Range; + canAttack = distance < AttackingLimb.attack.Range; // 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) + if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2) { - if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) + if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range) { humanoidAnimController.Crouching = true; } @@ -1439,14 +1413,14 @@ namespace Barotrauma if (canAttack) { - if (AttackLimb.attack.Ranged) + if (AttackingLimb.attack.Ranged) { // Check that is facing the target - float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); float angle = VectorExtensions.Angle(forward, toTarget); - canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); - if (canAttack && AttackLimb.attack.AvoidFriendlyFire) + canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle); + if (canAttack && AttackingLimb.attack.AvoidFriendlyFire) { float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; @@ -1460,11 +1434,11 @@ namespace Barotrauma } if (canAttack) { - canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range)); + canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range)); bool IsBlocked(Vector2 targetPosition) { - foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + foreach (var body in Submarine.PickBodies(AttackingLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) { Character hitTarget = null; if (body.UserData is Character c) @@ -1486,8 +1460,22 @@ namespace Barotrauma } } } + else if (!IsAttackRunning && !IsCoolDownRunning) + { + // If not, reset the attacking limb, if the cooldown is not running + // Don't use the property, because we don't want cancel reversing, if we are reversing. + if (attackLimbResetTimer > attackLimbResetInterval) + { + _attackingLimb = null; + attackLimbResetTimer = 0; + } + else + { + attackLimbResetTimer += deltaTime; + } + } } - Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; + Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. @@ -1502,9 +1490,9 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackLimb != null && AttackLimb.attack.Retreat) + if (AttackingLimb != null && AttackingLimb.attack.Retreat) { - UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + UpdateFallBack(attackWorldPos, deltaTime, false); } else { @@ -1539,7 +1527,7 @@ namespace Barotrauma } // When pursuing, we don't want to pursue too close float max = 300; - float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; + float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max; if (!canAttack || distance > margin) { // Steer towards the target if in the same room and swimming @@ -1570,10 +1558,10 @@ namespace Barotrauma } else { - if (AttackLimb.attack.Ranged) + if (AttackingLimb.attack.Ranged) { float dir = Character.AnimController.Dir; - if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin) + if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin) { SteeringManager.Reset(); } @@ -1670,9 +1658,9 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) { - strikeTimer = AttackLimb.attack.CoolDown; + strikeTimer = AttackingLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) @@ -1715,10 +1703,10 @@ namespace Barotrauma // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; - strikeTimer = AttackLimb.attack.CoolDown; + strikeTimer = AttackingLimb.attack.CoolDown; } else { @@ -1753,9 +1741,9 @@ namespace Barotrauma } } } - if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) { - strikeTimer = AttackLimb.attack.CoolDown; + strikeTimer = AttackingLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } canAttack = false; @@ -1812,7 +1800,7 @@ namespace Barotrauma } } - if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) + if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1823,7 +1811,7 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else if (AttackLimb.attack.Ranged) + else if (AttackingLimb.attack.Ranged) { // Too close UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1836,18 +1824,18 @@ namespace Barotrauma } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) + if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); } } else if (IsAttackRunning) { - AttackLimb.attack.ResetAttackTimer(); + AttackingLimb.attack.ResetAttackTimer(); } } - private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, IDamageable target) { if (attackingLimb == null) { return false; } if (target == null) { return false; } @@ -1866,11 +1854,10 @@ namespace Barotrauma // Check that is approximately facing the target Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; - if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) { return false; } float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); - float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); - if (angle > attack.RequiredAngle) { return false; } + float angle = VectorExtensions.Angle(forward, toTarget); + if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; } } return true; } @@ -1880,7 +1867,7 @@ namespace Barotrauma private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); - Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; + IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; @@ -1914,13 +1901,12 @@ namespace Barotrauma float CalculatePriority(Limb limb, Vector2 attackPos) { - float prio = 1 + limb.attack.Priority; - if (Character.AnimController.SimplePhysicsEnabled) { return prio; } + if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. // We also need a max value that is more than the actual range. float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); - return prio * distanceFactor; + return (1 + limb.attack.Priority) * distanceFactor; } } @@ -1933,7 +1919,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } - if (attackResult.Damage >= AIParams.DamageThreshold) + if (Character.Params.CanInteract && attackResult.Damage > 10) { ReleaseDragTargets(); } @@ -2021,11 +2007,9 @@ namespace Barotrauma if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning)) { + // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; - if (IsAttackRunning) - { - avoidGunFire = false; - } + avoidGunFire = false; } if (retaliate) { @@ -2038,7 +2022,7 @@ namespace Barotrauma } } } - else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold) + else if (avoidGunFire) { State = AIState.Escape; avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); @@ -2130,7 +2114,7 @@ namespace Barotrauma { if (attackingLimb.attack.CoolDownTimer > 0) { - SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f)); + SetAimTimer(); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; if (!(damageTarget is Character)) @@ -2261,19 +2245,19 @@ namespace Barotrauma // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } - Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); - if (!MathUtils.IsValid(dir)) + Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); + if (!MathUtils.IsValid(attackDir)) { - dir = Vector2.UnitY; + attackDir = Vector2.UnitY; } - steeringManager.SteeringManual(deltaTime, dir); - if (Character.AnimController.InWater && !Reverse) + steeringManager.SteeringManual(deltaTime, attackDir); + if (Character.AnimController.InWater) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } if (checkBlocking) { - return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2)); + return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2)); } return true; } @@ -3006,7 +2990,7 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; - Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; + Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { Vector2 rayEnd = SelectedAiTarget.SimPosition; @@ -3319,7 +3303,6 @@ namespace Barotrauma foreach (var triggerObject in activeTriggers) { AITrigger trigger = triggerObject.Key; - if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) { @@ -3488,7 +3471,7 @@ namespace Barotrauma disableTailCoroutine = null; } Character.AnimController.ReleaseStuckLimbs(); - AttackLimb = null; + AttackingLimb = null; movementMargin = 0; ResetEscape(); if (isStateChanged && to == AIState.Idle && from != to) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 5dc813a8c..a98cef44d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -148,14 +148,14 @@ namespace Barotrauma } if (TargetCharacter != null) { - if (enemyAI.AttackLimb?.attack == null) + if (enemyAI.AttackingLimb?.attack == null) { DeattachFromBody(reset: true, cooldown: 1); } else { - float range = enemyAI.AttackLimb.attack.DamageRange * 2f; - if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range) + float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; + if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) { DeattachFromBody(reset: true, cooldown: 1); } @@ -265,11 +265,11 @@ namespace Barotrauma if (enemyAI.IsSteeringThroughGap) { break; } if (_attachPos == Vector2.Zero) { break; } if (!AttachToSub && !AttachToCharacters) { break; } - if (enemyAI.AttackLimb == null) { break; } + if (enemyAI.AttackingLimb == null) { break; } if (targetBody == null) { break; } if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); - if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange) + if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index e282061e2..65597452e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -99,7 +99,8 @@ namespace Barotrauma { if (InWater || !CanWalk) { - return TargetMovement.LengthSquared() > SwimSlowParams.MovementSpeed; + float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; + return TargetMovement.LengthSquared() > avg * avg; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index f898ac8d4..699f94e37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -448,18 +448,21 @@ namespace Barotrauma movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; var mainLimb = MainLimb; - float t = 0.5f; - if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) + if (isMoving) { - Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); - float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); - if (dot < 0) + float t = 0.5f; + if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { - // Reduce the linear movement speed when not facing the movement direction - t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) + { + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + } } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 30fc791e6..b386fdd83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -443,6 +443,8 @@ namespace Barotrauma if (CurrentGroundedParams == null) { return; } Vector2 handPos; + //if you're allergic to magic numbers, stop reading now + Limb leftFoot = GetLimb(LimbType.LeftFoot); Limb rightFoot = GetLimb(LimbType.RightFoot); Limb head = GetLimb(LimbType.Head); @@ -597,20 +599,16 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - if (Crouching && !movingHorizontally && !aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } + if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (!aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); } - else - { - RotateHead(head); - } if (!onGround) { @@ -885,17 +883,23 @@ namespace Barotrauma } } float targetSpeed = TargetMovement.Length(); - if (aiming) + if (targetSpeed > 0.1f) { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - torso.SimPosition) * Dir; - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + if (!aiming) + { + float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } } - else if (targetSpeed > 0.1f) + else { - float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + if (aiming) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - torso.SimPosition) * Dir; + float newRotation = MathUtils.VectorToAngle(diff); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } } torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); @@ -910,14 +914,13 @@ namespace Barotrauma { torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } - - if (!aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue) + if (HeadAngle.HasValue) { head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } else { - RotateHead(head); + head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque); } //dont try to move upwards if head is already out of water @@ -948,18 +951,7 @@ namespace Barotrauma if (isNotRemote) { - float t = movementLerp; - if (targetSpeed > 0.00001f && !SimplePhysicsEnabled) - { - Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); - float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); - if (dot < 0) - { - // Reduce the linear movement speed when not facing the movement direction - t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); - } - } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp); } WalkPos += movement.Length(); @@ -1235,11 +1227,7 @@ namespace Barotrauma //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); - if (aiming) - { - RotateHead(head); - } - else + if (!aiming) { float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); @@ -1722,24 +1710,6 @@ namespace Barotrauma } } - private void RotateHead(Limb head) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 dir = (mousePos - head.SimPosition) * Dir; - float rot = MathUtils.VectorToAngle(dir); - var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso); - if (neckJoint != null) - { - float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation); - float lowerLimit = neckJoint.LowerLimit + offset; - float upperLimit = neckJoint.UpperLimit + offset; - float min = Math.Min(lowerLimit, upperLimit); - float max = Math.Max(lowerLimit, upperLimit); - rot = Math.Clamp(rot, min, max); - } - head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque); - } - private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) { if (!MathUtils.IsValid(pos)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 9277a79ee..874e6e000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -805,7 +805,7 @@ namespace Barotrauma SeverLimbJointProjSpecific(limbJoint, playSound: true); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData()); } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 53b7ee230..ff3af7ccf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -37,9 +37,7 @@ namespace Barotrauma Pursue, FollowThrough, FollowThroughUntilCanAttack, - IdleUntilCanAttack, - Reverse, - ReverseUntilCanAttack + IdleUntilCanAttack } struct AttackResult @@ -104,7 +102,7 @@ namespace Barotrauma public bool Retreat { get; private set; } private float _range; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float Range { get => _range * RangeMultiplier; @@ -112,16 +110,13 @@ namespace Barotrauma } private float _damageRange; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] public float DamageRange { get => _damageRange * RangeMultiplier; set => _damageRange = value; } - [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] - public float MinRange { get; private set; } - [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } @@ -691,7 +686,7 @@ namespace Barotrauma public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; - public bool IsValidTarget(Entity target) + public bool IsValidTarget(IDamageable target) { return TargetType switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 4978d0f49..3f6245bc7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1885,7 +1885,7 @@ namespace Barotrauma if (!attack.IsValidContext(currentContexts)) { return false; } if (attackTarget != null) { - if (!attack.IsValidTarget(attackTarget as Entity)) { return false; } + if (!attack.IsValidTarget(attackTarget)) { return false; } if (attackTarget is ISerializableEntity se && attackTarget is Character) { if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } @@ -3148,8 +3148,7 @@ namespace Barotrauma var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } - List inventoryItems = new List(Inventory.AllItemsMod); - foreach (Item inventoryItem in inventoryItems) + foreach (Item inventoryItem in Inventory.AllItemsMod) { if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents)) { @@ -3157,25 +3156,17 @@ namespace Barotrauma inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents); } } - //this needs to happen after the items have been dropped (we can no longer sync dropping the items if the character has been removed) - Spawner.AddEntityToRemoveQueue(this); } } - else - { - Spawner.AddEntityToRemoveQueue(this); - } + + Spawner.AddEntityToRemoveQueue(this); } public void DespawnNow(bool createNetworkEvents = true) { despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); - //update twice: first to spawn the duffel bag and move the items into it, then to remove the character - for (int i = 0; i < 2; i++) - { - Spawner.Update(createNetworkEvents); - } + Spawner.Update(createNetworkEvents); } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -4021,7 +4012,7 @@ namespace Barotrauma if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); } isDead = true; @@ -4177,11 +4168,6 @@ namespace Barotrauma } DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")"); -#if CLIENT - //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns - Inventory?.ApplyReceivedState(); -#endif - base.Remove(); foreach (Item heldItem in HeldItems.ToList()) @@ -4193,12 +4179,12 @@ namespace Barotrauma #if CLIENT GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false); - - if (Controlled == this) { Controlled = null; } #endif CharacterList.Remove(this); + if (Controlled == this) { Controlled = null; } + if (Inventory != null) { foreach (Item item in Inventory.AllItems) @@ -4280,7 +4266,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - newItem.CreateStatusEvent(); + GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData()); } #if SERVER newItem.GetComponent()?.SyncHistory(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 731461475..11eab9eba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -51,7 +51,7 @@ namespace Barotrauma } } - public struct CharacterStatusEventData : IEventData + public struct StatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index df1cede68..f2bc10d8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -852,19 +852,6 @@ namespace Barotrauma #if CLIENT public void RecreateHead(MultiplayerPreferences characterSettings) { - if (characterSettings.HairIndex == -1 && - characterSettings.BeardIndex == -1 && - characterSettings.MoustacheIndex == -1 && - characterSettings.FaceAttachmentIndex == -1) - { - //randomize if nothing is set - SetAttachments(Rand.RandSync.Unsynced); - characterSettings.HairIndex = Head.HairIndex; - characterSettings.BeardIndex = Head.BeardIndex; - characterSettings.MoustacheIndex = Head.MoustacheIndex; - characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex; - } - RecreateHead( characterSettings.TagSet.ToImmutableHashSet(), characterSettings.HairIndex, @@ -872,14 +859,9 @@ namespace Barotrauma characterSettings.MoustacheIndex, characterSettings.FaceAttachmentIndex); - Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor); - Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor); - Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor); - - Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor) - { - return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced); - } + Head.SkinColor = characterSettings.SkinColor; + Head.HairColor = characterSettings.HairColor; + Head.FacialHairColor = characterSettings.FacialHairColor; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index a83def334..320d295d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -52,19 +52,17 @@ namespace Barotrauma DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names."); continue; } - var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); - if (vitalityMultipliers == null) + + Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + Identifier afflictionType = subElement.GetAttributeIdentifier("type", ""); + float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); + if (!afflictionIdentifier.IsEmpty) { - vitalityMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); - } - if (vitalityMultipliers != null) - { - float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier)); + VitalityMultipliers.Add(afflictionIdentifier, multiplier); } else { - DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); + VitalityTypeMultipliers.Add(afflictionType, multiplier); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index c430090d1..b08cafe51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -540,8 +540,6 @@ namespace Barotrauma private set; } - public Items.Components.Rope AttachedRope { get; set; } - public string Name => Params.Name; // These properties are exposed for status effects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index a1920a820..2fdb19b98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -103,9 +103,6 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } - - [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] - public bool FixedHeadAngle { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation @@ -134,7 +131,7 @@ namespace Barotrauma get => MathHelper.ToDegrees(FootAngleInRadians); set { - FootAngleInRadians = MathHelper.ToRadians(value); + FootAngleInRadians = MathHelper.ToRadians(value); } } public float FootAngleInRadians { get; private set; } @@ -159,9 +156,6 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } - - [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] - public bool FixedHeadAngle { get; set; } } public interface IHumanAnimation @@ -172,7 +166,5 @@ namespace Barotrauma float ArmMoveStrength { get; set; } float HandMoveStrength { get; set; } - - bool FixedHeadAngle { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 08cc0f106..640bf791c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -104,9 +104,6 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } - [Serialize(false, IsPropertySaveable.Yes), Editable] - public bool DrawLast { get; set; } - public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -577,9 +574,6 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } - [Serialize(0f, IsPropertySaveable.Yes, description: "How much damage is required for single attack to trigger avoiding/releasing targets."), Editable(minValue: 0f, maxValue: 1000f)] - public float DamageThreshold { get; private set; } - [Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] public float AvoidTime { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 35865bb05..73e07dcf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -30,7 +30,6 @@ namespace Barotrauma public readonly bool NotSyncedInMultiplayer; public readonly ImmutableHashSet? AlternativeTypes; public readonly ImmutableHashSet Names; - private readonly MethodInfo? contentPathMutator; public TypeInfo(Type type) { @@ -41,11 +40,9 @@ namespace Barotrauma var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; - contentPathMutator - = Type.GetMethod(nameof(MutateContentPath), BindingFlags.Static | BindingFlags.Public); HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; - if (type.GetCustomAttribute(inherit: false)?.Names is { } altNames) + if (type.GetCustomAttribute()?.Names is { } altNames) { names.UnionWith(altNames); } @@ -53,10 +50,6 @@ namespace Barotrauma Names = names.ToImmutableHashSet(); } - public ContentPath MutateContentPath(ContentPath path) - => (ContentPath?)contentPathMutator?.Invoke(null, new object[] { path }) - ?? path; - public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); } @@ -87,7 +80,6 @@ namespace Barotrauma } try { - filePath = type.MutateContentPath(filePath); if (!File.Exists(filePath.FullPath)) { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs deleted file mode 100644 index 85f6a6fa1..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Barotrauma.IO; - -namespace Barotrauma -{ - sealed class ServerExecutableFile : OtherFile - { - //This content type doesn't do very much on its own, it's handled manually by the Host Server menu - public ServerExecutableFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } - - public static ContentPath MutateContentPath(ContentPath path) - { - if (File.Exists(path.FullPath)) { return path; } - - string rawValueWithoutExtension() - => Barotrauma.IO.Path.Combine( - Barotrauma.IO.Path.GetDirectoryName(path.RawValue ?? ""), - Barotrauma.IO.Path.GetFileNameWithoutExtension(path.RawValue ?? "")).CleanUpPath(); - - path = ContentPath.FromRaw(path.ContentPackage, rawValueWithoutExtension()); - if (File.Exists(path.FullPath)) { return path; } - - path = ContentPath.FromRaw(path.ContentPackage, - rawValueWithoutExtension() + ".exe"); - return path; - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 586da6857..dae572835 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -110,7 +110,7 @@ namespace Barotrauma !expectedHash.IsNullOrWhiteSpace() && !expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase); - public IEnumerable GetFiles() where T : ContentFile => Files.OfType(); + public IEnumerable GetFiles() where T : ContentFile => Files.Where(f => f is T).Cast(); public IEnumerable GetFiles(Type type) => !type.IsSubclassOf(typeof(ContentFile)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs index 02a6f4401..33daaec39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - [AttributeUsage(AttributeTargets.Class, Inherited = false)] + [AttributeUsage(AttributeTargets.Class)] public class RequiredByCorePackage : Attribute { public readonly ImmutableHashSet AlternativeTypes; @@ -18,7 +18,7 @@ namespace Barotrauma } } - [AttributeUsage(AttributeTargets.Class, Inherited = false)] + [AttributeUsage(AttributeTargets.Class)] public class AlternativeContentTypeNames : Attribute { public readonly ImmutableHashSet Names; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index a437d78cc..bf2faed95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -327,8 +327,7 @@ namespace Barotrauma => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); public static IEnumerable AllPackages - => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages) - .OfType(); + => LocalPackages.CollectionConcat(WorkshopPackages); public static void UpdateContentPackageList() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 5274ff2e9..8562d6800 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -168,34 +168,25 @@ namespace Barotrauma }; })); - void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab - { - NewMessage("***************", Color.Cyan); - foreach (T prefab in prefabs) - { - if (prefab.Name.IsNullOrEmpty()) { continue; } - string text = $"- {prefab.Name}"; - if (prefab.Tags.Any()) - { - text += $" ({string.Join(", ", prefab.Tags)})"; - } - if (prefab.AllowedLinks?.Any() ?? false) - { - text += $", Links: {string.Join(", ", prefab.AllowedLinks)}"; - } - NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple); - } - NewMessage("***************", Color.Cyan); - } commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) => { - printMapEntityPrefabs(ItemPrefab.Prefabs); - })); - - commands.Add(new Command("itemassemblies", "itemassemblies: List all the item assemblies available for spawning.", (string[] args) => - { - printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs); + NewMessage("***************", Color.Cyan); + foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) + { + if (itemPrefab.Name.IsNullOrEmpty()) { continue; } + string text = $"- {itemPrefab.Name}"; + if (itemPrefab.Tags.Any()) + { + text += $" ({string.Join(", ", itemPrefab.Tags)})"; + } + if (itemPrefab.AllowedLinks.Any()) + { + text += $", Links: {string.Join(", ", itemPrefab.AllowedLinks)}"; + } + NewMessage(text, Color.Cyan); + } + NewMessage("***************", Color.Cyan); })); @@ -211,7 +202,6 @@ namespace Barotrauma string[] creatureAndJobNames = CharacterPrefab.Prefabs.Select(p => p.Identifier.Value) .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value)) - .OrderBy(s => s) .ToArray(); return new string[][] @@ -742,16 +732,9 @@ namespace Barotrauma { #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { return; } - - if (GameMain.Client == null) - { - Character.Controlled = null; - GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; - } - else - { - GameMain.Client?.SendConsoleCommand("freecam"); - } + Character.Controlled = null; + GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; + GameMain.Client?.SendConsoleCommand("freecam"); #endif }, isCheat: true)); @@ -2399,7 +2382,7 @@ namespace Barotrauma public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { - ThrowError(error.Value, e, createMessageBox, appendStackTrace); + ThrowError(error.Value); } public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index d7d042774..6b09ce371 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -153,13 +153,6 @@ namespace Barotrauma swarmSpawned = true; } -#if DEBUG || UNSTABLE - if (State == 1 && !level.CheckBeaconActive()) - { - DebugConsole.ThrowError("Beacon became inactive!"); - State = 2; - } -#endif } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 8a02928df..934b9df66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -248,7 +248,7 @@ namespace Barotrauma { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - int deliveredItemCount = items.Count(it => IsItemDelivered(it)); + int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { GiveReward(); @@ -267,12 +267,5 @@ namespace Barotrauma items.Clear(); failed = !completed; } - - private bool IsItemDelivered(Item item) - { - if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } - var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine; - return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 413e835e1..a525764bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -419,17 +419,17 @@ namespace Barotrauma public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) { int remainingRewards = totalReward; - float sum = GetRewardDistibutionSum(crew); - if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } - foreach (Character character in crew) + HashSet nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet(); + float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution); + if (sum == 0) { return remainingRewards; } + foreach (Character character in nonBotCrew) { - int rewardDistribution = character.Wallet.RewardDistribution; - float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; - int reward = (int)(totalReward * rewardWeight); - reward = Math.Min(remainingRewards, reward); + float rewardWeight = character.Wallet.RewardDistribution / sum; + int reward = (int)Math.Floor(totalReward * rewardWeight); + reward = Math.Max(remainingRewards, reward); character.Wallet.Give(reward); remainingRewards -= reward; - if (remainingRewards <= 0) { break; } + if (0 >= remainingRewards) { break; } } return remainingRewards; @@ -442,27 +442,26 @@ namespace Barotrauma IEnumerable characters = crewManager.GetCharacters(); #if SERVER - return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters); + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters); #elif CLIENT return characters; #endif + static bool IsAlive(Character c) { return c?.Info != null && !c.IsDead; } } - public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - - public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) + public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) { - float sum = GetRewardDistibutionSum(crew, rewardDistribution); - if (MathUtils.NearlyEqual(sum, 0)) { return (0, 0, sum); } + float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; + if (sum == 0) { return (0, 0); } - float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; + float rewardWeight = rewardDistribution / sum; int rewardPercentage = (int)(rewardWeight * 100); return reward switch { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), - None _ => (0, rewardPercentage, sum), + Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage), + None _ => (0, rewardPercentage), _ => throw new ArgumentOutOfRangeException() }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index c51ad855c..ec2aea487 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -228,7 +228,7 @@ namespace Barotrauma } GiveReward(); completed = true; - if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))) + if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))) { level.LevelData.HasHuntingGrounds = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index fcd724d2d..585de85c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -30,16 +30,22 @@ namespace Barotrauma } #if CLIENT - public PurchasedItem(ItemPrefab itemPrefab, int quantity) - : this(itemPrefab, quantity, buyer: null) { } -#endif - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null) { ItemPrefab = itemPrefab; Quantity = quantity; IsStoreComponentEnabled = null; BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; } +#elif SERVER + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) + { + ItemPrefab = itemPrefab; + Quantity = quantity; + IsStoreComponentEnabled = null; + BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0; + } +#endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index abd84ef03..d32c25575 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -25,18 +25,12 @@ namespace Barotrauma public int Balance; } - /// - /// Network message for the server to update wallet values to clients - /// internal struct NetWalletUpdate : INetSerializableStruct { [NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)] public NetWalletTransaction[] Transactions; } - /// - /// Network message for the client to transfer money between wallets - /// [NetworkSerialize] internal struct NetWalletTransfer : INetSerializableStruct { @@ -45,10 +39,7 @@ namespace Barotrauma public int Amount; } - /// - /// Network message for the client to set the salary of someone - /// - internal struct NetWalletSetSalaryUpdate : INetSerializableStruct + internal struct NetWalletSalaryUpdate : INetSerializableStruct { [NetworkSerialize] public ushort Target; @@ -57,10 +48,6 @@ namespace Barotrauma public int NewRewardDistribution; } - /// - /// Represents the difference in balance and salary when a wallet gets updated - /// Not really used right now but could be used for notifications when receiving funds similar to how talents do it - /// [NetworkSerialize] internal struct WalletChangedData : INetSerializableStruct { @@ -95,9 +82,6 @@ namespace Barotrauma } } - /// - /// Represents an update that changed the amount of money or salary of the wallet - /// [NetworkSerialize] internal struct NetWalletTransaction : INetSerializableStruct { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 8f1f97323..dbde33069 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -321,32 +321,15 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); if (!huntingGroundsMissionPrefabs.Any()) { - DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); + DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); } else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - // Adjust the prefab commonness based on the difficulty tag - var prefabs = huntingGroundsMissionPrefabs.ToList(); - var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList(); - for (int i = 0; i < prefabs.Count; i++) - { - var prefab = prefabs[i]; - var weight = weights[i]; - if (prefab.Tags.Contains("easy")) - { - weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty)); - } - else if (prefab.Tags.Contains("hard")) - { - weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty)); - } - weights[i] = weight; - } - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index e6fc1f3e8..0da770a21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -754,9 +754,9 @@ namespace Barotrauma try { - ImmutableArray crewCharacters = GetSessionCrewCharacters().ToImmutableArray(); + IEnumerable crewCharacters = GetSessionCrewCharacters(); - int prevMoney = GetAmountOfMoney(crewCharacters); + int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution foreach (Mission mission in missions) { @@ -828,7 +828,7 @@ namespace Barotrauma LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { - GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton campaignMode.TotalPlayTime += roundDuration; } #if CLIENT @@ -840,17 +840,6 @@ namespace Barotrauma { RoundEnding = false; } - - int GetAmountOfMoney(IEnumerable crew) - { - if (!(GameMode is CampaignMode campaign)) { return 0; } - - return GameMain.NetworkMember switch - { - null => campaign.Bank.Balance, - _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance - }; - } } public void LogEndRoundStats(string eventId) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index f85b6bafe..6a8df4f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -216,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.GetWallet(client).TryDeduct(price)) + if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index f8249369e..e34ef8f1b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + [Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] public bool ApplyEffectsOnDocking { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 891fc3fce..40bd8a5b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -424,15 +424,6 @@ namespace Barotrauma.Items.Components } } - public override void UpdateBroken(float deltaTime, Camera cam) - { - //update when the item is broken too to get OnContaining effects to execute and contained item positions to update - if (IsActive) - { - Update(deltaTime, cam); - } - } - public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 0ee0d9d50..b375eb205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER - hull.BallastFlora.CreateNetworkMessage(new BallastFloraBehavior.SpawnEventData()); + hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 0a0d2af0d..be83e2267 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,4 +1,5 @@ using System; +using System.Xml.Linq; using Microsoft.Xna.Framework; using System.Collections.Generic; #if CLIENT @@ -150,6 +151,20 @@ namespace Barotrauma.Items.Components } set { + if (powerIn != null) + { + if (powerIn.Grid != null) + { + powerIn.Grid.Voltage = Math.Max(0.0f, value); + } + } + else if (powerOut != null) + { + if (powerOut.Grid != null) + { + powerOut.Grid.Voltage = Math.Max(0.0f, value); + } + } voltage = Math.Max(0.0f, value); } } @@ -198,6 +213,11 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = false; } #endif + if (powerIn == null) + { + //power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic) + Voltage -= deltaTime; + } } public override void Update(float deltaTime, Camera cam) @@ -450,11 +470,6 @@ namespace Barotrauma.Items.Components //Determine if devices are adding a load or providing power, also resolve solo nodes foreach (Powered powered in poweredList) { - //Make voltage decay to ensure the device powers down. - //This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery) - //or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead. - powered.Voltage -= deltaTime; - //Handle the device if it's got a power connection if (powered.powerIn != null && powered.powerOut != powered.powerIn) { @@ -485,7 +500,7 @@ namespace Barotrauma.Items.Components } else { - powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); + powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); powered.GridResolved(powered.powerIn); } } @@ -526,7 +541,7 @@ namespace Barotrauma.Items.Components else { //Perform power calculations for the singular connection - float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); + float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); if (powered is PowerTransfer pt2) { pt2.PowerLoad = loadOut; @@ -652,7 +667,7 @@ namespace Barotrauma.Items.Components public static bool ValidPowerConnection(Connection conn1, Connection conn2) { - return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput); + return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock"))); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 37c5e5533..fcddb442b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -49,6 +49,8 @@ namespace Barotrauma.Items.Components //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; + //a duration during which the projectile won't drop from the body it's stuck to + private const float PersistentStickJointDuration = 1.0f; private PrismaticJoint stickJoint; public Attack Attack { get; private set; } @@ -84,6 +86,8 @@ namespace Barotrauma.Items.Components get { return hits; } } + private float persistentStickJointTimer; + [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } @@ -112,6 +116,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] + public bool StickPermanently + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { @@ -140,13 +151,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "")] - public bool StickToLightTargets - { - get; - set; - } - [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan @@ -208,29 +212,16 @@ namespace Barotrauma.Items.Components set; } - private float stickTimer; - [Serialize(0f, IsPropertySaveable.No)] - public float StickDuration - { - get; - set; - } - - [Serialize(-1f, IsPropertySaveable.No)] - public float MaxJointTranslation - { - get; - set; - } - private float _maxJointTranslation = -1; - public Body StickTarget { get; private set; } - public bool IsStuckToTarget => StickTarget != null; + public bool IsStuckToTarget + { + get { return StickTarget != null; } + } private Category originalCollisionCategories; private Category originalCollisionTargets; @@ -669,22 +660,23 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - if (StickDuration > 0 && stickTimer > 0) + if (persistentStickJointTimer > 0.0f && !StickPermanently) { - stickTimer -= deltaTime; + persistentStickJointTimer -= deltaTime; return; } - float absoluteMaxTranslation = 100; // Update the item's transform to make sure it's inside the same sub as the target (or outside) - if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > absoluteMaxTranslation) + if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f) { item.UpdateTransform(); } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (StickTargetRemoved() || Math.Abs(stickJoint.JointTranslation) > _maxJointTranslation) + if (StickTargetRemoved() || + (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) || + Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far { Unstick(); #if SERVER @@ -944,12 +936,14 @@ namespace Barotrauma.Items.Components { item.body.LinearVelocity *= deflectedSpeedMultiplier; } - else if ( stickJoint == null && StickTarget == null && - StickToStructures && target.Body.UserData is Structure || - ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && + else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick + //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && + hits.Count() >= MaxTargetsToHit && + target.Body.Mass > item.body.Mass * 0.5f && (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || - (StickToItems && target.Body.UserData is Item)))) + (StickToStructures && target.Body.UserData is Structure) || + (StickToItems && target.Body.UserData is Item))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), @@ -1032,8 +1026,6 @@ namespace Barotrauma.Items.Components { if (stickJoint != null) { return; } - item.body.ResetDynamics(); - stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true) { MotorEnabled = true, @@ -1042,17 +1034,18 @@ namespace Barotrauma.Items.Components Breakpoint = 1000.0f }; - if (_maxJointTranslation == -1) + if (StickPermanently) { - if (item.Sprite != null && MaxJointTranslation < 0) - { - MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale; - } - MaxJointTranslation = Math.Min(MaxJointTranslation, 1000); - _maxJointTranslation = ConvertUnits.ToSimUnits(MaxJointTranslation); + stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; + item.body.ResetDynamics(); + } + else if (item.Sprite != null) + { + stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale); + stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale); } - stickTimer = StickDuration; + persistentStickJointTimer = PersistentStickJointDuration; StickTarget = targetBody; GameMain.World.Add(stickJoint); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index d77939f02..7c5b320ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -1,9 +1,9 @@ -using Barotrauma.Extensions; -using Barotrauma.Networking; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -12,36 +12,8 @@ namespace Barotrauma.Items.Components private ISpatialEntity source; private Item target; - private Vector2? launchDir; - - private void SetSource(ISpatialEntity source) - { - this.source = source; - if (source is Limb sourceLimb) - { - sourceLimb.AttachedRope = this; - float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir); - } - } - - private void ResetSource() - { - if (source is Limb sourceLimb && sourceLimb.AttachedRope == this) - { - sourceLimb.AttachedRope = null; - } - source = null; - } - private float snapTimer; - - [Serialize(1.0f, IsPropertySaveable.No, description: "")] - public float SnapAnimDuration - { - get; - set; - } + private const float SnapAnimDuration = 1.0f; private float raycastTimer; private const float RayCastInterval = 0.2f; @@ -74,13 +46,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(360.0f, IsPropertySaveable.No, description: "How far the source item can be from the projectile until the rope breaks.")] - public float MaxAngle - { - get; - set; - } - [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] public bool SnapOnCollision { @@ -150,8 +115,8 @@ namespace Barotrauma.Items.Components { System.Diagnostics.Debug.Assert(source != null); System.Diagnostics.Debug.Assert(target != null); + this.source = source; this.target = target; - SetSource(source); Snapped = false; ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; @@ -162,7 +127,7 @@ namespace Barotrauma.Items.Components if (source == null || target == null || target.Removed || (source is Entity sourceEntity && sourceEntity.Removed)) { - ResetSource(); + source = null; target = null; IsActive = false; return; @@ -179,27 +144,12 @@ namespace Barotrauma.Items.Components } Vector2 diff = target.WorldPosition - source.WorldPosition; - float lengthSqr = diff.LengthSquared(); - if (lengthSqr > MaxLength * MaxLength) + if (diff.LengthSquared() > MaxLength * MaxLength) { Snap(); return; } - if (MaxAngle < 180 && lengthSqr > 2500) - { - if (launchDir == null) - { - launchDir = diff; - } - float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff)); - if (angle > MaxAngle) - { - Snap(); - return; - } - } - #if CLIENT item.ResetCachedVisibleSize(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 094d5a973..1e2638509 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1677,17 +1677,12 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - CreateStatusEvent(); + GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } - public void CreateStatusEvent() - { - GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); - } - private bool isActive = true; public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index e0c65a747..a4a939944 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -63,7 +63,7 @@ namespace Barotrauma } } - private readonly struct ItemStatusEventData : IEventData + private readonly struct StatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 2be6ddb93..9708fd2e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -520,9 +520,8 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; } float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount; if (branchHealAmount <= 0.0f) { continue; } - float prevHealth = branch.Health; branch.Health += branchHealAmount; - branch.AccumulatedDamage += (prevHealth - branch.Health); + branch.AccumulatedDamage -= branchHealAmount; } } StateMachine.Update(deltaTime); @@ -634,8 +633,7 @@ namespace Barotrauma.MapCreatures.Behavior { 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); - DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); + DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot); } if (branch.Health <= 0.0f) { @@ -838,7 +836,7 @@ namespace Barotrauma.MapCreatures.Behavior } #if SERVER - CreateNetworkMessage(new BranchCreateEventData(newBranch, parent)); + SendNetworkMessage(new BranchCreateEventData(newBranch, parent)); #endif return true; } @@ -880,7 +878,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); + SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); } #endif } @@ -957,6 +955,8 @@ namespace Barotrauma.MapCreatures.Behavior /// private void CreateBody(BallastFloraBranch branch) { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + Rectangle rect = branch.Rect; Vector2 pos = Parent.Position + Offset + branch.Position; @@ -975,14 +975,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) { @@ -991,28 +983,8 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } - if (type != AttackType.Other && type != AttackType.CutFromRoot) - { - branch.AccumulatedDamage += damage; - Anger += damage * 0.001f; - } - - if (GameMain.NetworkMember != null) - { - // damage is handled server side - if (GameMain.NetworkMember.IsClient) - { - return; - } - else - { - //accumulate damage on the server's side to ensure clients get notified - if (type == AttackType.Other || type == AttackType.CutFromRoot) - { - branch.AccumulatedDamage += damage; - } - } - } + // damage is handled server side currently + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (attacker != null && toxinsCooldown <= 0) { @@ -1042,6 +1014,11 @@ namespace Barotrauma.MapCreatures.Behavior } branch.Health -= damage; + if (type != AttackType.Other && type != AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + Anger += damage * 0.001f; + } #if SERVER GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); @@ -1133,7 +1110,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!wasRemoved) { - CreateNetworkMessage(new BranchRemoveEventData(branch)); + SendNetworkMessage(new BranchRemoveEventData(branch)); } #endif } @@ -1164,7 +1141,7 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); #endif } @@ -1182,7 +1159,7 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - CreateNetworkMessage(new KillEventData()); + SendNetworkMessage(new KillEventData()); #endif } @@ -1204,7 +1181,7 @@ namespace Barotrauma.MapCreatures.Behavior _entityList.Remove(this); #if SERVER - CreateNetworkMessage(new KillEventData()); + SendNetworkMessage(new KillEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 05539c6e2..13d72bc6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -160,19 +160,16 @@ namespace Barotrauma public void Delete() { Dispose(); - try + if (File.Exists(ContentFile.Path)) { - if (ContentPackage is { Files: { Length: 1 } } - && ContentPackageManager.LocalPackages.Contains(ContentPackage)) + try { - Directory.Delete(ContentPackage.Dir, recursive: true); - ContentPackageManager.LocalPackages.Refresh(); - ContentPackageManager.EnabledPackages.DisableRemovedMods(); + File.Delete(ContentFile.Path); + } + catch (Exception e) + { + DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); } - } - catch (Exception e) - { - DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index f184f08f4..42b982c38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -3826,12 +3826,12 @@ namespace Barotrauma if (location != null) { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); } else { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); } foreach (string categoryToHide in locationType.HideEntitySubcategories) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index cc0a5bd5d..4fd6d7166 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -31,16 +31,8 @@ namespace Barotrauma public bool HasHuntingGrounds, OriginallyHadHuntingGrounds; - //minimum difficulty of the level before hunting grounds can appear - public const float HuntingGroundsDifficultyThreshold = 25; - - //probability of hunting grounds appearing in 100% difficulty levels - public const float MaxHuntingGroundsProbability = 0.3f; - public OutpostGenerationParams ForceOutpostGenerationParams; - public bool AllowInvalidOutpost; - public readonly Point Size; public readonly int InitialDepth; @@ -158,7 +150,11 @@ namespace Barotrauma } else { - HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability; + //minimum difficulty of the level before hunting grounds can appear + float huntingGroundsDifficultyThreshold = 25; + //probability of hunting grounds appearing in 100% difficulty levels + float maxHuntingGroundsProbability = 0.3f; + HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); } IsBeaconActive = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 2a1bbd3bf..f7b648463 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -182,7 +182,8 @@ namespace Barotrauma { for (int i = 0; i < Connections.Count; i++) { - Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability; + float maxHuntingGroundsProbability = 0.3f; + Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability; connectionElements[i].SetAttributeValue("hashuntinggrounds", true); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 336462b62..8a92048f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -57,17 +57,17 @@ namespace Barotrauma } } - public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false) + public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false) { - return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost); + return Generate(generationParams, locationType, location: null, onlyEntrance); } - public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) + public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false) { - return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost); + return Generate(generationParams, location.Type, location, onlyEntrance); } - private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) + private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) { var outpostModuleFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -197,19 +197,12 @@ namespace Barotrauma AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); if (pendingModuleFlags.Any(flag => flag != "none")) { - if (!allowInvalidOutpost) + remainingTries--; + if (remainingTries <= 0) { - remainingTries--; - if (remainingTries <= 0) - { - DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); - } - continue; - } - else - { - DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed."); + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); } + continue; } var outpostInfo = new SubmarineInfo() @@ -335,10 +328,7 @@ namespace Barotrauma selectedModule.Offset = (selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) - selectedModule.ThisGap.WorldPosition; - if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null) - { - selectedModule.Offset += moveDir * generationParams.MinHallwayLength; - } + selectedModule.Offset += moveDir * generationParams.MinHallwayLength; } entities[selectedModule] = moduleEntities; } @@ -474,16 +464,6 @@ namespace Barotrauma pendingModuleFlags.Remove(initialModuleFlag); pendingModuleFlags.Insert(0, initialModuleFlag); - if (pendingModuleFlags.Count > totalModuleCount) - { - DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules..."); - int removeCount = pendingModuleFlags.Count - totalModuleCount; - for (int i = 0; i < removeCount; i++) - { - pendingModuleFlags.Remove(pendingModuleFlags.Last()); - } - } - return pendingModuleFlags; } @@ -506,71 +486,46 @@ namespace Barotrauma if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - for (int i = 0; i < 2; i++) + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { - //try placing a module meant for this location type first, and if that fails, try choosing whatever fits - bool allowDifferentLocationType = i > 0; - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) + if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } + if (!allowExtendBelowInitialModule) { - if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) - { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - - PlacedModule newModule = null; - //try appending to the current module if possible - if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) - { - newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); - } - - if (newModule != null) - { - placedModules.Add(newModule); - } - else - { - //couldn't append to current module, try one of the other placed modules - foreach (PlacedModule otherModule in selectedModules) - { - if (otherModule == currentModule) { continue; } - foreach (OutpostModuleInfo.GapPosition otherGapPosition in - GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) - { - newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); - if (newModule != null) - { - placedModules.Add(newModule); - break; - } - } - if (newModule != null) { break; } - } - } - if (pendingModuleFlags.Count == 0) { return true; } + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + } + if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) + { + var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType); + if (newModule != null) { placedModules.Add(newModule); } + if (pendingModuleFlags.Count == 0) { return true; } } } - //couldn't place a module anywhere, we're probably fucked! - if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) + //couldn't place anything, retry + if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule)) { + //try to append to some other module first + foreach (PlacedModule otherModule in selectedModules) + { + if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) + { + return true; + } + } //try to replace the previously placed module with something else that we can append to + var failedModule = currentModule; for (int i = 0; i < 10; i++) { selectedModules.Remove(currentModule); - assertAllPreviousModulesPresent(); //readd the module types that the previous module was supposed to fulfill to the pending module types pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes); if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); } //retry - currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true); - assertAllPreviousModulesPresent(); + currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType); if (currentModule == null) { break; } if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { - assertAllPreviousModulesPresent(); return true; } } @@ -579,14 +534,9 @@ namespace Barotrauma foreach (PlacedModule placedModule in placedModules) { - AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule); + AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType); } return placedModules.Count > 0; - - void assertAllPreviousModulesPresent() - { - System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); - } } /// @@ -603,8 +553,7 @@ namespace Barotrauma List availableModules, List pendingModuleFlags, List selectedModules, - LocationType locationType, - bool allowDifferentLocationType) + LocationType locationType) { if (pendingModuleFlags.Count == 0) { return null; } @@ -613,7 +562,7 @@ namespace Barotrauma foreach (Identifier moduleFlag in pendingModuleFlags) { flagToPlace = moduleFlag; - nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType); + nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType); if (nextModule != null) { break; } } @@ -654,7 +603,6 @@ namespace Barotrauma foreach (PlacedModule otherModule in modules2) { if (module == otherModule) { continue; } - if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; } if (ModulesOverlap(module, otherModule)) { module1 = module; @@ -827,25 +775,22 @@ namespace Barotrauma } } - private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) + private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) { IEnumerable availableModules = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { availableModules = modules - .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); + .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); } else { availableModules = modules - .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); + .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); } - - availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); - if (prevModule != null) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); + availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo)); } if (availableModules.Count() == 0) { return null; } @@ -855,22 +800,15 @@ namespace Barotrauma availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type - if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) + if (!modulesSuitableForLocationType.Any()) { modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); } if (!modulesSuitableForLocationType.Any()) { - if (allowDifferentLocationType) - { - 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); - } - else - { - return null; - } + 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); } else { @@ -1101,20 +1039,16 @@ namespace Barotrauma if (hallwayLength <= 1.0f) { continue; } - Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(); - var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); - - var suitableHallwayModules = hallwayModules.Where(m => - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); - if (suitableHallwayModules.Count() == 0) + var suitableModules = availableModules.Where(m => + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); + if (suitableModules.Count() == 0) { - suitableHallwayModules = hallwayModules.Where(m => + suitableModules = availableModules.Where(m => !m.OutpostModuleInfo.AllowAttachToModules.Any() || m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")); } - - var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType); + var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType); if (hallwayInfo == null) { DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\"."); @@ -1236,7 +1170,7 @@ namespace Barotrauma var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap); if (startWaypoint == null) { - DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); + DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); continue; } var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 794a9671b..c79a10a64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -45,9 +45,6 @@ namespace Barotrauma [Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] public GapPosition GapPositions { get; set; } - [Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")] - public GapPosition CanAttachToPrevious { get; set; } - public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index 1fb8afbae..ae5e402d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -48,11 +48,11 @@ namespace Barotrauma { if (!subs.Any()) yield return CoroutineStatus.Success; -#if CLIENT Character.Controlled = null; + cam.TargetPos = Vector2.Zero; +#if CLIENT GameMain.LightManager.LosEnabled = false; #endif - cam.TargetPos = Vector2.Zero; Level.Loaded.TopBarrier.Enabled = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index acdee62e9..84bf74588 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -322,7 +322,7 @@ namespace Barotrauma #endif Tags = tags.ToImmutableHashSet(); - AllowedLinks = ImmutableHashSet.Empty; + AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); } protected override void CreateInstance(Rectangle rect) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index e405ecb4f..90f123c95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic - TRANSFER_MONEY, // wallet transfers + MONEY, //wallet updates REWARD_DISTRIBUTION, // wallet reward distribution READY_CHECK, READY_TO_SPAWN diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 89c8bc1bd..dacc2c8a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -172,11 +172,11 @@ namespace Barotrauma return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase); } - public static ContentPath GetAttributeContentPath(this XElement element, string name, ContentPackage contentPackage) + public static ContentPath GetAttributeContentPath(this XElement element, string name, + ContentPackage contentPackage) { - var attribute = element?.GetAttribute(name); - if (attribute == null) { return null; } - return ContentPath.FromRaw(contentPackage, GetAttributeString(attribute, null)); + if (element?.GetAttribute(name) == null) { return null; } + return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null)); } public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 551b48fdc..b917b844a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -10,7 +10,6 @@ using System.Xml.Linq; using Barotrauma.IO; #if CLIENT using Barotrauma.ClientSource.Settings; -using Barotrauma.Networking; using Microsoft.Xna.Framework.Input; #endif @@ -454,12 +453,8 @@ namespace Barotrauma 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); - currentConfig = newConfig; +#warning TODO: Implement program state updates; #if CLIENT if (setGraphicsMode) @@ -467,24 +462,6 @@ namespace Barotrauma GameMain.Instance.ApplyGraphicsSettings(); } - if (audioOutputChanged) - { - GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice); - } - - if (voiceCaptureChanged) - { - VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); - } - - if (textScaleChanged) - { - foreach (var font in GUIStyle.Fonts.Values) - { - font.Prefabs.ForEach(p => p.LoadFont()); - } - } - GameMain.SoundManager?.ApplySettings(); #endif if (languageChanged) { TextManager.ClearCache(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 2bdef6f7b..688ebbfbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -68,21 +68,15 @@ namespace Barotrauma public bool IsTriggered { get; private set; } - public float Timer { get; private set; } + public float Timer { get; private set; } = -1; public bool IsActive { get; private set; } - public bool IsPermanent { get; private set; } - public void Launch() { IsTriggered = true; IsActive = true; - IsPermanent = Duration <= 0; - if (!IsPermanent) - { - Timer = Duration; - } + Timer = Duration; } public void Reset() @@ -94,7 +88,6 @@ namespace Barotrauma public void UpdateTimer(float deltaTime) { - if (IsPermanent) { return; } Timer -= deltaTime; if (Timer < 0) { @@ -1250,27 +1243,24 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - var target = targets[i]; - Limb targetLimb = target as Limb; - if (targetLimb == null && target is Character character) + if (targets[i] is Character character) { foreach (Limb limb in character.AnimController.Limbs) { if (limb.body == sourceBody) { - targetLimb = limb; if (breakLimb) { character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); } + else + { + limb.HideAndDisable(hideLimbTimer); + } break; } } } - if (hideLimb) - { - targetLimb?.HideAndDisable(hideLimbTimer); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 40f135e26..fefee9d61 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -2,19 +2,19 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; namespace Barotrauma.IO { static class Validation { - private static readonly ImmutableArray unwritableDirs = new[] { "Content".ToIdentifier() }.ToImmutableArray(); - private static readonly ImmutableArray unwritableExtensions = new[] + private static readonly string[] unwritableDirs = new string[] { "Content" }; + private static readonly string[] unwritableExtensions = new string[] { - ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe, .dll and .json handled separately in CanWrite) + ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite) ".bat", ".sh", //shell scripts - }.ToIdentifiers().ToImmutableArray(); + ".json" //deps.json + }; /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. @@ -24,27 +24,25 @@ namespace Barotrauma.IO public static bool CanWrite(string path, bool isDirectory) { path = System.IO.Path.GetFullPath(path).CleanUpPath(); - string localModsDir = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); - string workshopModsDir = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); if (!isDirectory) { - Identifier extension = System.IO.Path.GetExtension(path).Replace(" ", "").ToIdentifier(); - if (unwritableExtensions.Any(e => e == extension)) + string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); + if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) { return false; } - if (!path.StartsWith(workshopModsDir, StringComparison.OrdinalIgnoreCase) - && !path.StartsWith(localModsDir, StringComparison.OrdinalIgnoreCase) - && (extension == ".dll" || extension == ".exe" || extension == ".json")) + if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) + && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) { return false; } } - foreach (var unwritableDir in unwritableDirs) + foreach (string unwritableDir in unwritableDirs) { - string dir = System.IO.Path.GetFullPath(unwritableDir.Value).CleanUpPath(); + string dir = System.IO.Path.GetFullPath(unwritableDir).CleanUpPath(); if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 32a3fb494..f5663e4fd 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,62 +1,3 @@ ---------------------------------------------------------------------------------------------------------- -v0.17.3.0 ---------------------------------------------------------------------------------------------------------- - -Changes: -- A new abyss monster (placeholder art and sounds, name pending) -- Overhauled colonies: completely new modules, improved layouts, new structures and items, new events. -- Adjustments to reactors and supercapacitors to prevent the increased supercapacitor loads from crippling the subs on default recharge rates: slightly increased Humpback reactor output and decreased the default recharge rate of the capacitors, reduced recharge rates in the 3 new subs and set the supercapacitor efficiency to match the rest of the subs. -- Throw an error in debug and unstable builds if beacon station becomes inactive after it's been activated (hopefully helps us diagnose why beacon missions sometimes fail after the beacon's been activated). -- Reimplemented ServerExecutable to be usable in non-core packages. Now, players must select a server executable from a dropdown in the "Host server" menu if multiple are available. -- Animation adjustment: The head now rotates towards the mouse cursor while aiming or swimming. -- Swim animation adjustment: The body now rotates towards the aim target also when the character is moving, and not only while staying still. Moving while not facing the movement direction results in reduced movement speed. -- Set the bottom hole probability to 0 in Cold Caverns, which reduces the size and the frequency of holes in the level bottom. -- Adjusted the probabilities for spawning the Thalamus in the wrecks. -- Added a (WIP) difficulty hierarchy for the abyss monsters. Easier monsters should spawn more frequently on an easier difficulty level, the harder should spawn more frequently on higher difficulty levels. Currently the new abyss monster is defined as the easiest, and Endworm the hardest. Charybdis is in between. - -Fixes: -- Re-filled Typhon 2 oxygen tank shelves. -- Fixed spawnpoint editing panel being too small on large resolutions. -- Fixed inability to equip one-handed items when there's a suitable container in the other hand (e.g. flashlight when there's a storage container in the other hand). -- Cargo missions don't require the cargo to be inside a hull: being in the sub is enough. Fixes inability to complete cargo missions with unconventional subs where the cargo is stored outside hulls. -- Fixed non-equipped items that can't be put into a duffel bag disappearing when a character despawns. -- Fixed incorrect animation parameters being used for swimming while wearing a regular diving suit. -- Fixed projectiles sometimes staying attached to the target even when they are far from it. -- Fixed monsters sometimes trying to follow targets after losing the track of them even when they should be falling back from them (according to the after attack behavior). -- Fixed monsters sometimes using the after attack behavior of the current attack even when the cooldown of that attack is not active. -- Fixed monsters sometimes being unable to target the submarine, because their attack was incorrectly considered invalid. -- Fixed fractal guardians fleeing to a shelter immediatedly after taking some damage when they have targeted the guardian pod once and have not changed the target yet (e.g. if you shoot a guardian that is returning from the pod and if it has not yet spotted you). - -Fixes (unstable only): -- Fixed text scale slider not working. -- Fixed audio capture and output settings changes not being applied until the game was restarted. -- Fixed numerous circumstances where the Publish tab could cause a crash or softlock. -- Fixed creating and deleting item assemblies in the submarine editor. -- Fixed deleting submarines in the submarine editor. -- Trimmed down the filenames of mods transferred from the server to the clients. -- Fixed ballast flora's damage visualizations (particles, branches shaking, healthbar) not working in multiplayer (unstable only). -- Fixed occasional "invalid SetAttackTarget/ExecuteAttack" errors in multiplayer (unstable only). -- Fixed clients not taking control off the previous character properly when using freecam, preventing the character from moving if another player or AI takes control of it (unstable only). -- Fixed "event data was of the wrong type" error when characters spawn with items with depleted condition in their inventory (unstable only). -- Fixed incorrect power displayed on the reactor when unwired (unstable only). -- Fixed devices not powering down if they're disconnected from the grid when their voltage has been set above 0. Could be reproduced by powering up the sub in mp campaign, entering a new level and disconnecting e.g. the oxygen generator from the grid (unstable). -- Fixed hidden subs resetting client-side when restarting a server (unstable only). -- Randomize character appearance in server lobby if it hasn't been set (= when launching the game or v0.17 for the first time). Unstable only. - -Modding: -- ItemContainers apply the OnContaining effects even when the item is broken. Doesn't affect any vanilla items. -- Ropes attached to limbs now automatically snap when another attack is chosen. -- Ropes can now be set to break from the end instead of always breaking from the middle (see the new abyss monster for an example). -- Ropes can be set to break if they are in too steep angle to the target. -- Projectiles always stick permanently unless a stick duration is defined. -- Characters (with deformable sprites) can be set to be drawn after (on top of) other characters. Normally characters are drawn in the order of spawning. -- AI Triggers can now be permanent. -- Added a generic damage threshold that currently defines how much damage the character needs to take from a single hit to hit the avoiding and releasing captured targets. -- Added a support for multiple identifiers and types in the limb health definitions. -- Added a support for min range for ranged attacks. -- Fixed monsters not being able to shoot faster than every ~1.5 second if they change the attacking limb. -- Added new after attack behaviors: Reverse and ReverseUntilCanAttack. - --------------------------------------------------------------------------------------------------------- v0.17.2.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt index a2c5918d6..f29df7c55 100644 --- a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt +++ b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt @@ -1,4 +1,4 @@ -Copyright (c) 2019 FakeFish Ltd. +Copyright (c) 2019 FakeFish Games Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/README.md b/README.md index 7943f78d2..99597222e 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,6 @@ If you're interested in working on the code, either to develop mods or to contri ### Windows - [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 8.0 support (VS 2019 or later recommended) ### Linux -- [.NET Core 3.1 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) +- [.NET Core 3.0 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) ### macOS - [Visual Studio 2019 for Mac](https://visualstudio.microsoft.com/vs/mac/) From c1b8e5a3417c3b1f572bc503a4c570cb4ed8315f Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Wed, 30 Mar 2022 00:08:09 +0900 Subject: [PATCH 6/9] Unstable 0.17.4.0 --- .../ClientSource}/CameraTransition.cs | 0 .../Characters/AI/EnemyAIController.cs | 2 +- .../ClientSource/Characters/Attack.cs | 2 +- .../Characters/CharacterNetworking.cs | 21 +- .../Characters/Health/CharacterHealth.cs | 2 +- .../ContentPackageManager.cs | 24 +- .../ClientSource/DebugConsole.cs | 24 +- .../ClientSource/GUI/ComponentStyle.cs | 2 +- .../ClientSource/GUI/CrewManagement.cs | 8 +- .../ClientSource/GUI/GUIComponent.cs | 8 +- .../ClientSource/GUI/GUIContextMenu.cs | 4 +- .../ClientSource/GUI/GUIDropDown.cs | 5 +- .../ClientSource/GUI/GUILayoutGroup.cs | 4 - .../ClientSource/GUI/GUIListBox.cs | 17 +- .../ClientSource/GUI/GUIPrefab.cs | 2 +- .../ClientSource/GUI/MedicalClinicUI.cs | 14 +- .../ClientSource/GUI/Store.cs | 307 +++--- .../ClientSource/GUI/SubmarineSelection.cs | 19 +- .../ClientSource/GUI/TabMenu.cs | 69 +- .../ClientSource/GUI/UpgradeStore.cs | 13 +- .../BarotraumaClient/ClientSource/GameMain.cs | 4 +- .../ClientSource/GameSession/CargoManager.cs | 101 +- .../GameSession/GameModes/CampaignMode.cs | 6 +- .../GameModes/MultiPlayerCampaign.cs | 96 +- .../GameModes/Tutorials/EngineerTutorial.cs | 2 +- .../GameModes/Tutorials/MechanicTutorial.cs | 2 +- .../GameModes/Tutorials/OfficerTutorial.cs | 2 +- .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../ClientSource/Items/CharacterInventory.cs | 2 +- .../Items/Components/Holdable/IdCard.cs | 2 +- .../Items/Components/ItemComponent.cs | 2 +- .../Items/Components/ItemContainer.cs | 7 +- .../Items/Components/ItemLabel.cs | 67 +- .../Items/Components/Machines/Fabricator.cs | 122 ++- .../ClientSource/Items/Components/Rope.cs | 20 +- .../ClientSource/Items/Inventory.cs | 2 +- .../ClientSource/Items/Item.cs | 4 +- .../ClientSource/Items/ItemPrefab.cs | 2 +- .../Map/Creatures/BallastFloraBehavior.cs | 4 +- .../ClientSource/Map/Map/Map.cs | 6 +- .../ClientSource/Map/WayPoint.cs | 12 +- .../Networking/FileTransfer/FileReceiver.cs | 21 +- .../Networking/Voip/VoipCapture.cs | 13 +- .../Networking/Voip/VoipClient.cs | 14 +- .../ClientSource/Particles/ParticlePrefab.cs | 10 +- .../ClientSource/Screens/CampaignUI.cs | 5 +- .../CharacterEditor/CharacterEditorScreen.cs | 3 +- .../ClientSource/Screens/GameScreen.cs | 24 +- .../ClientSource/Screens/LevelEditorScreen.cs | 10 +- .../ClientSource/Screens/MainMenuScreen.cs | 158 ++-- .../ClientSource/Screens/ModDownloadScreen.cs | 24 +- .../ClientSource/Screens/NetLobbyScreen.cs | 2 +- .../Screens/SpriteEditorScreen.cs | 2 +- .../ClientSource/Screens/SubEditorScreen.cs | 149 ++- .../Serialization/SerializableEntityEditor.cs | 30 +- .../StatusEffects/StatusEffect.cs | 2 +- .../ClientSource/Steam/ItemList.cs | 78 +- .../ClientSource/Steam/PublishTab.cs | 105 +- .../ClientSource/Steam/Workshop.cs | 24 +- .../ClientSource/Steam/WorkshopMenu.cs | 44 +- .../BarotraumaClient/LinuxClient.csproj | 6 +- Barotrauma/BarotraumaClient/MacClient.csproj | 6 +- .../BarotraumaClient/WindowsClient.csproj | 6 +- .../BarotraumaServer/LinuxServer.csproj | 4 +- Barotrauma/BarotraumaServer/MacServer.csproj | 4 +- .../ServerSource/Characters/Character.cs | 2 +- .../Characters/CharacterNetworking.cs | 6 +- .../ServerSource/GameSession/CargoManager.cs | 48 +- .../GameModes/MultiPlayerCampaign.cs | 168 ++-- .../Items/Components/Projectile.cs | 4 +- .../ServerSource/Items/Item.cs | 3 +- .../Map/Creatures/BallastFloraBehavior.cs | 32 +- .../Networking/FileTransfer/ModSender.cs | 6 +- .../ServerSource/Networking/GameServer.cs | 2 +- .../ServerSource/Networking/ServerSettings.cs | 7 +- .../ServerSource/Traitors/Traitor.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 4 +- .../LocalMods/PowerTestSub/PowerTestSub.sub | Bin 9564 -> 0 bytes .../LocalMods/PowerTestSub/filelist.xml | 4 - .../Characters/AI/EnemyAIController.cs | 227 +++-- .../SharedSource/Characters/AI/LatchOntoAI.cs | 10 +- .../Objectives/AIObjectiveExtinguishFire.cs | 9 +- .../AI/Objectives/AIObjectiveFixLeak.cs | 2 +- .../Characters/Animation/AnimController.cs | 3 +- .../Animation/FishAnimController.cs | 19 +- .../Animation/HumanoidAnimController.cs | 74 +- .../Characters/Animation/Ragdoll.cs | 2 +- .../SharedSource/Characters/Attack.cs | 27 +- .../SharedSource/Characters/Character.cs | 38 +- .../Characters/CharacterEventData.cs | 2 +- .../SharedSource/Characters/CharacterInfo.cs | 24 +- .../Health/Afflictions/AfflictionPrefab.cs | 4 +- .../Characters/Health/CharacterHealth.cs | 18 +- .../SharedSource/Characters/Limb.cs | 2 + .../Params/Animation/HumanoidAnimations.cs | 10 +- .../Characters/Params/CharacterParams.cs | 50 +- .../AbilityConditionItemInSubmarine.cs | 2 +- .../AbilityConditionLocation.cs | 2 +- .../AbilityConditionInHull.cs | 5 +- .../AbilityConditionInWater.cs | 5 +- .../Characters/Talents/TalentPrefab.cs | 2 +- .../ContentFile/ContentFile.cs | 10 +- .../ContentFile/ServerExecutableFile.cs | 28 + .../ContentPackage/ContentPackage.cs | 2 +- .../ContentPackage/CorePackage.cs | 4 +- .../ContentPackageManager.cs | 27 +- .../ContentManagement/ContentPath.cs | 2 +- .../ContentManagement/ContentXElement.cs | 2 - .../MissingContentPackageException.cs | 2 +- .../ContentManagement/ModProject.cs | 0 .../SharedSource/DebugConsole.cs | 86 +- .../SharedSource/Events/ArtifactEvent.cs | 2 +- .../Events/EventActions/EventAction.cs | 2 +- .../Events/Missions/BeaconMission.cs | 7 + .../Events/Missions/CargoMission.cs | 9 +- .../SharedSource/Events/Missions/Mission.cs | 33 +- .../Events/Missions/MissionPrefab.cs | 2 +- .../Events/Missions/MonsterMission.cs | 2 +- .../Events/Missions/SalvageMission.cs | 2 +- .../SharedSource/GameSession/CargoManager.cs | 198 ++-- .../SharedSource/GameSession/Data/Wallet.cs | 18 +- .../GameSession/GameModes/CampaignMode.cs | 40 +- .../GameModes/MultiPlayerCampaign.cs | 75 +- .../SharedSource/GameSession/GameSession.cs | 17 +- .../GameSession/UpgradeManager.cs | 2 +- .../Items/Components/DockingPort.cs | 2 +- .../Items/Components/Holdable/Holdable.cs | 2 +- .../Items/Components/Holdable/RepairTool.cs | 6 +- .../Items/Components/ItemComponent.cs | 2 +- .../Items/Components/ItemContainer.cs | 9 + .../Items/Components/Machines/Controller.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 29 +- .../Items/Components/Machines/Pump.cs | 2 +- .../Items/Components/Power/Powered.cs | 31 +- .../Items/Components/Projectile.cs | 121 ++- .../SharedSource/Items/Components/Rope.cs | 62 +- .../Items/Components/Signal/Connection.cs | 2 +- .../Components/Signal/CustomInterface.cs | 2 +- .../Items/Components/Signal/MotionSensor.cs | 2 +- .../SharedSource/Items/Components/Wearable.cs | 2 +- .../SharedSource/Items/Item.cs | 14 +- .../SharedSource/Items/ItemEventData.cs | 2 +- .../SharedSource/Items/ItemPrefab.cs | 244 ++--- .../SharedSource/Items/RelatedItem.cs | 2 +- .../Map/Creatures/BallastFloraBehavior.cs | 57 +- .../SharedSource/Map/Explosion.cs | 2 +- .../BarotraumaShared/SharedSource/Map/Gap.cs | 12 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 12 +- .../SharedSource/Map/ItemAssemblyPrefab.cs | 17 +- .../SharedSource/Map/Levels/Level.cs | 4 +- .../SharedSource/Map/Levels/LevelData.cs | 18 +- .../Map/Levels/LevelObjects/LevelTrigger.cs | 2 +- .../SharedSource/Map/Map/Location.cs | 895 ++++++++++-------- .../SharedSource/Map/Map/Map.cs | 33 +- .../SharedSource/Map/Md5Hash.cs | 17 +- .../Map/Outposts/OutpostGenerationParams.cs | 22 + .../Map/Outposts/OutpostGenerator.cs | 166 +++- .../Map/Outposts/OutpostModuleInfo.cs | 3 + .../SharedSource/Map/PriceInfo.cs | 102 +- .../SharedSource/Map/RoundEndCinematic.cs | 4 +- .../SharedSource/Map/Structure.cs | 6 +- .../SharedSource/Map/StructurePrefab.cs | 18 +- .../SharedSource/Map/Submarine.cs | 4 +- .../SharedSource/Map/SubmarineInfo.cs | 4 +- .../SharedSource/Map/WayPoint.cs | 8 +- .../SharedSource/Networking/NetworkMember.cs | 2 +- .../SharedSource/Networking/ServerSettings.cs | 37 +- .../Serialization/XMLExtensions.cs | 8 +- .../SharedSource/Settings/GameSettings.cs | 25 +- .../StatusEffects/PropertyConditional.cs | 2 + .../StatusEffects/StatusEffect.cs | 30 +- .../SharedSource/Steam/Workshop.cs | 25 +- .../SharedSource/Text/TextManager.cs | 6 + .../SharedSource/Utils/SafeIO.cs | 26 +- Barotrauma/BarotraumaShared/changelog.txt | 101 ++ .../LICENSE_webm_mem_playback.txt | 2 +- README.md | 2 +- 177 files changed, 3388 insertions(+), 1977 deletions(-) rename Barotrauma/{BarotraumaShared/SharedSource => BarotraumaClient/ClientSource}/CameraTransition.cs (100%) delete mode 100644 Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub delete mode 100644 Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/filelist.xml create mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/CameraTransition.cs rename to Barotrauma/BarotraumaClient/ClientSource/CameraTransition.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 9b31bf9bf..bfb8b7202 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -39,7 +39,7 @@ namespace Barotrauma targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUIStyle.Red * 0.5f, 0, 4); - if (wallTarget != null) + if (wallTarget != null && !IsCoolDownRunning) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 2c7bc106e..890be7cb1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -15,7 +15,7 @@ namespace Barotrauma partial void InitProjSpecific(ContentXElement element) { - if (element.Attribute("sound") != null) + if (element.GetAttribute("sound") != null) { DebugConsole.ThrowError("Error in attack ("+element+") - sounds should be defined as child elements, not as attributes."); return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 22d741e27..72fba4aaf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -152,7 +152,7 @@ namespace Barotrauma case TreatmentEventData _: msg.Write(AnimController.Anim == AnimController.Animation.CPR); break; - case StatusEventData _: + case CharacterStatusEventData _: //do nothing break; case UpdateTalentsEventData _: @@ -343,8 +343,12 @@ namespace Barotrauma if (controlled == this) { Controlled = null; - IsRemotePlayer = ownerID > 0; } + if (GameMain.Client?.Character == this) + { + GameMain.Client.Character = null; + } + IsRemotePlayer = ownerID > 0; } break; case EventType.Status: @@ -371,7 +375,9 @@ namespace Barotrauma if (attackLimbIndex == 255 || Removed) { break; } if (attackLimbIndex >= AnimController.Limbs.Length) { - DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"); + string errorMsg = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Limb index out of bounds (character: {Name}, limb index: {attackLimbIndex}, limb count: {AnimController.Limbs.Length})"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:AttackLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); break; } Limb attackLimb = AnimController.Limbs[attackLimbIndex]; @@ -380,13 +386,16 @@ namespace Barotrauma if (targetEntity == null && eventType == EventType.SetAttackTarget) { DebugConsole.ThrowError($"Received invalid SetAttackTarget message. Target entity not found (ID {targetEntityID})"); + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetNotFound", GameAnalyticsManager.ErrorSeverity.Error, "Received invalid SetAttackTarget message. Target entity not found."); break; } - if (targetEntity is Character targetCharacter) + if (targetEntity is Character targetCharacter && targetLimbIndex != 255) { if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) { DebugConsole.ThrowError($"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.Name}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"); + string errorMsgWithoutName = $"Received invalid {(eventType == EventType.SetAttackTarget ? "SetAttackTarget" : "ExecuteAttack")} message. Target limb index out of bounds (target character: {targetCharacter.SpeciesName}, limb index: {targetLimbIndex}, limb count: {targetCharacter.AnimController.Limbs.Length})"; + GameAnalyticsManager.AddErrorEventOnce("Character.ClientEventRead:TargetLimbOutOfBounds", GameAnalyticsManager.ErrorSeverity.Error, errorMsgWithoutName); break; } targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; @@ -560,6 +569,10 @@ namespace Barotrauma } character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); + if (character.CampaignInteractionType == CampaignMode.InteractionType.Store) + { + character.MerchantIdentifier = inc.ReadIdentifier(); + } 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 b61c76669..436e47141 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -400,7 +400,7 @@ namespace Barotrauma { if (GameMain.Client != null) { - GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.StatusEventData()); + GameMain.Client.CreateEntityEvent(Character.Controlled, new Character.CharacterStatusEventData()); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs index 3ae094982..86a971bf5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/ContentPackageManager.cs @@ -12,22 +12,30 @@ namespace Barotrauma { public sealed partial class PackageSource : ICollection { - public ContentPackage SaveAndEnableRegularMod(ModProject modProject) + public string SaveRegularMod(ModProject modProject) { if (modProject.IsCore) { throw new ArgumentException("ModProject must not be a core package"); } - - //save the content package + string fileListPath = Path.Combine(directory, ToolBox.RemoveInvalidFileNameChars(modProject.Name), ContentPackage.FileListFileName) .CleanUpPathCrossPlatform(correctFilenameCase: false); - Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)!); modProject.Save(fileListPath); Refresh(); EnabledPackages.DisableRemovedMods(); - var newPackage = Regular.First(p => p.Path == fileListPath); - //enable it - EnabledPackages.EnableRegular(newPackage); + return fileListPath; + } - return newPackage; + public RegularPackage GetRegularModByPath(string fileListPath) + { + return Regular.First(p => p.Path == fileListPath); + } + + public RegularPackage SaveAndEnableRegularMod(ModProject modProject) + { + string fileListPath = SaveRegularMod(modProject); + var package = GetRegularModByPath(fileListPath); + EnabledPackages.EnableRegular(package); + + return package; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 4f4d110e3..f653a1a83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1454,9 +1454,9 @@ namespace Barotrauma // omega nesting incoming if (fabricationRecipe != null) { - foreach (KeyValuePair itemLocationPrice in itemPrefab.GetSellPricesOver(0)) + foreach (var priceInfo in itemPrefab.GetSellPricesOver(0)) { - NewMessage(" If bought at " + itemLocationPrice.Key + " it costs " + itemLocationPrice.Value.Price); + NewMessage($" If bought at {GetSeller(priceInfo.Value)} it costs {priceInfo.Value.Price}"); int totalPrice = 0; int? totalBestPrice = 0; foreach (var ingredient in fabricationRecipe.RequiredItems) @@ -1464,31 +1464,33 @@ namespace Barotrauma foreach (ItemPrefab ingredientItemPrefab in ingredient.ItemPrefabs) { int defaultPrice = ingredientItemPrefab.DefaultPrice?.Price ?? 0; - NewMessage(" Its ingredient " + ingredientItemPrefab.Name + " has base cost " + defaultPrice); + NewMessage($" Its ingredient {ingredientItemPrefab.Name} has base cost {defaultPrice}"); totalPrice += defaultPrice; totalBestPrice += ingredientItemPrefab.GetMinPrice(); int basePrice = defaultPrice; - foreach (KeyValuePair ingredientItemLocationPrice in ingredientItemPrefab.GetBuyPricesUnder()) + foreach (var ingredientItemPriceInfo in ingredientItemPrefab.GetBuyPricesUnder()) { - if (basePrice > ingredientItemLocationPrice.Value.Price) + if (basePrice > ingredientItemPriceInfo.Value.Price) { - NewMessage(" Location " + ingredientItemLocationPrice.Key + " sells ingredient " + ingredientItemPrefab.Name + " for cheaper, " + ingredientItemLocationPrice.Value.Price, Color.Yellow); + NewMessage($" {GetSeller(ingredientItemPriceInfo.Value).CapitaliseFirstInvariant()} sells ingredient {ingredientItemPrefab.Name} for cheaper, {ingredientItemPriceInfo.Value.Price}", Color.Yellow); } else { - NewMessage(" Location " + ingredientItemLocationPrice.Key + " sells ingredient " + ingredientItemPrefab.Name + " for more, " + ingredientItemLocationPrice.Value.Price, Color.Teal); + NewMessage($" {GetSeller(ingredientItemPriceInfo.Value).CapitaliseFirstInvariant()} sells ingredient {ingredientItemPrefab.Name} for more, {ingredientItemPriceInfo.Value.Price}", Color.Teal); } } } } int costDifference = itemPrefab.DefaultPrice.Price - totalPrice; - NewMessage(" Constructing the item from store-bought items provides " + costDifference + " profit with default values."); + NewMessage($" Constructing the item from store-bought items provides {costDifference} profit with default values."); if (totalBestPrice.HasValue) { - int? bestDifference = itemLocationPrice.Value.Price - totalBestPrice; - NewMessage(" Constructing the item from store-bought items provides " + bestDifference + " profit with best-case scenario values."); + int? bestDifference = priceInfo.Value.Price - totalBestPrice; + NewMessage($" Constructing the item from store-bought items provides {bestDifference} profit with best-case scenario values."); } + + static string GetSeller(PriceInfo priceInfo) => $"store with identifier \"{priceInfo.StoreIdentifier}\""; } } }, @@ -1763,7 +1765,7 @@ namespace Barotrauma //check missing mission texts foreach (var missionPrefab in MissionPrefab.Prefabs) { - Identifier missionId = (missionPrefab.ConfigElement.Attribute("textidentifier") == null ? missionPrefab.Identifier : missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty)); + 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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index 7d3b239de..e99e15745 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -130,7 +130,7 @@ namespace Barotrauma UISprite newSprite = new UISprite(subElement); GUIComponent.ComponentState spriteState = GUIComponent.ComponentState.None; - if (subElement.Attribute("state") != null) + if (subElement.GetAttribute("state") != null) { string stateStr = subElement.GetAttributeString("state", "None"); Enum.TryParse(stateStr, out spriteState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 04eabc733..17f493e12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -172,7 +172,7 @@ namespace Barotrauma { AutoScaleVertical = true, TextScale = 1.1f, - TextGetter = () => FormatCurrency(campaign.Wallet.Balance) + TextGetter = () => TextManager.FormatCurrency(campaign.Wallet.Balance) }; var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, @@ -400,7 +400,7 @@ namespace Barotrauma if (listBox != crewList) { new GUITextBlock(new RectTransform(new Vector2(width, 1.0f), mainGroup.RectTransform), - FormatCurrency(characterInfo.Salary), + TextManager.FormatCurrency(characterInfo.Salary), textAlignment: Alignment.Center) { CanBeFocused = false @@ -629,7 +629,7 @@ namespace Barotrauma { total += ((InfoSkill)c.UserData).CharacterInfo.Salary; }); - totalBlock.Text = FormatCurrency(total); + totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.Wallet.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); @@ -925,7 +925,5 @@ namespace Barotrauma GameMain.Client.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } } - - private LocalizedString FormatCurrency(int currency) => TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", currency)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index e78e4e160..deb5dab1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -40,7 +40,7 @@ namespace Barotrauma public IEnumerable GetAllChildren() where T : GUIComponent { - return GetAllChildren().Where(c => c is T).Select(c => c as T); + return GetAllChildren().OfType(); } /// @@ -67,7 +67,7 @@ namespace Barotrauma { foreach (GUIComponent child in Children) { - if (child.UserData == obj || (child.UserData != null && child.UserData.Equals(obj))) { return child; } + if (Equals(child.UserData, obj)) { return child; } } return null; } @@ -108,7 +108,7 @@ namespace Barotrauma } public GUIComponent FindChild(object userData, bool recursive = false) { - var matchingChild = Children.FirstOrDefault(c => c.UserData == userData); + var matchingChild = Children.FirstOrDefault(c => Equals(c.UserData, userData)); if (recursive && matchingChild == null) { foreach (GUIComponent child in Children) @@ -123,7 +123,7 @@ namespace Barotrauma public IEnumerable FindChildren(object userData) { - return Children.Where(c => c.UserData == userData); + return Children.Where(c => Equals(c.UserData, userData)); } public IEnumerable FindChildren(Func predicate) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 5b4ae436a..2de088d11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -16,8 +16,8 @@ namespace Barotrauma public LocalizedString Tooltip; - public ContextMenuOption(string labelTag, bool isEnabled, Action onSelected) - : this(TextManager.Get(labelTag), isEnabled, onSelected) { } + public ContextMenuOption(string label, bool isEnabled, Action onSelected) + : this(TextManager.Get(label).Fallback(label), isEnabled, onSelected) { } public ContextMenuOption(Identifier labelTag, bool isEnabled, Action onSelected) : this(TextManager.Get(labelTag), isEnabled, onSelected) { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index 5ec73c90f..d34f6bf91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -314,13 +314,12 @@ namespace Barotrauma foreach (GUIComponent child in ListBox.Content.Children) { var tickBox = child.GetChild(); - if (obj == child.UserData) { tickBox.Selected = true; } + if (Equals(obj, child.UserData)) { tickBox.Selected = true; } } } else { - GUITextBlock textBlock = component as GUITextBlock; - if (textBlock == null) + if (!(component is GUITextBlock textBlock)) { textBlock = component.GetChild(); if (textBlock is null && !AllowNonText) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index 33837ece7..d21951cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -96,15 +96,11 @@ namespace Barotrauma switch (child.ScaleBasis) { case ScaleBasis.BothHeight: - child.MinSize = new Point(child.Rect.Height, child.MinSize.Y); - break; case ScaleBasis.Smallest when Rect.Height <= Rect.Width: case ScaleBasis.Largest when Rect.Height > Rect.Width: child.MinSize = new Point((int)((child.Rect.Height * child.RelativeSize.X) / child.RelativeSize.Y), child.MinSize.Y); break; case ScaleBasis.BothWidth: - child.MinSize = new Point(child.MinSize.X, child.Rect.Width); - break; case ScaleBasis.Smallest when Rect.Width <= Rect.Height: case ScaleBasis.Largest when Rect.Width > Rect.Height: child.MinSize = new Point(child.MinSize.X, (int)((child.Rect.Width * child.RelativeSize.Y) / child.RelativeSize.X)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index a40d29880..efaaebbc6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -402,8 +402,7 @@ namespace Barotrauma int i = 0; foreach (GUIComponent child in children) { - if ((child.UserData != null && child.UserData.Equals(userData)) || - (child.UserData == null && userData == null)) + if (Equals(child.UserData, userData)) { Select(i, force, autoScroll); if (!SelectMultiple) { return; } @@ -1219,6 +1218,20 @@ namespace Barotrauma i++; } + if (isDraggingElement && CurrentDragMode == DragMode.DragOutsideBox && HideDraggedElement) + { + Rectangle drawRect = DraggedElement.Rect; + int draggedElementIndex = Content.GetChildIndex(DraggedElement); + CalculateChildrenOffsets((index, point) => + { + if (draggedElementIndex == index) + { + drawRect.Location = Content.Rect.Location + point; + } + }); + GUI.DrawRectangle(spriteBatch, drawRect, Color.White * 0.5f, thickness: 2f); + } + if (HideChildrenOutsideFrame) { spriteBatch.End(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs index ccef64092..1487d6943 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIPrefab.cs @@ -65,7 +65,7 @@ namespace Barotrauma LoadFont(); } - private void LoadFont() + public void LoadFont() { string fontPath = GetFontFilePath(element); uint size = GetFontSize(element); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs index ed32a00b9..93f8803cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -268,7 +268,7 @@ namespace Barotrauma } int totalCost = medicalClinic.GetTotalCost(); - healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); + healList.PriceBlock.Text = TextManager.FormatCurrency(totalCost); healList.PriceBlock.TextColor = GUIStyle.Red; healList.HealButton.Enabled = false; if (medicalClinic.GetWallet().CanAfford(totalCost)) @@ -288,7 +288,7 @@ namespace Barotrauma { if (element.FindAfflictionElement(affliction) is { } existingAffliction) { - existingAffliction.Price.Text = UpgradeStore.FormatCurrency(affliction.Strength); + existingAffliction.Price.Text = TextManager.FormatCurrency(affliction.Strength); continue; } @@ -467,7 +467,7 @@ namespace Barotrauma GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { - TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetWallet().Balance), + TextGetter = () => TextManager.FormatCurrency(medicalClinic.GetWallet().Balance), AutoScaleVertical = true, TextScale = 1.1f }; @@ -571,7 +571,7 @@ namespace Barotrauma GUILayoutGroup priceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true); GUITextBlock priceLabelBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.Get("campaignstore.total")); - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUIStyle.SubHeadingFont, + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.FormatCurrency(medicalClinic.GetTotalCost()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); @@ -684,7 +684,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), UpgradeStore.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.LargeFont) { Padding = Vector4.Zero }; @@ -876,7 +876,7 @@ namespace Barotrauma ToolTip = prefab.Description }; - GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), TextManager.FormatCurrency(affliction.Price), font: GUIStyle.LargeFont); GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); @@ -931,7 +931,7 @@ namespace Barotrauma }); } - private static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) + 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 87c7ebe15..7d408efd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -44,6 +44,7 @@ namespace Barotrauma private bool suppressBuySell; private int buyTotal, sellTotal, sellFromSubTotal; + private GUITextBlock storeNameBlock; private GUITextBlock merchantBalanceBlock; private GUITextBlock currentSellValueBlock, newSellValueBlock; private GUIImage sellValueChangeArrow; @@ -65,6 +66,7 @@ namespace Barotrauma private Point resolutionWhenCreated; private Dictionary OwnedItems { get; } = new Dictionary(); + private Location.StoreInfo ActiveStore { get; set; } private CargoManager CargoManager => campaignUI.Campaign.CargoManager; private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation; @@ -238,6 +240,39 @@ namespace Barotrauma campaignUI.Campaign.CargoManager.OnItemsInSellFromSubCrateChanged += () => { needsSellingFromSubRefresh = true; }; } + public void SelectStore(Identifier identifier) + { + if (CurrentLocation?.Stores != null) + { + if (CurrentLocation.GetStore(identifier) is { } store) + { + ActiveStore = store; + if (storeNameBlock != null) + { + var storeName = TextManager.Get($"storename.{store.Identifier}"); + if (storeName.IsNullOrEmpty()) + { + storeName = TextManager.Get("store"); + } + storeNameBlock.SetRichText(storeName); + } + } + else + { + ActiveStore = null; + string msg = $"Error selecting store with identifier \"{identifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location."; + DebugConsole.ShowError(msg); + GameAnalyticsManager.AddErrorEventOnce("Store.SelectStore:StoreDoesntExist", GameAnalyticsManager.ErrorSeverity.Error, msg); + } + } + else + { + ActiveStore = null; + } + RefreshItemsToSell(); + Refresh(); + } + public void Refresh(bool updateOwned = true) { UpdatePermissions(); @@ -321,7 +356,7 @@ namespace Barotrauma }; var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width; new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreTradingIcon"); - new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont) + storeNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont) { CanBeFocused = false, ForceUpperCase = ForceUpperCase.Yes @@ -350,7 +385,7 @@ namespace Barotrauma TextScale = 1.1f, TextGetter = () => { - merchantBalanceBlock.TextColor = CurrentLocation?.BalanceColor ?? Color.Red; + merchantBalanceBlock.TextColor = ActiveStore?.BalanceColor ?? Color.Red; return GetMerchantBalanceText(); } }; @@ -388,17 +423,17 @@ namespace Barotrauma { int balanceAfterTransaction = activeTab switch { - StoreTab.Buy => CurrentLocation.StoreCurrentBalance + buyTotal, - StoreTab.Sell => CurrentLocation.StoreCurrentBalance - sellTotal, - StoreTab.SellSub => CurrentLocation.StoreCurrentBalance - sellFromSubTotal, + StoreTab.Buy => ActiveStore.Balance + buyTotal, + StoreTab.Sell => ActiveStore.Balance - sellTotal, + StoreTab.SellSub => ActiveStore.Balance - sellFromSubTotal, _ => throw new NotImplementedException(), }; - if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) + if (balanceAfterTransaction != ActiveStore.Balance) { var newStatus = CurrentLocation.GetStoreBalanceStatus(balanceAfterTransaction); - if (CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) + if (ActiveStore.ActiveBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) { - string tooltipTag = newStatus.SellPriceModifier > CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier ? + string tooltipTag = newStatus.SellPriceModifier > ActiveStore.ActiveBalanceStatus.SellPriceModifier ? "campaingstore.valueincreasetooltip" : "campaingstore.valuedecreasetooltip"; sellValueContainer.ToolTip = TextManager.Get(tooltipTag); currentSellValueBlock.TextColor = newStatus.Color; @@ -406,14 +441,14 @@ namespace Barotrauma sellValueChangeArrow.Visible = true; newSellValueBlock.TextColor = newStatus.Color; newSellValueBlock.Text = $"{(newStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; - return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; } } sellValueContainer.ToolTip = TextManager.Get("campaignstore.sellvaluetooltip"); - currentSellValueBlock.TextColor = CurrentLocation.BalanceColor; + currentSellValueBlock.TextColor = ActiveStore.BalanceColor; sellValueChangeArrow.Visible = false; newSellValueBlock.Text = null; - return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + return $"{(ActiveStore.ActiveBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; } else { @@ -698,9 +733,9 @@ namespace Barotrauma if (!HasActiveTabPermissions()) { return false; } var itemsToRemove = activeTab switch { - StoreTab.Buy => new List(CargoManager.ItemsInBuyCrate), - StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), + StoreTab.Buy => new List(CargoManager.GetBuyCrateItems(ActiveStore)), + StoreTab.Sell => new List(CargoManager.GetSellCrateItems(ActiveStore)), + StoreTab.SellSub => new List(CargoManager.GetSubCrateItems(ActiveStore)), _ => throw new NotImplementedException(), }; itemsToRemove.ForEach(i => ClearFromShoppingCrate(i)); @@ -708,14 +743,13 @@ namespace Barotrauma } }; - Refresh(); ChangeStoreTab(activeTab); resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } - private LocalizedString GetMerchantBalanceText() => GetCurrencyFormatted(CurrentLocation?.StoreCurrentBalance ?? 0); + private LocalizedString GetMerchantBalanceText() => TextManager.FormatCurrency(ActiveStore?.Balance ?? 0); - private LocalizedString GetPlayerBalanceText() => GetCurrencyFormatted(PlayerWallet.Balance); + private LocalizedString GetPlayerBalanceText() => TextManager.FormatCurrency(PlayerWallet.Balance); private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { @@ -746,21 +780,25 @@ namespace Barotrauma private void UpdateLocation(Location prevLocation, Location newLocation) { if (prevLocation == newLocation) { return; } - if (prevLocation?.Reputation != null) { - prevLocation.Reputation.OnReputationValueChanged = null; + prevLocation.Reputation.OnReputationValueChanged -= SetNeedsRefresh; } - if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo _))) + if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtFrom(newLocation))) { selectedItemCategory = null; searchBox.Text = ""; ChangeStoreTab(StoreTab.Buy); if (newLocation?.Reputation != null) { - newLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; + newLocation.Reputation.OnReputationValueChanged += SetNeedsRefresh; } } + + void SetNeedsRefresh() + { + needsRefresh = true; + } } private void ChangeStoreTab(StoreTab tab) @@ -862,9 +900,9 @@ namespace Barotrauma bool hasPermissions = HasBuyPermissions; HashSet existingItemFrames = new HashSet(); - int dailySpecialCount = CurrentLocation?.DailySpecials.Count() ?? 3; + int dailySpecialCount = ActiveStore.DailySpecials.Count; - if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) + if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) { @@ -881,32 +919,32 @@ namespace Barotrauma prevDailySpecialCount = dailySpecialCount; } - foreach (PurchasedItem item in CurrentLocation.StoreStock) + foreach (PurchasedItem item in ActiveStore.Stock) { CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (ItemPrefab itemPrefab in CurrentLocation.DailySpecials) + foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials) { - if (CurrentLocation.StoreStock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } + if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } CreateOrUpdateItemFrame(itemPrefab, 0); } void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) { - if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo)) { - var isDailySpecial = CurrentLocation.DailySpecials.Contains(itemPrefab); + bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab); var itemFrame = isDailySpecial ? storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeBuyList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.PurchasedItems.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem purchasedItem) + if (CargoManager.GetPurchasedItem(ActiveStore, itemPrefab) is { } purchasedItem) { quantity = Math.Max(quantity - purchasedItem.Quantity, 0); } - if (CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInBuyCrate) + if (CargoManager.GetBuyCrateItem(ActiveStore, itemPrefab) is { } buyCrateItem) { - quantity = Math.Max(quantity - itemInBuyCrate.Quantity, 0); + quantity = Math.Max(quantity - buyCrateItem.Quantity, 0); } if (itemFrame == null) { @@ -945,7 +983,7 @@ namespace Barotrauma bool hasPermissions = HasTabPermissions(StoreTab.Sell); HashSet existingItemFrames = new HashSet(); - if ((storeRequestedGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) + if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any()) { if (storeRequestedGoodGroup == null) { @@ -965,7 +1003,7 @@ namespace Barotrauma CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (var requestedGood in CurrentLocation.RequestedGoods) + foreach (var requestedGood in ActiveStore.RequestedGoods) { if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } CreateOrUpdateItemFrame(requestedGood, 0); @@ -973,15 +1011,15 @@ namespace Barotrauma void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) { - PriceInfo priceInfo = itemPrefab.GetPriceInfo(CurrentLocation); + PriceInfo priceInfo = itemPrefab.GetPriceInfo(ActiveStore); if (priceInfo == null) { return; } - var isRequestedGood = CurrentLocation.RequestedGoods.Contains(itemPrefab); + var isRequestedGood = ActiveStore.RequestedGoods.Contains(itemPrefab); var itemFrame = isRequestedGood ? storeRequestedGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeSellList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInSellCrate) + if (CargoManager.GetSellCrateItem(ActiveStore, itemPrefab) is { } sellCrateItem) { - itemQuantity = Math.Max(itemQuantity - itemInSellCrate.Quantity, 0); + itemQuantity = Math.Max(itemQuantity - sellCrateItem.Quantity, 0); } if (itemFrame == null) { @@ -1023,7 +1061,7 @@ namespace Barotrauma bool hasPermissions = HasSellSubPermissions; HashSet existingItemFrames = new HashSet(); - if ((storeRequestedSubGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) + if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any()) { if (storeRequestedSubGoodGroup == null) { @@ -1043,7 +1081,7 @@ namespace Barotrauma CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (var requestedGood in CurrentLocation.RequestedGoods) + foreach (var requestedGood in ActiveStore.RequestedGoods) { if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } CreateOrUpdateItemFrame(requestedGood, 0); @@ -1051,15 +1089,15 @@ namespace Barotrauma void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) { - PriceInfo priceInfo = itemPrefab.GetPriceInfo(CurrentLocation); + PriceInfo priceInfo = itemPrefab.GetPriceInfo(ActiveStore); if (priceInfo == null) { return; } - var isRequestedGood = CurrentLocation.RequestedGoods.Contains(itemPrefab); + bool isRequestedGood = ActiveStore.RequestedGoods.Contains(itemPrefab); var itemFrame = isRequestedGood ? storeRequestedSubGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : storeSellFromSubList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); - if (CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInSellFromSubCrate) + if (CargoManager.GetSubCrateItem(ActiveStore, itemPrefab) is { } subCrateItem) { - itemQuantity = Math.Max(itemQuantity - itemInSellFromSubCrate.Quantity, 0); + itemQuantity = Math.Max(itemQuantity - subCrateItem.Quantity, 0); } if (itemFrame == null) { @@ -1102,13 +1140,13 @@ namespace Barotrauma { if (buying) { - undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( - CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0); + undiscountedPriceBlock.TextGetter = () => TextManager.FormatCurrency( + ActiveStore?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0); } else { - undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( - CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0); + undiscountedPriceBlock.TextGetter = () => TextManager.FormatCurrency( + ActiveStore?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0); } } @@ -1116,11 +1154,11 @@ namespace Barotrauma { if (buying) { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0); + priceBlock.TextGetter = () => TextManager.FormatCurrency(ActiveStore?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0); } else { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0); + priceBlock.TextGetter = () => TextManager.FormatCurrency(ActiveStore?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0); } } } @@ -1135,21 +1173,21 @@ namespace Barotrauma { item.Quantity += 1; } - else if (playerItem.Prefab.GetPriceInfo(CurrentLocation) != null) + else if (playerItem.Prefab.GetPriceInfo(ActiveStore) != null) { itemsToSell.Add(new PurchasedItem(playerItem.Prefab, 1)); } } // Remove items from sell crate if they aren't in player inventory anymore - var itemsInCrate = new List(CargoManager.ItemsInSellCrate); + var itemsInCrate = new List(CargoManager.GetSellCrateItems(ActiveStore)); foreach (PurchasedItem crateItem in itemsInCrate) { var playerItem = itemsToSell.Find(i => i.ItemPrefab == crateItem.ItemPrefab); var playerItemQuantity = playerItem != null ? playerItem.Quantity : 0; if (crateItem.Quantity > playerItemQuantity) { - CargoManager.ModifyItemQuantityInSellCrate(crateItem.ItemPrefab, playerItemQuantity - crateItem.Quantity); + CargoManager.ModifyItemQuantityInSellCrate(ActiveStore.Identifier, crateItem.ItemPrefab, playerItemQuantity - crateItem.Quantity); } } needsItemsToSellRefresh = false; @@ -1165,35 +1203,35 @@ namespace Barotrauma { item.Quantity += 1; } - else if (subItem.Prefab.GetPriceInfo(CurrentLocation) != null) + else if (subItem.Prefab.GetPriceInfo(ActiveStore) != null) { itemsToSellFromSub.Add(new PurchasedItem(subItem.Prefab, 1)); } } // Remove items from sell crate if they aren't on the sub anymore - var itemsInCrate = new List(CargoManager.ItemsInSellFromSubCrate); + var itemsInCrate = new List(CargoManager.GetSubCrateItems(ActiveStore)); foreach (PurchasedItem crateItem in itemsInCrate) { var subItem = itemsToSellFromSub.Find(i => i.ItemPrefab == crateItem.ItemPrefab); var subItemQuantity = subItem != null ? subItem.Quantity : 0; if (crateItem.Quantity > subItemQuantity) { - CargoManager.ModifyItemQuantityInSellFromSubCrate(crateItem.ItemPrefab, subItemQuantity - crateItem.Quantity); + CargoManager.ModifyItemQuantityInSubSellCrate(ActiveStore.Identifier, crateItem.ItemPrefab, subItemQuantity - crateItem.Quantity); } } sellableItemsFromSubUpdateTimer = 0.0f; needsItemsToSellFromSubRefresh = false; } - private void RefreshShoppingCrateList(List items, GUIListBox listBox, StoreTab tab) + private void RefreshShoppingCrateList(IEnumerable items, GUIListBox listBox, StoreTab tab) { bool hasPermissions = HasTabPermissions(tab); HashSet existingItemFrames = new HashSet(); int totalPrice = 0; foreach (PurchasedItem item in items) { - if (!(item.ItemPrefab.GetPriceInfo(CurrentLocation) is { } priceInfo)) { continue; } + if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; } GUINumberInput numInput = null; if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame)) { @@ -1227,9 +1265,9 @@ namespace Barotrauma { int price = tab switch { - StoreTab.Buy => CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), - StoreTab.Sell => CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), - StoreTab.SellSub => CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), + StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo), _ => throw new NotImplementedException() }; totalPrice += item.Quantity * price; @@ -1265,11 +1303,11 @@ namespace Barotrauma SetConfirmButtonStatus(); } - private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.ItemsInBuyCrate, shoppingCrateBuyList, StoreTab.Buy); + private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.GetBuyCrateItems(ActiveStore), shoppingCrateBuyList, StoreTab.Buy); - private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.ItemsInSellCrate, shoppingCrateSellList, StoreTab.Sell); + private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.GetSellCrateItems(ActiveStore), shoppingCrateSellList, StoreTab.Sell); - private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.ItemsInSellFromSubCrate, shoppingCrateSellFromSubList, StoreTab.SellSub); + private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.GetSubCrateItems(ActiveStore), shoppingCrateSellFromSubList, StoreTab.SellSub); private void SortItems(GUIListBox list, SortingMethod sortingMethod) { @@ -1316,8 +1354,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { - var sortResult = CurrentLocation.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( - CurrentLocation.GetAdjustedItemSellPrice(itemY.ItemPrefab)); + int sortResult = ActiveStore.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( + ActiveStore.GetAdjustedItemSellPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } return sortResult; } @@ -1340,8 +1378,8 @@ namespace Barotrauma { if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) { - var sortResult = CurrentLocation.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( - CurrentLocation.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); + int sortResult = ActiveStore.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( + ActiveStore.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } return sortResult; } @@ -1485,7 +1523,7 @@ namespace Barotrauma }; bool isSellingRelatedList = containingTab != StoreTab.Buy; bool locationHasDealOnItem = isSellingRelatedList ? - CurrentLocation.RequestedGoods.Contains(pi.ItemPrefab) : CurrentLocation.DailySpecials.Contains(pi.ItemPrefab); + ActiveStore.RequestedGoods.Contains(pi.ItemPrefab) : ActiveStore.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft) { @@ -1673,7 +1711,7 @@ namespace Barotrauma } // Add items already purchased - CargoManager?.PurchasedItems?.ForEach(pi => AddNonEmptyOwnedItems(pi)); + CargoManager?.GetPurchasedItems(ActiveStore).ForEach(pi => AddNonEmptyOwnedItems(pi)); ownedItemsUpdateTimer = 0.0f; @@ -1689,7 +1727,7 @@ namespace Barotrauma void AddOwnedItem(Item item) { - if (!(item?.Prefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo)) { return; } + if (!(item?.Prefab.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)) { return; } bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f; if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity)) { @@ -1862,7 +1900,7 @@ namespace Barotrauma { list = mode switch { - StoreTab.Buy => CurrentLocation?.StoreStock, + StoreTab.Buy => ActiveStore?.Stock, StoreTab.Sell => itemsToSell, StoreTab.SellSub => itemsToSellFromSub, _ => throw new NotImplementedException() @@ -1876,7 +1914,7 @@ namespace Barotrauma { if (mode == StoreTab.Buy) { - var purchasedItem = CargoManager.PurchasedItems.Find(i => i.ItemPrefab == item.ItemPrefab); + var purchasedItem = CargoManager.GetPurchasedItem(ActiveStore, item.ItemPrefab); if (purchasedItem != null) { return Math.Max(item.Quantity - purchasedItem.Quantity, 0); } } return item.Quantity; @@ -1887,22 +1925,19 @@ namespace Barotrauma } } - private LocalizedString GetCurrencyFormatted(int amount) => - TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); - private bool ModifyBuyQuantity(PurchasedItem item, int quantity) { if (item?.ItemPrefab == null) { return false; } if (!HasBuyPermissions) { return false; } if (quantity > 0) { - var itemInCrate = CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab); - if (itemInCrate != null && itemInCrate.Quantity >= CargoManager.MaxQuantity) { return false; } + var crateItem = CargoManager.GetBuyCrateItem(ActiveStore, item.ItemPrefab); + if (crateItem != null && crateItem.Quantity >= CargoManager.MaxQuantity) { return false; } // Make sure there's enough available in the store - var totalQuantityToBuy = itemInCrate != null ? itemInCrate.Quantity + quantity : quantity; + var totalQuantityToBuy = crateItem != null ? crateItem.Quantity + quantity : quantity; if (totalQuantityToBuy > GetMaxAvailable(item.ItemPrefab, StoreTab.Buy)) { return false; } } - CargoManager.ModifyItemQuantityInBuyCrate(item.ItemPrefab, quantity); + CargoManager.ModifyItemQuantityInBuyCrate(ActiveStore.Identifier, item.ItemPrefab, quantity); GameMain.Client?.SendCampaignState(); return true; } @@ -1914,11 +1949,11 @@ namespace Barotrauma if (quantity > 0) { // Make sure there's enough available to sell - var itemToSell = CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + var itemToSell = CargoManager.GetSellCrateItem(ActiveStore, item.ItemPrefab); var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.Sell)) { return false; } } - CargoManager.ModifyItemQuantityInSellCrate(item.ItemPrefab, quantity); + CargoManager.ModifyItemQuantityInSellCrate(ActiveStore.Identifier, item.ItemPrefab, quantity); return true; } @@ -1929,11 +1964,11 @@ namespace Barotrauma if (quantity > 0) { // Make sure there's enough available to sell - var itemToSell = CargoManager.ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == item.ItemPrefab); + var itemToSell = CargoManager.GetSubCrateItem(ActiveStore, item.ItemPrefab); var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity; if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellSub)) { return false; } } - CargoManager.ModifyItemQuantityInSellFromSubCrate(item.ItemPrefab, quantity); + CargoManager.ModifyItemQuantityInSubSellCrate(ActiveStore.Identifier, item.ItemPrefab, quantity); GameMain.Client?.SendCampaignState(); return true; } @@ -1981,32 +2016,27 @@ namespace Barotrauma private bool BuyItems() { if (!HasBuyPermissions) { return false; } - - var itemsToPurchase = new List(CargoManager.ItemsInBuyCrate); + var itemsToPurchase = new List(CargoManager.GetBuyCrateItems(ActiveStore)); var itemsToRemove = new List(); - var totalPrice = 0; - foreach (PurchasedItem item in itemsToPurchase) + int totalPrice = 0; + foreach (var item in itemsToPurchase) { - if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo)) { itemsToRemove.Add(item); continue; } - totalPrice += item.Quantity * CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); + totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); - if (itemsToPurchase.None() || !PlayerWallet.CanAfford(totalPrice)) { return false; } - - CargoManager.PurchaseItems(itemsToPurchase, true); + CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, true); GameMain.Client?.SendCampaignState(); - var dialog = new GUIMessageBox( TextManager.Get("newsupplies"), TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.Name), new LocalizedString[] { TextManager.Get("Ok") }); dialog.Buttons[0].OnClicked += dialog.Close; - return false; } @@ -2018,8 +2048,8 @@ namespace Barotrauma { itemsToSell = activeTab switch { - StoreTab.Sell => new List(CargoManager.ItemsInSellCrate), - StoreTab.SellSub => new List(CargoManager.ItemsInSellFromSubCrate), + StoreTab.Sell => new List(CargoManager.GetSellCrateItems(ActiveStore)), + StoreTab.SellSub => new List(CargoManager.GetSubCrateItems(ActiveStore)), _ => throw new NotImplementedException() }; } @@ -2032,9 +2062,9 @@ namespace Barotrauma int totalValue = 0; foreach (PurchasedItem item in itemsToSell) { - if (item?.ItemPrefab?.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) + if (item?.ItemPrefab?.GetPriceInfo(ActiveStore) is PriceInfo priceInfo) { - totalValue += item.Quantity * CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); + totalValue += item.Quantity * ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); } else { @@ -2042,8 +2072,8 @@ namespace Barotrauma } } itemsToRemove.ForEach(i => itemsToSell.Remove(i)); - if (itemsToSell.None() || totalValue > CurrentLocation.StoreCurrentBalance) { return false; } - CargoManager.SellItems(itemsToSell, activeTab); + if (itemsToSell.None() || totalValue > ActiveStore.Balance) { return false; } + CargoManager.SellItems(ActiveStore.Identifier, itemsToSell, activeTab); GameMain.Client?.SendCampaignState(); return false; } @@ -2052,7 +2082,7 @@ namespace Barotrauma { if (IsBuying) { - shoppingCrateTotal.Text = GetCurrencyFormatted(buyTotal); + shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal); shoppingCrateTotal.TextColor = !PlayerWallet.CanAfford(buyTotal) ? Color.Red : Color.White; } else @@ -2063,8 +2093,8 @@ namespace Barotrauma StoreTab.SellSub => sellFromSubTotal, _ => throw new NotImplementedException(), }; - shoppingCrateTotal.Text = GetCurrencyFormatted(total); - shoppingCrateTotal.TextColor = CurrentLocation != null && total > CurrentLocation.StoreCurrentBalance ? Color.Red : Color.White; + shoppingCrateTotal.Text = TextManager.FormatCurrency(total); + shoppingCrateTotal.TextColor = CurrentLocation != null && total > ActiveStore.Balance ? Color.Red : Color.White; } } @@ -2100,8 +2130,8 @@ namespace Barotrauma activeTab switch { StoreTab.Buy => PlayerWallet.CanAfford(buyTotal), - StoreTab.Sell => CurrentLocation != null && sellTotal <= CurrentLocation.StoreCurrentBalance, - StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= CurrentLocation.StoreCurrentBalance, + StoreTab.Sell => CurrentLocation != null && sellTotal <= ActiveStore.Balance, + StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance, _ => false }; } @@ -2124,6 +2154,7 @@ namespace Barotrauma if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); + needsRefresh = true; } else { @@ -2131,38 +2162,62 @@ namespace Barotrauma ownedItemsUpdateTimer += deltaTime; if (ownedItemsUpdateTimer >= timerUpdateInterval) { - var prevOwnedItems = new Dictionary(OwnedItems); + bool checkForRefresh = !needsItemsToSellRefresh || !needsRefresh; + var prevOwnedItems = checkForRefresh ? new Dictionary(OwnedItems) : null; UpdateOwnedItems(); - bool refresh = OwnedItems.Count != prevOwnedItems.Count || - OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) || - OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) || - prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key)); - if (refresh) + if (checkForRefresh) { - needsItemsToSellRefresh = true; - needsRefresh = true; + bool refresh = OwnedItems.Count != prevOwnedItems.Count || + OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) || + OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) || + prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key)); + if (refresh) + { + needsItemsToSellRefresh = true; + needsRefresh = true; + } } } // Update the sellable sub items at short intervals and check if the interface should be refreshed sellableItemsFromSubUpdateTimer += deltaTime; if (sellableItemsFromSubUpdateTimer >= timerUpdateInterval) { - var prevSubItems = new List(itemsToSellFromSub); + bool checkForRefresh = !needsRefresh; + var prevSubItems = checkForRefresh ? new List(itemsToSellFromSub) : null; RefreshItemsToSellFromSub(); - needsRefresh = needsRefresh || - itemsToSellFromSub.Count != prevSubItems.Count || - itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || - itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || - prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); + if (checkForRefresh) + { + needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count || + itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) || + itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) || + prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab)); + } } } - - if (needsItemsToSellRefresh) { RefreshItemsToSell(); } - if (needsItemsToSellFromSubRefresh) { RefreshItemsToSellFromSub(); } - if (needsRefresh || HavePermissionsChanged()) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) { RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) { RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); } - if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) { RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); } + if (needsItemsToSellRefresh) + { + RefreshItemsToSell(); + } + if (needsItemsToSellFromSubRefresh) + { + RefreshItemsToSellFromSub(); + } + if (needsRefresh || HavePermissionsChanged()) + { + Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) + { + RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell)) + { + RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f); + } + if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub)) + { + RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f); + } updateStopwatch.Stop(); GameMain.PerformanceCounter.AddPartialElapsedTicks("GameSessionUpdate", "StoreUpdate", updateStopwatch.ElapsedTicks); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 4272b8dcc..152424061 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -31,7 +31,7 @@ namespace Barotrauma private readonly List subsToShow; private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; - private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; + private LocalizedString purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyName; private readonly RectTransform parent; private readonly Action closeAction; private Sprite pageIndicator; @@ -99,8 +99,7 @@ namespace Barotrauma priceText = TextManager.Get("price"); } - currencyShorthandText = TextManager.Get("currencyformat"); - currencyLongText = TextManager.Get("credit").Value.ToLowerInvariant(); + currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); UpdateSubmarines(); missingPreviewText = TextManager.Get("SubPreviewImageNotFound"); @@ -335,7 +334,7 @@ namespace Barotrauma if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) { - LocalizedString amountString = currencyShorthandText.Replace("[credits]", subToDisplay.Price.ToString()); + LocalizedString amountString = TextManager.FormatCurrency(subToDisplay.Price); submarineDisplays[i].submarineFee.Text = priceText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -344,7 +343,7 @@ namespace Barotrauma { if (deliveryFee > 0) { - LocalizedString amountString = currencyShorthandText.Replace("[credits]", deliveryFee.ToString()); + LocalizedString amountString = TextManager.FormatCurrency(deliveryFee); submarineDisplays[i].submarineFee.Text = deliveryFeeText.Replace("[amount]", amountString).Replace("[currencyname]", string.Empty).TrimEnd(); } else @@ -584,7 +583,7 @@ namespace Barotrauma if (!GameMain.GameSession.Campaign.Wallet.CanAfford(deliveryFee) && deliveryFee > 0) { new GUIMessageBox(TextManager.Get("deliveryrequestheader"), TextManager.GetWithVariables("notenoughmoneyfordeliverytext", - ("[currencyname]", currencyLongText), + ("[currencyname]", currencyName), ("[submarinename]", selectedSubmarine.DisplayName), ("[location1]", deliveryLocationName), ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name))); @@ -601,7 +600,7 @@ namespace Barotrauma ("[location2]", GameMain.GameSession.Map.CurrentLocation.Name), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName), ("[amount]", deliveryFee.ToString()), - ("[currencyname]", currencyLongText)), messageBoxOptions); + ("[currencyname]", currencyName)), messageBoxOptions); } else { @@ -632,7 +631,7 @@ namespace Barotrauma if (!GameMain.GameSession.Campaign.Wallet.CanAfford(selectedSubmarine.Price)) { new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("notenoughmoneyforpurchasetext", - ("[currencyname]", currencyLongText), + ("[currencyname]", currencyName), ("[submarinename]", selectedSubmarine.DisplayName))); return; } @@ -644,7 +643,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(TextManager.Get("purchaseandswitchsubmarineheader"), TextManager.GetWithVariables("purchaseandswitchsubmarinetext", ("[submarinename1]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), - ("[currencyname]", currencyLongText), + ("[currencyname]", currencyName), ("[submarinename2]", CurrentOrPendingSubmarine().DisplayName)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => @@ -667,7 +666,7 @@ namespace Barotrauma msgBox = new GUIMessageBox(TextManager.Get("purchasesubmarineheader"), TextManager.GetWithVariables("purchasesubmarinetext", ("[submarinename]", selectedSubmarine.DisplayName), ("[amount]", selectedSubmarine.Price.ToString()), - ("[currencyname]", currencyLongText)), messageBoxOptions); + ("[currencyname]", currencyName)), messageBoxOptions); msgBox.Buttons[0].OnClicked = (applyButton, obj) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 201d8d242..abd866462 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -218,7 +218,7 @@ namespace Barotrauma public void AddToGUIUpdateList() { - infoFrame?.AddToGUIUpdateList(); + infoFrame?.AddToGUIUpdateList(order: 1); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); } @@ -379,8 +379,7 @@ namespace Barotrauma private void CreateCrewListFrame(GUIFrame crewFrame) { - // FIXME remove TestScreen stuff - crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new []{ TestScreen.dummyCharacter }; + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? Array.Empty(); teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -817,8 +816,11 @@ namespace Barotrauma else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); - if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewClient(client, preview); } - CreateWalletFrame(background, client.Character); + GameMain.Client?.SelectCrewClient(client, preview); + if (client.Character != null) + { + CreateWalletFrame(background, client.Character); + } } return true; @@ -845,22 +847,23 @@ 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); - GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); + 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), TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()), textAlignment: Alignment.BottomRight); + GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight); 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) { - Range = Vector2.UnitY, + ToolTip = TextManager.Get("crewwallet.salary.tooltip"), + Range = new Vector2(0, 1), BarScrollValue = targetWallet.RewardDistribution / 100f, Step = 0.01f, BarSize = 0.1f, OnMoved = (bar, scroll) => { - rewardBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", GetSharePercentage()); + SetRewardText((int)(scroll * 100), rewardBlock); return true; }, OnReleased = (bar, scroll) => @@ -871,6 +874,9 @@ namespace Barotrauma return true; } }; + + SetRewardText(targetWallet.RewardDistribution, rewardBlock); + // @formatter:off GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter)) { @@ -883,7 +889,7 @@ namespace Barotrauma GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedTransferMenuLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform)); GUITextBlock leftName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), character.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont); - GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), UpgradeStore.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue }; + GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue }; GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform), childAnchor: Anchor.TopRight); GUITextBlock rightName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight); GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red }; @@ -902,8 +908,11 @@ namespace Barotrauma GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; // @formatter:on + ImmutableArray layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout); + MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups); transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical") { + ToolTip = TextManager.Get("crewwallet.transfer.tooltip"), OnClicked = (button, o) => { isTransferMenuOpen = !isTransferMenuOpen; @@ -951,13 +960,15 @@ namespace Barotrauma break; } + MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); + if (!hasPermissions) { centerButton.Enabled = centerButton.CanBeFocused = false; salarySlider.Enabled = salarySlider.CanBeFocused = false; } - leftBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); UpdateAllInputs(); @@ -983,7 +994,7 @@ namespace Barotrauma { if (e.Wallet == targetWallet) { - moneyBlock.Text = UpgradeStore.FormatCurrency(e.Info.Balance); + moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; } UpdateAllInputs(); @@ -1022,23 +1033,23 @@ namespace Barotrauma confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0; if (transferAmountInput.IntValue == 0) { - rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance); + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); rightBalance.TextColor = GUIStyle.TextColorNormal; - leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance); + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance); leftBalance.TextColor = GUIStyle.TextColorNormal; } else if (isSending) { - rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue); rightBalance.TextColor = GUIStyle.Blue; - leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue); leftBalance.TextColor = GUIStyle.Red; } else { - rightBalance.Text = UpgradeStore.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); + rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue); rightBalance.TextColor = GUIStyle.Red; - leftBalance.Text = UpgradeStore.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); + leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue); leftBalance.TextColor = GUIStyle.Blue; } } @@ -1073,14 +1084,14 @@ namespace Barotrauma Receiver = to.Select(option => option.ID), Amount = amount }; - IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.MONEY); + IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.TRANSFER_MONEY); transfer.Write(msg); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } static void SetRewardDistribution(Character character, int newValue) { - INetSerializableStruct transfer = new NetWalletSalaryUpdate + INetSerializableStruct transfer = new NetWalletSetSalaryUpdate { Target = character.ID, NewRewardDistribution = newValue @@ -1090,7 +1101,23 @@ namespace Barotrauma GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); } - string GetSharePercentage() => Mission.GetRewardShare(targetWallet.RewardDistribution, salaryCrew, Option.None()).Percentage.ToString(); + 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index f4ec7f408..a6a0f3a54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -287,7 +287,7 @@ namespace Barotrauma GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight); GUILayoutGroup priceLayout = new GUILayoutGroup(rectT(1, 0.8f, rightLayout), childAnchor: Anchor.Center) { RelativeSpacing = 0.08f }; new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.Get("CampaignStore.Balance"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right); - new GUITextBlock(rectT(1f, 0f, priceLayout), FormatCurrency(PlayerWallet.Balance, format: true), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => FormatCurrency(PlayerWallet.Balance, format: true) }; + new GUITextBlock(rectT(1f, 0f, priceLayout), TextManager.FormatCurrency(PlayerWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right) { TextGetter = () => TextManager.FormatCurrency(PlayerWallet.Balance) }; new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true }; repairButton.OnClicked = upgradeButton.OnClicked = (button, o) => @@ -571,7 +571,7 @@ namespace Barotrauma var repairIcon = new GUIFrame(rectT(new Point(contentLayout.Rect.Height, contentLayout.Rect.Height), contentLayout), style: imageStyle); GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true }; new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true }; - new GUITextBlock(rectT(1, 0, textLayout), FormatCurrency(price)); + 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 = PlayerWallet.Balance >= price && !isDisabled, OnClicked = onPressed }; contentLayout.Recalculate(); @@ -1094,7 +1094,7 @@ namespace Barotrauma if (addBuyButton) { - var formattedPrice = FormatCurrency(Math.Abs(price)); + var formattedPrice = TextManager.FormatCurrency(Math.Abs(price)); //negative price = refund if (price < 0) { formattedPrice = "+" + formattedPrice; } buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, prefabLayout), childAnchor: Anchor.TopCenter) { UserData = "buybutton" }; @@ -1577,7 +1577,7 @@ namespace Barotrauma if (priceLabel != null && !WaitForServerUpdate) { - priceLabel.Text = FormatCurrency(price); + priceLabel.Text = TextManager.FormatCurrency(price); if (currentLevel >= prefab.MaxLevel) { priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade"); @@ -1695,11 +1695,6 @@ namespace Barotrauma private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); - public static LocalizedString FormatCurrency(int money, bool format = true) - { - return TextManager.GetWithVariable("CurrencyFormat", "[credits]", format ? string.Format(CultureInfo.InvariantCulture, "{0:N0}", money) : money.ToString()); - } - // just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me) private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index c1ed2807f..e4c8ab417 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1038,9 +1038,9 @@ 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) { - spCampaign.Map.CurrentLocation.AddToStock(spCampaign.CargoManager.SoldItems); + spCampaign.Map.CurrentLocation.AddStock(spCampaign.CargoManager.SoldItems); spCampaign.CargoManager.ClearSoldItemsProjSpecific(); - spCampaign.Map.CurrentLocation.RemoveFromStock(spCampaign.CargoManager.PurchasedItems); + spCampaign.Map.CurrentLocation.RemoveStock(spCampaign.CargoManager.PurchasedItems); } SaveUtil.SaveGame(GameSession.SavePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 3c9ac933b..29cea3e9b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -45,28 +45,37 @@ namespace Barotrauma return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } - public void SetItemsInBuyCrate(List items) + public void SetItemsInBuyCrate(Dictionary> items) { ItemsInBuyCrate.Clear(); - ItemsInBuyCrate.AddRange(items); + foreach (var entry in items) + { + ItemsInBuyCrate.Add(entry.Key, entry.Value); + } OnItemsInBuyCrateChanged?.Invoke(); } - public void SetItemsInSubSellCrate(List items) + public void SetItemsInSubSellCrate(Dictionary> items) { ItemsInSellFromSubCrate.Clear(); - ItemsInSellFromSubCrate.AddRange(items); + foreach (var entry in items) + { + ItemsInSellFromSubCrate.Add(entry.Key, entry.Value); + } OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void SetSoldItems(List items) + public void SetSoldItems(Dictionary> items) { SoldItems.Clear(); - SoldItems.AddRange(items); + foreach (var entry in items) + { + SoldItems.Add(entry.Key, entry.Value); + } foreach (var se in SoldEntities) { if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } - if (SoldItems.Any(si => Match(si, se, true))) + if (SoldItems.Any(si => si.Value.Any(si => Match(si, se, true)))) { se.Status = SoldEntity.SellStatus.Confirmed; } @@ -75,13 +84,16 @@ namespace Barotrauma se.Status = SoldEntity.SellStatus.Unconfirmed; } } - foreach (var si in SoldItems) + foreach (var soldItems in SoldItems.Values) { - if (si.Origin != SoldItem.SellOrigin.Submarine) { continue; } - if (!(SoldEntities.FirstOrDefault(se => se.Item == null && Match(si, se, false)) is SoldEntity soldEntityMatch)) { continue; } - if (!(Entity.FindEntityByID(si.ID) is Item item)) { continue; } - soldEntityMatch.SetItem(item); - soldEntityMatch.Status = SoldEntity.SellStatus.Confirmed; + foreach (var si in soldItems) + { + if (si.Origin != SoldItem.SellOrigin.Submarine) { continue; } + if (!(SoldEntities.FirstOrDefault(se => se.Item == null && Match(si, se, false)) is SoldEntity soldEntityMatch)) { continue; } + if (!(Entity.FindEntityByID(si.ID) is Item item)) { continue; } + soldEntityMatch.SetItem(item); + soldEntityMatch.Status = SoldEntity.SellStatus.Confirmed; + } } OnSoldItemsChanged?.Invoke(); @@ -94,45 +106,24 @@ namespace Barotrauma } } - public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) + public void ModifyItemQuantityInSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity) { - var itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemToSell != null) + if (GetSellCrateItem(storeIdentifier, itemPrefab) is { } item) { - itemToSell.Quantity += changeInQuantity; - if (itemToSell.Quantity < 1) + item.Quantity += changeInQuantity; + if (item.Quantity < 1) { - ItemsInSellCrate.Remove(itemToSell); + GetSellCrateItems(storeIdentifier)?.Remove(item); } } else if (changeInQuantity > 0) { - itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); - ItemsInSellCrate.Add(itemToSell); + GetSellCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity)); } OnItemsInSellCrateChanged?.Invoke(); } - public void ModifyItemQuantityInSellFromSubCrate(ItemPrefab itemPrefab, int changeInQuantity) - { - var itemToSell = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemToSell != null) - { - itemToSell.Quantity += changeInQuantity; - if (itemToSell.Quantity < 1) - { - ItemsInSellFromSubCrate.Remove(itemToSell); - } - } - else if (changeInQuantity > 0) - { - itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); - ItemsInSellFromSubCrate.Add(itemToSell); - } - OnItemsInSellFromSubCrateChanged?.Invoke(); - } - - public void SellItems(List itemsToSell, Store.StoreTab sellingMode) + public void SellItems(Identifier storeIdentifier, List itemsToSell, Store.StoreTab sellingMode) { IEnumerable sellableItems; try @@ -146,19 +137,24 @@ namespace Barotrauma } catch (NotImplementedException e) { - DebugConsole.ShowError($"Error selling items: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); + DebugConsole.ShowError($"Error selling items: uknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}"); return; } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; byte sellerId = GameMain.Client?.ID ?? 0; - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in itemsToSell) + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); + if (!(Location.GetStore(storeIdentifier) is { } store)) + { + DebugConsole.ShowError($"Error selling items at {Location}: no store with identifier \"{storeIdentifier}\" exists.\n{Environment.StackTrace.CleanupStackTrace()}"); + return; + } + var storeSpecificSoldItems = GetSoldItems(storeIdentifier, create: true); + foreach (var item in itemsToSell) { int itemValue = item.Quantity * sellValues[item.ItemPrefab]; // check if the store can afford the item - if (Location.StoreCurrentBalance < itemValue) { continue; } + if (store.Balance < itemValue) { continue; } // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton) var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab); int count = Math.Min(item.Quantity, matchingItems.Count()); @@ -168,7 +164,7 @@ namespace Barotrauma for (int i = 0; i < count; i++) { var matchingItem = matchingItems.ElementAt(i); - SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); + storeSpecificSoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local)); if (canAddToRemoveQueue) { Entity.Spawner.AddItemToRemoveQueue(matchingItem); } } @@ -178,22 +174,23 @@ namespace Barotrauma // When selling from the sub in multiplayer, the server will determine the items that are sold for (int i = 0; i < count; i++) { - SoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); + storeSpecificSoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(item.ItemPrefab, SoldEntity.SellStatus.Local)); } } // Exchange money - Location.StoreCurrentBalance -= itemValue; + store.Balance -= itemValue; campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); // Remove from the sell crate - if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) + var sellCrate = (sellingMode == Store.StoreTab.Sell ? GetSellCrateItems(storeIdentifier) : GetSubCrateItems(storeIdentifier)); + if (sellCrate?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) { itemToSell.Quantity -= item.Quantity; if (itemToSell.Quantity < 1) { - (sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Remove(itemToSell); + sellCrate.Remove(itemToSell); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 7186c3bd9..6d334c7a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -63,6 +63,10 @@ namespace Barotrauma } } + /// + /// Gets the current personal wallet + /// In singleplayer this is the campaign bank and in multiplayer this is the personal wallet + /// public virtual Wallet Wallet => GetWallet(); public override void ShowStartMessage() @@ -301,7 +305,7 @@ namespace Barotrauma goto default; default: ShowCampaignUI = true; - CampaignUI.SelectTab(npc.CampaignInteractionType); + CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); CampaignUI.UpgradeStore?.RefreshAll(); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 597939e79..2847da846 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -553,36 +553,10 @@ namespace Barotrauma msg.Write(PurchasedItemRepairs); msg.Write(PurchasedLostShuttles); - msg.Write((UInt16)CargoManager.ItemsInBuyCrate.Count); - foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) - { - msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.ItemsInSellFromSubCrate.Count); - foreach (PurchasedItem pi in CargoManager.ItemsInSellFromSubCrate) - { - msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.PurchasedItems.Count); - foreach (PurchasedItem pi in CargoManager.PurchasedItems) - { - msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.SoldItems.Count); - foreach (SoldItem si in CargoManager.SoldItems) - { - msg.Write(si.ItemPrefab.Identifier); - msg.Write((UInt16)si.ID); - msg.Write(si.Removed); - msg.Write(si.SellerID); - msg.Write((byte)si.Origin); - } + WriteItems(msg, CargoManager.ItemsInBuyCrate); + WriteItems(msg, CargoManager.ItemsInSellFromSubCrate); + WriteItems(msg, CargoManager.PurchasedItems); + WriteItems(msg, CargoManager.SoldItems); msg.Write((ushort)UpgradeManager.PurchasedUpgrades.Count); foreach (var (prefab, category, level) in UpgradeManager.PurchasedUpgrades) @@ -644,50 +618,22 @@ namespace Barotrauma availableMissions.Add((missionIdentifier, connectionIndex)); } - UInt16? storeBalance = null; + var storeBalances = new Dictionary(); if (msg.ReadBoolean()) { - storeBalance = msg.ReadUInt16(); + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier identifier = msg.ReadIdentifier(); + UInt16 storeBalance = msg.ReadUInt16(); + storeBalances.Add(identifier, storeBalance); + } } - UInt16 buyCrateItemCount = msg.ReadUInt16(); - List buyCrateItems = new List(); - for (int i = 0; i < buyCrateItemCount; i++) - { - Identifier itemPrefabIdentifier = msg.ReadIdentifier(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); - } - - UInt16 subSellCrateItemCount = msg.ReadUInt16(); - List subSellCrateItems = new List(); - for (int i = 0; i < subSellCrateItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); - } - - UInt16 purchasedItemCount = msg.ReadUInt16(); - List purchasedItems = new List(); - for (int i = 0; i < purchasedItemCount; i++) - { - Identifier itemPrefabIdentifier = msg.ReadIdentifier(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); - } - - UInt16 soldItemCount = msg.ReadUInt16(); - List soldItems = new List(); - for (int i = 0; i < soldItemCount; i++) - { - Identifier itemPrefabIdentifier = msg.ReadIdentifier(); - UInt16 id = msg.ReadUInt16(); - bool removed = msg.ReadBoolean(); - byte sellerId = msg.ReadByte(); - byte origin = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); - } + 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(); @@ -756,7 +702,13 @@ namespace Barotrauma campaign.CargoManager.SetItemsInSubSellCrate(subSellCrateItems); campaign.CargoManager.SetPurchasedItems(purchasedItems); campaign.CargoManager.SetSoldItems(soldItems); - if (storeBalance.HasValue) { campaign.Map.CurrentLocation.StoreCurrentBalance = storeBalance.Value; } + 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) @@ -914,7 +866,7 @@ namespace Barotrauma WalletInfo info = transaction.Info; switch (transaction.CharacterID) { - case Some { Value: var charID}: + case Some { Value: var charID }: { Character targetCharacter = Character.CharacterList?.FirstOrDefault(c => c.ID == charID); if (targetCharacter is null) { break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index da8a40fad..891fc70b3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -121,7 +121,7 @@ namespace Barotrauma.Tutorials return new CharacterInfo( CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + JobPrefab.Prefabs["engineer"], Rand.RandSync.Unsynced, 0, new Skill("medical".ToIdentifier(), 0), new Skill("weapons".ToIdentifier(), 0), new Skill("mechanical".ToIdentifier(), 20), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index b26b2d32e..a9ea9047a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -147,7 +147,7 @@ namespace Barotrauma.Tutorials return new CharacterInfo( CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + JobPrefab.Prefabs["mechanic"], Rand.RandSync.Unsynced, 0, new Skill("medical".ToIdentifier(), 0), new Skill("weapons".ToIdentifier(), 0), new Skill("mechanical".ToIdentifier(), 50), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index 68d91df57..751cc4cd8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -128,7 +128,7 @@ namespace Barotrauma.Tutorials return new CharacterInfo( CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: new Job( - JobPrefab.Prefabs["medicaldoctor"], Rand.RandSync.Unsynced, 0, + JobPrefab.Prefabs["securityofficer"], Rand.RandSync.Unsynced, 0, new Skill("medical".ToIdentifier(), 20), new Skill("weapons".ToIdentifier(), 70), new Skill("mechanical".ToIdentifier(), 20), diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 2770fc3c0..abe401df3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -318,7 +318,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - var (share, percentage) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew(), Option.Some(reward)); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew().Where(c => c != controlled), Option.Some(reward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index e19ace1a9..4746f0e9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -966,7 +966,7 @@ namespace Barotrauma } else if (character.HeldItems.Any(i => i.OwnInventory != null && - (i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) + ((i.OwnInventory.CanBePut(item) && allowInventorySwap) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item))))) { return QuickUseAction.PutToEquippedItem; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index ad2a8cdf2..cc05194e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -56,7 +56,7 @@ namespace Barotrauma.Items.Components ContentXElement spriteElement = limbElement.GetChildElement("sprite"); if (spriteElement == null) { continue; } - string spritePath = spriteElement.Attribute("texture").Value; + string spritePath = spriteElement.GetAttribute("texture").Value; spritePath = characterInfo.ReplaceVars(spritePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index b153c5b80..02ad190c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -462,7 +462,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "guiframe": - if (subElement.Attribute("rect") != null) + if (subElement.GetAttribute("rect") != null) { DebugConsole.ThrowError($"Error in item config \"{item.ConfigFilePath}\" - GUIFrame defined as rect, use RectTransform instead."); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 597c90daa..5fe3adde1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,8 +1,7 @@ -using System; -using System.Linq; -using System.Xml.Linq; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; +using System.Linq; namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 7c7821844..39e714c89 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -2,12 +2,14 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class ItemLabel : ItemComponent, IDrawableComponent + partial class ItemLabel : ItemComponent, IDrawableComponent, IHasExtraTextPickerEntries { private GUITextBlock textBlock; @@ -94,7 +96,15 @@ namespace Barotrauma.Items.Components get { return textBlock == null ? 1.0f : textBlock.TextScale; } set { - if (textBlock != null) { textBlock.TextScale = MathHelper.Clamp(value, 0.1f, 10.0f); } + if (textBlock != null) + { + float prevScale = TextBlock.TextScale; + textBlock.TextScale = MathHelper.Clamp(value, 0.1f, 10.0f); + if (!MathUtils.NearlyEqual(prevScale, TextBlock.TextScale)) + { + SetScrollingText(); + } + } } } @@ -106,7 +116,7 @@ namespace Barotrauma.Items.Components set { scrollable = value; - IsActive = value; + IsActive = value || parseSpecialTextTagOnStart; TextBlock.Wrap = !scrollable; TextBlock.TextAlignment = scrollable ? Alignment.CenterLeft : Alignment.Center; } @@ -136,18 +146,23 @@ namespace Barotrauma.Items.Components { } + public IEnumerable GetExtraTextPickerEntries() + { + return SpecialTextTags; + } + private void SetScrollingText() { if (!scrollable) { return; } - float totalWidth = textBlock.Font.MeasureString(DisplayText).X; + float totalWidth = textBlock.Font.MeasureString(DisplayText).X * TextBlock.TextScale; float textAreaWidth = Math.Max(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z, 0); if (totalWidth >= textAreaWidth) { //add enough spaces to fill the rect //(so the text can scroll entirely out of view before we reset it back to start) needsScrolling = true; - float spaceWidth = textBlock.Font.MeasureChar(' ').X; + float spaceWidth = textBlock.Font.MeasureChar(' ').X * TextBlock.TextScale; scrollingText = new string(' ', (int)Math.Ceiling(textAreaWidth / spaceWidth)) + DisplayText.Value; } else @@ -166,7 +181,7 @@ namespace Barotrauma.Items.Components charWidths = new float[scrollingText.Length]; for (int i = 0; i < scrollingText.Length; i++) { - float charWidth = TextBlock.Font.MeasureChar(scrollingText[i]).X; + float charWidth = TextBlock.Font.MeasureChar(scrollingText[i]).X * TextBlock.TextScale; scrollPadding = Math.Max(charWidth, scrollPadding); charWidths[i] = charWidth; } @@ -174,9 +189,18 @@ namespace Barotrauma.Items.Components scrollIndex = MathHelper.Clamp(scrollIndex, 0, DisplayText.Length); } + private static readonly string[] SpecialTextTags = new string[] { "[CurrentLocationName]", "[CurrentBiomeName]", "[CurrentSubName]" }; + private bool parseSpecialTextTagOnStart; private void SetDisplayText(string value) { + if (SpecialTextTags.Contains(value)) + { + parseSpecialTextTagOnStart = true; + IsActive = true; + } + DisplayText = IgnoreLocalization ? value : TextManager.Get(value).Fallback(value); + TextBlock.Text = DisplayText; if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) { @@ -198,9 +222,37 @@ namespace Barotrauma.Items.Components }; } + private void ParseSpecialTextTag() + { + switch (text) + { + case "[CurrentLocationName]": + SetDisplayText(Level.Loaded?.StartLocation?.Name ?? string.Empty); + break; + case "[CurrentBiomeName]": + SetDisplayText(Level.Loaded?.LevelData?.Biome?.DisplayName.Value ?? string.Empty); + break; + case "[CurrentSubName]": + SetDisplayText(item.Submarine?.Info?.DisplayName.Value ?? string.Empty); + break; + default: + break; + } + } + public override void Update(float deltaTime, Camera cam) { - if (!scrollable) { return; } + if (parseSpecialTextTagOnStart) + { + ParseSpecialTextTag(); + parseSpecialTextTagOnStart = false; + } + + if (!scrollable) + { + IsActive = false; + return; + } if (scrollingText == null) { @@ -286,5 +338,6 @@ namespace Barotrauma.Items.Components { Text = msg.ReadString(); } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7d2a5101f..433243ee3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection.Metadata; @@ -46,9 +47,12 @@ namespace Barotrauma.Items.Components private GUITextBlock requiredTimeBlock; + [Serialize("FabricatorCreate", IsPropertySaveable.Yes)] + public string CreateButtonText { get; set; } + partial void InitProjSpecific() { - CreateGUI(); + //CreateGUI(); } protected override void OnResolutionChanged() @@ -68,9 +72,11 @@ namespace Barotrauma.Items.Components AutoScaleVertical = true }; - var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.02f + RelativeSpacing = 0.02f, + Stretch = true, + CanBeFocused = true }; // === TOP AREA === @@ -131,41 +137,55 @@ namespace Barotrauma.Items.Components // === BOTTOM AREA === // var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.3f), mainFrame.RectTransform), style: null); + if (inputContainer.Capacity > 0) + { // === SEPARATOR === // var separatorArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.15f), bottomFrame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.CenterLeft, isHorizontal: true) { - Stretch = true, + Stretch = true, RelativeSpacing = 0.03f }; - var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; - inputLabel.RectTransform.Resize(new Point((int) inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); - new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); + var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero }; + inputLabel.RectTransform.Resize(new Point((int)inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height)); + new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine"); // === INPUT AREA === // var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1f), bottomFrame.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft); - - // === INPUT SLOTS === // - inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); - new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; - // === ACTIVATE BUTTON === // - var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); - activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), - TextManager.Get("FabricatorCreate"), style: "DeviceButton") - { - OnClicked = StartButtonClicked, - UserData = selectedItem, - Enabled = false - }; - // === POWER WARNING === // - inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) - { - HoverColor = Color.Black, - IgnoreLayoutGroups = true, - Visible = false, - CanBeFocused = false - }; + // === INPUT SLOTS === // + inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); + new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; + + // === ACTIVATE BUTTON === // + var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); + activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), + TextManager.Get(CreateButtonText), style: "DeviceButton") + { + OnClicked = StartButtonClicked, + UserData = selectedItem, + Enabled = false + }; + } + else + { + bottomFrame.RectTransform.RelativeSize = new Vector2(1.0f, 0.1f); + activateButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomFrame.RectTransform, Anchor.CenterRight), + TextManager.Get(CreateButtonText), style: "DeviceButton") + { + OnClicked = StartButtonClicked, + UserData = selectedItem, + Enabled = false + }; + } + // === POWER WARNING === // + inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), + TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) + { + HoverColor = Color.Black, + IgnoreLayoutGroups = true, + Visible = false, + CanBeFocused = false + }; CreateRecipes(); } @@ -222,8 +242,12 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific() { - inputContainer.AllowUIOverlap = true; - inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; + CreateGUI(); + if (inputInventoryHolder != null) + { + inputContainer.AllowUIOverlap = true; + inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform; + } outputContainer.AllowUIOverlap = true; outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; } @@ -262,7 +286,7 @@ namespace Barotrauma.Items.Components var insufficientSkillsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), TextManager.Get("fabricatorinsufficientskills"), textColor: Color.Orange, font: GUIStyle.SubHeadingFont) - { + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -271,10 +295,14 @@ namespace Barotrauma.Items.Components { insufficientSkillsText.RectTransform.RepositionChildInHierarchy(itemList.Content.RectTransform.GetChildIndex(firstinSufficient.RectTransform)); } + else + { + sufficientSkillsText.Visible = false; + } var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont) - { + { AutoScaleHorizontal = true, CanBeFocused = false }; @@ -593,14 +621,28 @@ namespace Barotrauma.Items.Components float requiredTime = overrideRequiredTime ?? (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), - TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) + if (requiredTime > 0.0f) { - AutoScaleHorizontal = true, - }; - - requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), - font: GUIStyle.SmallFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), + TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) + { + AutoScaleHorizontal = true, + }; + requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), + font: GUIStyle.SmallFont); + } + + if (SelectedItem.RequiredMoney > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), + TextManager.Get("subeditor.price"), textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) + { + AutoScaleHorizontal = true, + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.FormatCurrency(SelectedItem.RequiredMoney), + font: GUIStyle.SmallFont); + + } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 2dc8ec191..36713eab5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -34,6 +34,13 @@ namespace Barotrauma.Items.Components [Serialize("0.5,0.5)", IsPropertySaveable.No)] public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); + [Serialize(true, IsPropertySaveable.No, description: "")] + public bool BreakFromMiddle + { + get; + set; + } + public Vector2 DrawSize { get @@ -124,9 +131,14 @@ namespace Barotrauma.Items.Components int width = (int)(SpriteWidth * snapState); if (width > 0.0f) - { - DrawRope(spriteBatch, endPos - diff * snapState * 0.5f, endPos, width); - DrawRope(spriteBatch, startPos, startPos + diff * snapState * 0.5f, width); + { + float positionMultiplier = snapState; + if (BreakFromMiddle) + { + positionMultiplier /= 2; + DrawRope(spriteBatch, endPos - diff * positionMultiplier, endPos, width); + } + DrawRope(spriteBatch, startPos, startPos + diff * positionMultiplier, width); } } else @@ -143,7 +155,7 @@ namespace Barotrauma.Items.Components float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); } - if (endSprite != null) + if (endSprite != null && (!Snapped || BreakFromMiddle)) { float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 1c27aaf3c..6224c628c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -1846,7 +1846,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private void ApplyReceivedState() + public void ApplyReceivedState() { if (receivedItemIDs == null || (Owner != null && Owner.Removed)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 8cb513864..3cc3610e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1198,9 +1198,9 @@ namespace Barotrauma Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { - if (ic is Repairable) + if (ic is Repairable r) { - if (!IsFullCondition) { color = Color.Cyan; } + if (r.IsBelowRepairThreshold) { color = Color.Cyan; } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 860e59d1a..864384257 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -173,7 +173,7 @@ namespace Barotrauma int groupID = 0; DecorativeSprite decorativeSprite = null; - if (subElement.Attribute("texture") == null) + if (subElement.GetAttribute("texture") == null) { groupID = subElement.GetAttributeInt("randomgroupid", 0); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 80d814e5f..264b5b2e7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -370,6 +370,7 @@ namespace Barotrauma.MapCreatures.Behavior private BallastFloraBranch ReadBranch(IReadMessage msg) { int id = msg.ReadInt32(); + bool isRootGrowth = msg.ReadBoolean(); byte type = (byte)msg.ReadRangedInteger(0b0000, 0b1111); byte sides = (byte)msg.ReadRangedInteger(0b0000, 0b1111); int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); @@ -385,7 +386,8 @@ namespace Barotrauma.MapCreatures.Behavior { ID = id, MaxHealth = maxHealth, - Sides = (TileSide) sides + Sides = (TileSide) sides, + IsRootGrowth = isRootGrowth }; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 92112a32a..0b68a66ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -217,8 +217,8 @@ namespace Barotrauma Vector2 mapTileSize = mapTile.size * generationParams.MapTileScale; int startX = (int)Math.Max(Math.Floor(location.MapPosition.X / mapTileSize.X - 0.25f), 0); int startY = (int)Math.Max(Math.Floor(location.MapPosition.Y / mapTileSize.Y - 0.25f), 0); - int endX = (int)Math.Min(Math.Floor(location.MapPosition.X / mapTileSize.X + 0.25f), mapTiles.GetLength(0)); - int endY = (int)Math.Min(Math.Floor(location.MapPosition.Y / mapTileSize.Y + 0.25f), mapTiles.GetLength(1)); + int endX = (int)Math.Min(Math.Floor(location.MapPosition.X / mapTileSize.X + 0.25f), mapTiles.GetLength(0) - 1); + int endY = (int)Math.Min(Math.Floor(location.MapPosition.Y / mapTileSize.Y + 0.25f), mapTiles.GetLength(1) - 1); for (int x = startX; x <= endX; x++) { for (int y = startY; y <= endY; y++) @@ -451,7 +451,7 @@ namespace Barotrauma SelectLocation(-1); if (GameMain.Client == null) { - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); ProgressWorld(); Radiation?.OnStep(1); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 8cd2acaaf..e69d0bbb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -272,11 +272,7 @@ namespace Barotrauma private GUIComponent CreateEditingHUD() { - int width = 500; - int height = spawnType == SpawnType.Path ? 80 : 200; - int x = GameMain.GraphicsWidth / 2 - width / 2, y = 30; - - editingHUD = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas) { ScreenSpaceOffset = new Point(x, y) }) + editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; @@ -284,7 +280,7 @@ namespace Barotrauma var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center)) { Stretch = true, - RelativeSpacing = 0.05f + AbsoluteSpacing = (int)(GUI.Scale * 5) }; if (spawnType == SpawnType.Path) @@ -418,6 +414,10 @@ namespace Barotrauma }; } + editingHUD.RectTransform.Resize(new Point( + editingHUD.Rect.Width, + (int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y))); + PositionEditingHUD(); return editingHUD; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 4c2721291..8f046b69d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -155,8 +155,13 @@ namespace Barotrauma.Networking Dispose(true); } } - - const int MaxFileSize = 50000000; //50 MB + + private static int GetMaxFileSizeInBytes(FileTransferType fileTransferType) => + fileTransferType switch + { + FileTransferType.Mod => 500 * 1024 * 1024, //500 MiB should be good enough, right? + _ => 50 * 1024 * 1024 //50 MiB for everything other than mods + }; public delegate void TransferInDelegate(FileTransferIn fileStreamReceiver); public TransferInDelegate OnFinished; @@ -410,18 +415,18 @@ namespace Barotrauma.Networking { errorMessage = ""; - if (fileSize > MaxFileSize) - { - errorMessage = "File too large (" + MathUtils.GetBytesReadable(fileSize) + ")"; - return false; - } - if (!Enum.IsDefined(typeof(FileTransferType), (int)type)) { errorMessage = "Unknown file type"; return false; } + if (fileSize > GetMaxFileSizeInBytes((FileTransferType)type)) + { + errorMessage = $"File too large ({MathUtils.GetBytesReadable(fileSize)} > {MathUtils.GetBytesReadable(GetMaxFileSizeInBytes((FileTransferType)type))})"; + return false; + } + if (string.IsNullOrEmpty(fileName) || fileName.IndexOfAny(Path.GetInvalidFileNameChars().ToArray()) > -1) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index 98073f188..cf2caabed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -163,16 +163,11 @@ namespace Barotrauma.Networking public static void ChangeCaptureDevice(string deviceName) { - var config = GameSettings.CurrentConfig; - config.Audio.VoiceCaptureDevice = deviceName; - GameSettings.SetCurrentConfig(config); + if (Instance == null) { return; } - if (Instance != null) - { - UInt16 storedBufferID = Instance.LatestBufferID; - Instance.Dispose(); - Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); - } + UInt16 storedBufferID = Instance.LatestBufferID; + Instance.Dispose(); + Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } IntPtr nativeBuffer; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 85570fcbd..8ce374562 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -54,7 +54,17 @@ namespace Barotrauma.Networking } else { - if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + try + { + if (VoipCapture.Instance == null) { VoipCapture.Create(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, storedBufferID); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"VoipCature.Create failed: {e.Message} {e.StackTrace.CleanupStackTrace()}"); + var config = GameSettings.CurrentConfig; + config.Audio.VoiceSetting = VoiceMode.Disabled; + GameSettings.SetCurrentConfig(config); + } if (VoipCapture.Instance == null || VoipCapture.Instance.EnqueuedTotalLength <= 0) { return; } } @@ -146,7 +156,7 @@ namespace Barotrauma.Networking { var soundIconStyle = GUIStyle.GetComponentStyle("GUISoundIcon"); Rectangle sourceRect = soundIconStyle.Sprites.First().Value.First().Sprite.SourceRect; - var indexPieces = soundIconStyle.Element.Attribute("sheetindices").Value.Split(';'); + var indexPieces = soundIconStyle.Element.GetAttribute("sheetindices").Value.Split(';'); voiceIconSheetRects = new Rectangle[indexPieces.Length]; for (int i = 0; i < indexPieces.Length; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index de2a858cc..519af766f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -242,30 +242,30 @@ namespace Barotrauma.Particles } //if velocity change in water is not given, it defaults to the normal velocity change - if (element.Attribute("velocitychangewater") == null) + if (element.GetAttribute("velocitychangewater") == null) { VelocityChangeWater = VelocityChange; } - if (element.Attribute("angularvelocity") != null) + if (element.GetAttribute("angularvelocity") != null) { AngularVelocityMin = element.GetAttributeFloat("angularvelocity", 0.0f); AngularVelocityMax = AngularVelocityMin; } - if (element.Attribute("startsize") != null) + if (element.GetAttribute("startsize") != null) { StartSizeMin = element.GetAttributeVector2("startsize", Vector2.One); StartSizeMax = StartSizeMin; } - if (element.Attribute("sizechange") != null) + if (element.GetAttribute("sizechange") != null) { SizeChangeMin = element.GetAttributeVector2("sizechange", Vector2.Zero); SizeChangeMax = SizeChangeMin; } - if (element.Attribute("startrotation") != null) + if (element.GetAttribute("startrotation") != null) { StartRotationMin = element.GetAttributeFloat("startrotation", 0.0f); StartRotationMax = StartRotationMin; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index e8b404bb2..efbb7b0cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -680,7 +680,7 @@ namespace Barotrauma //locationInfoPanel?.UpdateAuto(1.0f); } - public void SelectTab(CampaignMode.InteractionType tab) + public void SelectTab(CampaignMode.InteractionType tab, Identifier storeIdentifier = default) { if (Campaign.ShowCampaignUI || (Campaign.ForceMapUI && tab == CampaignMode.InteractionType.Map)) { @@ -724,8 +724,7 @@ namespace Barotrauma } break; case CampaignMode.InteractionType.Store: - Store.RefreshItemsToSell(); - Store.Refresh(); + Store.SelectStore(storeIdentifier); break; case CampaignMode.InteractionType.Crew: CrewManagement.UpdateCrew(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 56032d102..e9de25b54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2805,7 +2805,8 @@ namespace Barotrauma.CharacterEditor return false; } #endif - if (!character.IsHuman && !string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) + ContentPath texturePath = ContentPath.FromRaw(character.Prefab.ContentPackage, RagdollParams.Texture); + if (!character.IsHuman && (texturePath.IsNullOrWhiteSpace() || !File.Exists(texturePath.Value))) { DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}"); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index e58af763e..4efe53c5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -248,18 +248,24 @@ namespace Barotrauma } spriteBatch.End(); - //draw characters with deformable limbs last, because they can't be batched into SpriteBatch - //pretty hacky way of preventing draw order issues between normal and deformable sprites spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); - //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) - for (int i = Character.CharacterList.Count - 1; i >= 0; i--) - { - Character c = Character.CharacterList[i]; - if (!c.IsVisible || c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } - c.Draw(spriteBatch, Cam); - } + DrawDeformed(firstPass: true); + DrawDeformed(firstPass: false); spriteBatch.End(); + void DrawDeformed(bool firstPass) + { + //backwards order to render the most recently spawned characters in front (characters spawned later have a larger sprite depth) + for (int i = Character.CharacterList.Count - 1; i >= 0; i--) + { + Character c = Character.CharacterList[i]; + if (!c.IsVisible) { continue; } + if (c.Params.DrawLast == firstPass) { continue; } + if (c.AnimController.Limbs.All(l => l.DeformSprite == null)) { continue; } + c.Draw(spriteBatch, Cam); + } + } + Level.Loaded?.DrawFront(spriteBatch, cam); //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 470977caf..a1bbea8ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -32,7 +32,7 @@ namespace Barotrauma private readonly GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled, mirrorLevel; + private readonly GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; private Sprite editingSprite; @@ -126,6 +126,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { SerializeAll(); + GUI.AddMessage(TextManager.Get("leveleditor.allsaved"), GUIStyle.Green); return true; } }; @@ -169,6 +170,12 @@ namespace Barotrauma 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), + TextManager.Get("leveleditor.allowinvalidoutpost")) + { + ToolTip = TextManager.Get("leveleditor.allowinvalidoutpost.tooltip") + }; + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -179,6 +186,7 @@ namespace Barotrauma 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); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0ff737f3c..7fb77b452 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -46,6 +46,7 @@ namespace Barotrauma private GUITextBox serverNameBox, passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; + private GUIDropDown serverExecutableDropdown; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -418,7 +419,14 @@ 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) }); - var tutorialTypes = ReflectionUtils.GetDerivedNonAbstract(); + var tutorialTypes = new List() + { + typeof(MechanicTutorial), + typeof(EngineerTutorial), + typeof(DoctorTutorial), + typeof(OfficerTutorial), + typeof(CaptainTutorial), + }; foreach (Type tutorialType in tutorialTypes) { Tutorial tutorial = (Tutorial)Activator.CreateInstance(tutorialType); @@ -557,6 +565,35 @@ namespace Barotrauma GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.HostServer); }); return true; } + + serverExecutableDropdown.ListBox.Content.Children.ToArray() + .Where(c => c.UserData is ServerExecutableFile f && !ContentPackageManager.EnabledPackages.All.Contains(f.ContentPackage)) + .ForEach(serverExecutableDropdown.ListBox.RemoveChild); + var newServerExes + = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles()) + .Where(f => serverExecutableDropdown.ListBox.Content.Children.None(c => c.UserData == f)) + .ToArray(); + foreach (var newServerExe in newServerExes) + { + serverExecutableDropdown.AddItem($"{newServerExe.ContentPackage.Name} - {Path.GetFileNameWithoutExtension(newServerExe.Path.Value)}", userData: newServerExe); + } + serverExecutableDropdown.ListBox.Content.Children.ForEach(c => + { + c.RectTransform.RelativeSize = (1.0f, c.RectTransform.RelativeSize.Y); + c.ForceLayoutRecalculation(); + }); + bool serverExePickable = serverExecutableDropdown.ListBox.Content.CountChildren > 1; + serverExecutableDropdown.Parent.Visible + = serverExePickable; + serverExecutableDropdown.Parent.RectTransform.RelativeSize + = (1.0f, serverExePickable ? 0.1f : 0.0f); + serverExecutableDropdown.Parent.ForceLayoutRecalculation(); + (serverExecutableDropdown.Parent.Parent as GUILayoutGroup)?.Recalculate(); + if (serverExecutableDropdown.SelectedComponent is null) + { + serverExecutableDropdown.Select(0); + } + break; case Tab.Tutorials: if (!GameSettings.CurrentConfig.CampaignDisclaimerShown) @@ -784,7 +821,7 @@ namespace Barotrauma GameMain.ResetNetLobbyScreen(); try { - string exeName = "DedicatedServer.exe"; + string exeName = serverExecutableDropdown.SelectedComponent?.UserData is ServerExecutableFile f ? f.Path.Value : "DedicatedServer"; string arguments = "-name \"" + ToolBox.EscapeCharacters(name) + "\"" + " -public " + isPublicBox.Selected.ToString() + @@ -814,15 +851,20 @@ namespace Barotrauma arguments += " -ownerkey " + ownerKey; } - string filename = exeName; -#if LINUX || OSX - filename = "./" + Path.GetFileNameWithoutExtension(exeName); - //arguments = ToolBox.EscapeCharacters(arguments); + string filename = Path.Combine( + Path.GetDirectoryName(exeName), + Path.GetFileNameWithoutExtension(exeName)); +#if WINDOWS + filename += ".exe"; +#else + filename = "./" + exeName; #endif + var processInfo = new ProcessStartInfo { FileName = filename, Arguments = arguments, + WorkingDirectory = Directory.GetCurrentDirectory(), #if !DEBUG CreateNoWindow = true, UseShellExecute = false, @@ -1184,12 +1226,12 @@ namespace Barotrauma label.RectTransform.MaxSize = serverNameBox.RectTransform.MaxSize; var maxPlayersLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("MaxPlayers"), textAlignment: textAlignment); - var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true) + var buttonContainer = new GUILayoutGroup(new RectTransform(textFieldSize, maxPlayersLabel.RectTransform, Anchor.CenterRight), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.1f }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton", textAlignment: Alignment.Center) { UserData = -1, OnClicked = ChangeMaxPlayers @@ -1209,7 +1251,7 @@ namespace Barotrauma currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers, 1, NetConfig.MaxPlayers); maxPlayersBox.Text = currMaxPlayers.ToString(); }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) + new GUIButton(new RectTransform(Vector2.One, buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { UserData = 1, OnClicked = ChangeMaxPlayers @@ -1223,6 +1265,41 @@ namespace Barotrauma }; label.RectTransform.MaxSize = passwordBox.RectTransform.MaxSize; + var serverExecutableLabel = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), + TextManager.Get("ServerExecutable"), textAlignment: textAlignment); + const string vanillaServerOption = "Vanilla"; + serverExecutableDropdown + = new GUIDropDown(new RectTransform(textFieldSize, serverExecutableLabel.RectTransform, Anchor.CenterRight), + vanillaServerOption); + var listBoxSize = serverExecutableDropdown.ListBox.RectTransform.RelativeSize; + serverExecutableDropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.5f, listBoxSize.Y); + serverExecutableDropdown.AddItem(vanillaServerOption, userData: null); + serverExecutableDropdown.OnSelected = (selected, userData) => + { + if (userData != null) + { + var warningBox = new GUIMessageBox(headerText: TextManager.Get("Warning"), + text: TextManager.GetWithVariable("ModServerExesAtYourOwnRisk", "[exename]", serverExecutableDropdown.Text), + new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); + warningBox.Buttons[0].OnClicked = (_, __) => + { + warningBox.Close(); + return false; + }; + warningBox.Buttons[1].OnClicked = (_, __) => + { + serverExecutableDropdown.Select(0); + warningBox.Close(); + return false; + }; + } + + serverExecutableDropdown.Text = ToolBox.LimitString(serverExecutableDropdown.Text, + serverExecutableDropdown.Font, serverExecutableDropdown.Rect.Width * 8 / 10); + + return true; + }; + // tickbox upper --------------- var tickboxAreaUpper = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, tickBoxSize.Y), parent.RectTransform), isHorizontal: true); @@ -1312,8 +1389,8 @@ namespace Barotrauma { var client = new RestClient(RemoteContentUrl); var request = new RestRequest("MenuContent.xml", Method.GET); - client.ExecuteAsync(request, RemoteContentReceived); - CoroutineManager.StartCoroutine(WairForRemoteContentReceived()); + TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), + RemoteContentReceived); } catch (Exception e) @@ -1327,58 +1404,31 @@ namespace Barotrauma } } - private IEnumerable WairForRemoteContentReceived() + private void RemoteContentReceived(Task t) { - while (true) + try { - lock (remoteContentLock) + if (!t.TryGetResult(out IRestResponse remoteContentResponse)) { throw new Exception("Task did not return a valid result"); } + string xml = remoteContentResponse.Content; + int index = xml.IndexOf('<'); + if (index > 0) { xml = xml.Substring(index, xml.Length - index); } + if (!string.IsNullOrWhiteSpace(xml)) { - if (remoteContentResponse != null) { break; } - } - yield return new WaitForSeconds(0.1f); - } - lock (remoteContentLock) - { - if (remoteContentResponse.ResponseStatus != ResponseStatus.Completed || remoteContentResponse.StatusCode != HttpStatusCode.OK) - { - yield return CoroutineStatus.Success; - } - - try - { - string xml = remoteContentResponse.Content; - int index = xml.IndexOf('<'); - if (index > 0) { xml = xml.Substring(index, xml.Length - index); } - if (!string.IsNullOrWhiteSpace(xml)) + remoteContentDoc = XDocument.Parse(xml); + foreach (var subElement in remoteContentDoc?.Root.Elements()) { - remoteContentDoc = XDocument.Parse(xml); - foreach (var subElement in remoteContentDoc?.Root.Elements()) - { - GUIComponent.FromXML(subElement.FromPackage(null), remoteContentContainer.RectTransform); - } + 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.WairForRemoteContentReceived:Exception", GameAnalyticsManager.ErrorSeverity.Error, - "Reading received remote main menu content failed. " + e.Message); - } } - yield return CoroutineStatus.Success; - } - private readonly object remoteContentLock = new object(); - private IRestResponse remoteContentResponse; - - private void RemoteContentReceived(IRestResponse response, RestRequestAsyncHandle handle) - { - lock (remoteContentLock) + catch (Exception e) { - remoteContentResponse = response; +#if DEBUG + DebugConsole.ThrowError("Reading received remote main menu content failed.", e); +#endif + GameAnalyticsManager.AddErrorEventOnce("MainMenuScreen.RemoteContentReceived: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 478571b01..681edc43a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -29,10 +30,19 @@ namespace Barotrauma currentDownload = null; confirmDownload = false; } + + private void DeletePrevDownloads() + { + if (Directory.Exists(ModReceiver.DownloadFolder)) + { + Directory.Delete(ModReceiver.DownloadFolder, recursive: true); + } + } public override void Select() { base.Select(); + DeletePrevDownloads(); Reset(); Frame.ClearChildren(); @@ -67,6 +77,18 @@ namespace Barotrauma .Where(sp => sp.ContentPackage is null).ToArray(); if (!missingPackages.Any()) { + if (!GameMain.Client.IsServerOwner) + { + ContentPackageManager.EnabledPackages.BackUp(); + ContentPackageManager.EnabledPackages.SetCore( + GameMain.Client.ClientPeer.ServerContentPackages + .Select(p => p.CorePackage) + .First(p => p != null)); + ContentPackageManager.EnabledPackages.SetRegular( + GameMain.Client.ClientPeer.ServerContentPackages + .Select(p => p.RegularPackage) + .Where(p => p != null).ToArray()); + } GameMain.NetLobbyScreen.Select(); return; } @@ -201,7 +223,7 @@ namespace Barotrauma if (!pendingDownloads.Contains(p)) { - downloadProgress.ClearChildren(); + downloadProgress.GetAllChildren().ToArray().ForEach(c => downloadProgress.RemoveChild(c)); return 1.0f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 8abc2da45..af54e3654 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -3786,7 +3786,7 @@ namespace Barotrauma RelativeSpacing = 0.02f }; - var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIDragIndicator") { CanBeFocused = false diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 7e5b5e6ae..aad8f3493 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -380,7 +380,7 @@ namespace Barotrauma string spriteFolder = ""; ContentPath texturePath = null; - if (element.Attribute("texture") != null) + if (element.GetAttribute("texture") != null) { texturePath = element.GetAttributeContentPath("texture"); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 2e55fe64b..7e6abbd01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -9,11 +9,7 @@ using System.Linq; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; -#if DEBUG -using System.IO; -#else using Barotrauma.IO; -#endif namespace Barotrauma { @@ -1262,7 +1258,9 @@ namespace Barotrauma } textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - if (ep.Category == MapEntityCategory.ItemAssembly) + if (ep.Category == MapEntityCategory.ItemAssembly + && ep.ContentPackage?.Files.Length == 1 + && ContentPackageManager.LocalPackages.Contains(ep.ContentPackage)) { var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, TextManager.Get("Delete"), style: "GUIButtonSmall") @@ -1978,6 +1976,7 @@ namespace Barotrauma if (newPackage is RegularPackage regular) { ContentPackageManager.EnabledPackages.EnableRegular(regular); + GameSettings.SaveCurrentConfig(); } } SubmarineInfo.RefreshSavedSub(savePath); @@ -2164,12 +2163,12 @@ namespace Barotrauma var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"Any").ToEnumerable()), selectMultiple: true); - allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any"); + allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any".ToIdentifier()); if (MainSub.Info.OutpostModuleInfo == null || !MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")) { - allowAttachDropDown.SelectItem("any"); + allowAttachDropDown.SelectItem("any".ToIdentifier()); } foreach (Identifier flag in availableFlags) { @@ -2211,7 +2210,7 @@ namespace Barotrauma locationTypeDropDown.SelectItem(locationType); } } - if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any"); } + if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any".ToIdentifier()); } locationTypeDropDown.OnSelected += (_, __) => { @@ -2225,9 +2224,7 @@ namespace Barotrauma // gap positions --------------------- var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), TextManager.Get("outpostmodulegappositions"), textAlignment: Alignment.CenterLeft); - var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), text: "", selectMultiple: true); @@ -2238,11 +2235,11 @@ namespace Barotrauma { outpostModuleInfo.DetermineGapPositions(MainSub); } - foreach (var gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + foreach (OutpostModuleInfo.GapPosition gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) { - if ((OutpostModuleInfo.GapPosition)gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + if (gapPos == OutpostModuleInfo.GapPosition.None) { continue; } gapPositionDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); - if (outpostModuleInfo.GapPositions.HasFlag((OutpostModuleInfo.GapPosition)gapPos)) + if (outpostModuleInfo.GapPositions.HasFlag(gapPos)) { gapPositionDropDown.SelectItem(gapPos); } @@ -2271,6 +2268,49 @@ namespace Barotrauma }; gapPositionGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), TextManager.Get("canattachtoprevious"), textAlignment: Alignment.CenterLeft) + { + ToolTip = TextManager.Get("canattachtoprevious.tooltip") + }; + var canAttachToPrevDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), + text: "", selectMultiple: true); + if (outpostModuleInfo != null) + { + foreach (OutpostModuleInfo.GapPosition gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition))) + { + if (gapPos == OutpostModuleInfo.GapPosition.None) { continue; } + canAttachToPrevDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos); + if (outpostModuleInfo.CanAttachToPrevious.HasFlag(gapPos)) + { + canAttachToPrevDropDown.SelectItem(gapPos); + } + } + } + + canAttachToPrevDropDown.OnSelected += (_, __) => + { + if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; } + Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious = OutpostModuleInfo.GapPosition.None; + if (canAttachToPrevDropDown.SelectedDataMultiple.Any()) + { + List gapPosTexts = new List(); + foreach (OutpostModuleInfo.GapPosition gapPos in canAttachToPrevDropDown.SelectedDataMultiple) + { + Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious |= gapPos; + gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString()).Value); + } + canAttachToPrevDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); + } + else + { + canAttachToPrevDropDown.Text = ToolBox.LimitString("None", canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width); + } + return true; + }; + canAttachToPrevGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + // ------------------- var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), outpostSettingsContainer.RectTransform), isHorizontal: true) @@ -2583,7 +2623,18 @@ namespace Barotrauma //don't show content packages that only define submarine files //(it doesn't make sense to require another sub to be installed to install this one) if (contentPack.Files.All(f => f is SubmarineFile)) { continue; } - if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } + + if (!contentPacks.Contains(contentPack.Name)) + { + string altName = contentPack.AltNames.FirstOrDefault(n => contentPacks.Contains(n)); + if (!string.IsNullOrEmpty(altName)) + { + MainSub.Info.RequiredContentPackages.Remove(altName); + MainSub.Info.RequiredContentPackages.Add(contentPack.Name); + contentPacks.Remove(altName); + } + contentPacks.Add(contentPack.Name); + } } foreach (string contentPackageName in contentPacks) @@ -2749,11 +2800,7 @@ namespace Barotrauma } bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; -#if DEBUG - string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; -#else string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); -#endif string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform(); if (File.Exists(filePath)) { @@ -2782,26 +2829,27 @@ namespace Barotrauma void Save() { - XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); -#if DEBUG - doc.Save(filePath); -#else - doc.SaveSafe(filePath); -#endif - ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); + ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.Regular.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath)); if (existingContentPackage == null) { //content package doesn't exist, create one ModProject modProject = new ModProject() { Name = nameBox.Text }; - var newFile = ModProject.File.FromPath(filePath); + var newFile = ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml")); modProject.AddFile(newFile); - ContentPackageManager.LocalPackages.SaveAndEnableRegularMod(modProject); - } - else - { - EnqueueForReload(existingContentPackage); + string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject); + existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath); } + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); + doc.SaveSafe(filePath); + + var resultPackage = ContentPackageManager.ReloadContentPackage(existingContentPackage) as RegularPackage; + if (!ContentPackageManager.EnabledPackages.Regular.Contains(resultPackage)) + { + ContentPackageManager.EnabledPackages.EnableRegular(resultPackage); + GameSettings.SaveCurrentConfig(); + } + UpdateEntityList(); } @@ -2884,11 +2932,8 @@ namespace Barotrauma { if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { -#if DEBUG - deleteBtn.Enabled = true; -#else - deleteBtn.Enabled = userData is SubmarineInfo subInfo && !subInfo.IsVanillaSubmarine(); -#endif + deleteBtn.Enabled = userData is SubmarineInfo subInfo + && (GetContentPackageIntrinsicallyTiedToSub(subInfo) != null || Path.GetDirectoryName(subInfo.FilePath) == SubmarineInfo.SavePath); } return true; } @@ -3141,18 +3186,10 @@ namespace Barotrauma ReconstructLayers(); } - private RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) - { - foreach (RegularPackage regularPackage in ContentPackageManager.RegularPackages) - { - if (regularPackage.Files.Length == 1 && regularPackage.Files[0].Path == sub.FilePath) - { - return regularPackage; - } - } - - return null; - } + private static RegularPackage GetContentPackageIntrinsicallyTiedToSub(SubmarineInfo sub) + => ContentPackageManager.LocalPackages.Regular + .Where(p => p.Files.Length == 1) + .FirstOrDefault(regularPackage => regularPackage.Files[0].Path == sub.FilePath); private void TryDeleteSub(SubmarineInfo sub) { @@ -3160,8 +3197,10 @@ 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); - if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } + bool isInOldSavePath = Path.GetDirectoryName(sub.FilePath) == SubmarineInfo.SavePath; + if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage) && !isInOldSavePath) { return; } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), @@ -3171,10 +3210,18 @@ namespace Barotrauma { try { - Directory.Delete(Path.GetDirectoryName(subPackage.Path), true); + if (subPackage != null) + { + Directory.Delete(Path.GetDirectoryName(subPackage.Path), recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); + } + else if (isInOldSavePath && File.Exists(sub.FilePath)) + { + File.Delete(sub.FilePath); + } sub.Dispose(); - File.Delete(sub.FilePath); SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index f8186ff57..af4eba036 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -390,13 +390,13 @@ namespace Barotrauma } GUIComponent propertyField = null; - if (value is bool) + if (value is bool boolVal) { - propertyField = CreateBoolField(entity, property, (bool)value, displayName, toolTip); + propertyField = CreateBoolField(entity, property, boolVal, displayName, toolTip); } - else if (value is string) + else if (value is string stringVal) { - propertyField = CreateStringField(entity, property, (string)value, displayName, toolTip); + propertyField = CreateStringField(entity, property, stringVal, displayName, toolTip); } else if (value.GetType().IsEnum) { @@ -1277,7 +1277,7 @@ namespace Barotrauma public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { - var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); + var msgBox = new GUIMessageBox("", "", new LocalizedString[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var textList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), msgBox.Content.RectTransform, Anchor.TopCenter)) @@ -1307,6 +1307,18 @@ namespace Barotrauma UserData = tagTextPair.Key.ToString() }; } + + if (entity is IHasExtraTextPickerEntries hasExtraTextPickerEntries) + { + foreach (string extraEntry in hasExtraTextPickerEntries.GetExtraTextPickerEntries()) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textList.Content.RectTransform) { MinSize = new Point(0, 20) }, + ToolBox.LimitString(extraEntry, GUIStyle.Font, textList.Content.Rect.Width), GUIStyle.Green) + { + UserData = extraEntry + }; + } + } } private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property) @@ -1445,4 +1457,12 @@ namespace Barotrauma } } } + + /// + /// Implement this interface to insert extra entires to the text pickers created for the SerializableEntityEditors of the entity + /// + interface IHasExtraTextPickerEntries + { + public IEnumerable GetExtraTextPickerEntries(); + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index ca6ea6874..cd908d386 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -40,7 +40,7 @@ namespace Barotrauma if (sound?.Sound != null) { loopSound = subElement.GetAttributeBool("loop", false); - if (subElement.Attribute("selectionmode") != null) + if (subElement.GetAttribute("selectionmode") != null) { if (Enum.TryParse(subElement.GetAttributeString("selectionmode", "Random"), out SoundSelectionMode selectionMode)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs index c94e33092..5ab0a9d8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs @@ -533,6 +533,9 @@ namespace Barotrauma.Steam private void PopulateFrameWithItemInfo(Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) { taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; + + var contentPackage + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform)); @@ -567,34 +570,74 @@ namespace Barotrauma.Steam RectTransform rightSideButtonRectT() => new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight); + + bool reinstallAction(GUIButton button, object o) + { + TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.Reinstall(workshopItem), t => + { + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); + }); + return false; + } + + var (updateButton, updateSprite) = CreatePaddedButton( + rightSideButtonRectT(), + "GUIUpdateButton", + spriteScale: 0.8f); + updateButton.ToolTip = TextManager.Get("WorkshopItemUpdate"); + updateButton.Visible = false; + updateButton.OnClicked = reinstallAction; + + if (contentPackage != null) + { + TaskPool.Add( + $"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", + contentPackage.IsUpToDate(), + t => + { + if (!t.TryGetResult(out bool isUpToDate)) { return; } + + updateButton.Visible = !isUpToDate; + }); + } var (reinstallButton, reinstallSprite) = CreatePaddedButton( rightSideButtonRectT(), "GUIReloadButton", spriteScale: 0.8f); reinstallButton.ToolTip = TextManager.Get("WorkshopItemReinstall"); - reinstallButton.OnClicked += (button, o) => - { - SteamManager.Workshop.Uninstall(workshopItem); - TaskPool.Add($"Reinstall{workshopItem.Id}", SteamManager.Workshop.ForceRedownload(workshopItem), t => { }); - return false; - }; + reinstallButton.OnClicked = reinstallAction; var reinstallButtonUpdater = new GUICustomComponent( new RectTransform(Vector2.Zero, reinstallButton.RectTransform), onUpdate: (f, component) => { - reinstallButton.Visible = workshopItem.IsSubscribed; + reinstallButton.Visible = workshopItem.IsSubscribed || workshopItem.Owner.Id == SteamManager.GetSteamID(); reinstallButton.Enabled = !workshopItem.IsDownloading && !workshopItem.IsDownloadPending && !SteamManager.Workshop.IsInstalling(workshopItem); + reinstallSprite.Color = reinstallButton.Enabled ? reinstallSprite.Style.Color : Color.DimGray; + updateButton.Enabled = reinstallButton.Enabled && contentPackage != null && ContentPackageManager.WorkshopPackages.Contains(contentPackage); + updateSprite.Color = reinstallSprite.Color; + + if (contentPackage != null + && !ContentPackageManager.WorkshopPackages.Contains(contentPackage) + && ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) + { + updateButton.Visible = false; + } }); CreateSubscribeButton(workshopItem, rightSideButtonRectT(), spriteScale: 0.8f); + + var padding = new GUIFrame( + new RectTransform((0.15f, 1.0f), headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), + style: null); - var padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null); + padding = new GUIFrame(new RectTransform((1.0f, 0.015f), verticalLayout.RectTransform), style: null); var horizontalLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.45f), verticalLayout.RectTransform), isHorizontal: true) @@ -638,7 +681,7 @@ namespace Barotrauma.Steam isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; var starColor = Color.Lerp( - Color.Lerp(Color.Red, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)), + Color.Lerp(Color.White, Color.Yellow, Math.Min(workshopItem.Score * 2.0f, 1.0f)), Color.Lime, Math.Max(0.0f, (workshopItem.Score - 0.5f) * 2.0f)); for (int i = 0; i < 5; i++) { @@ -652,12 +695,23 @@ namespace Barotrauma.Steam star.SelectedColor = starColor; } } - var scoreVoteCountPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), + var scoreTextPadding = new GUIFrame(new RectTransform((0.5f, 1.0f), scoreStarContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: null); + + var scoreTextContainer = new GUIFrame(new RectTransform(Vector2.One, scoreStarContainer.RectTransform), + style: null); + var scoreVoteCount = new GUITextBlock( - new RectTransform(Vector2.One, scoreStarContainer.RectTransform), + new RectTransform((1.0f, 1.5f), scoreTextContainer.RectTransform, Anchor.Center), TextManager.GetWithVariable("WorkshopItemVotes", "[VoteCount]", - (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.CenterLeft) + (workshopItem.VotesUp + workshopItem.VotesDown).ToString()), textAlignment: Alignment.BottomLeft) + { + Padding = Vector4.Zero + }; + var subscriptionCount = new GUITextBlock( + new RectTransform((1.0f, 1.5f), scoreTextContainer.RectTransform, Anchor.Center), + TextManager.GetWithVariable("WorkshopItemSubscriptions", "[SubscriptionCount]", + workshopItem.NumUniqueSubscriptions.ToString()), textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs index 45fa95f50..720121b09 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs @@ -89,23 +89,22 @@ namespace Barotrauma.Steam return (fileCount, byteCount); } + + private void DeselectPublishedItem() + { + var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); + Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } + ? action + : null; + deselectAction?.Invoke(); + SelectTab(Tab.Publish); + } private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame) { ContentPackageManager.LocalPackages.Refresh(); ContentPackageManager.WorkshopPackages.Refresh(); - var deselectCarrier = selfModsList.Parent.FindChild(c => c.UserData is ActionCarrier { Id: var id } && id == "deselect"); - Action? deselectAction = deselectCarrier.UserData is ActionCarrier { Action: var action } - ? action - : null; - - void deselectItem() - { - deselectAction?.Invoke(); - SelectTab(Tab.Publish); - } - parentFrame.ClearChildren(); GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform), childAnchor: Anchor.TopCenter); @@ -146,7 +145,7 @@ namespace Barotrauma.Steam { OnClicked = (button, o) => { - deselectItem(); + DeselectPublishedItem(); return false; } }; @@ -285,7 +284,7 @@ namespace Barotrauma.Steam RectTransform newButtonRectT() => new RectTransform((0.4f, 1.0f), buttonLayout.RectTransform); - var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get("WorkshopItemPublish")) + var publishItemButton = new GUIButton(newButtonRectT(), TextManager.Get(workshopItem.Id != 0 ? "WorkshopItemUpdate" : "WorkshopItemPublish")) { OnClicked = (button, o) => { @@ -333,8 +332,9 @@ namespace Barotrauma.Steam TaskPool.Add($"Delete{workshopItem.Id}", Steamworks.SteamUGC.DeleteFileAsync(workshopItem.Id), t => { + SteamManager.Workshop.Uninstall(workshopItem); confirmDeletion.Close(); - deselectItem(); + DeselectPublishedItem(); }); return false; }; @@ -364,24 +364,10 @@ namespace Barotrauma.Steam return false; }; - var coroutineEval = subcoroutine(messageBox.Text, messageBox); + var coroutineEval = subcoroutine(messageBox.Text, messageBox).GetEnumerator(); while (true) { - bool moveNext = true; - try - { - moveNext = coroutineEval.GetEnumerator().MoveNext(); - } - catch (Exception e) - { - DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); - messageBox.Close(); - } - if (!moveNext) - { - messageBox.Close(); - } - var status = coroutineEval.GetEnumerator().Current; + var status = coroutineEval.Current; if (messageBox.Closed) { yield return CoroutineStatus.Success; @@ -397,6 +383,20 @@ namespace Barotrauma.Steam { yield return status; } + bool moveNext = true; + try + { + moveNext = coroutineEval.MoveNext(); + } + catch (Exception e) + { + DebugConsole.ThrowError($"{e.Message} {e.StackTrace.CleanupStackTrace()}"); + messageBox.Close(); + } + if (!moveNext) + { + messageBox.Close(); + } } } @@ -408,26 +408,9 @@ namespace Barotrauma.Steam { if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) { - //Must download! - while (!SteamManager.Workshop.CanBeInstalled(workshopItem)) - { - bool shouldForceInstall = workshopItem.IsInstalled - && Directory.Exists(workshopItem.Directory) - && !SteamManager.Workshop.IsItemDirectoryUpToDate(workshopItem); - shouldForceInstall |= workshopItem is - { IsDownloading: false, IsDownloadPending: false, IsInstalled: false }; - if (shouldForceInstall) - { - SteamManager.Workshop.ForceRedownload(workshopItem); - } - currentStepText.Text = TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); - yield return new WaitForSeconds(0.5f); - } - } - else - { - SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); + SteamManager.Workshop.NukeDownload(workshopItem); } + SteamManager.Workshop.DownloadModThenEnqueueInstall(workshopItem); TaskPool.Add($"Install {workshopItem.Title}", SteamManager.Workshop.WaitForInstall(workshopItem), (t) => @@ -436,7 +419,9 @@ namespace Barotrauma.Steam }); while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id)) { - currentStepText.Text = TextManager.Get("PublishPopupInstall"); + currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem) + ? TextManager.Get("PublishPopupInstall") + : TextManager.GetWithVariable("PublishPopupDownload", "[percentage]", Percentage(workshopItem.DownloadAmount)); yield return new WaitForSeconds(0.5f); } @@ -457,7 +442,6 @@ namespace Barotrauma.Steam currentStepText.Text = TextManager.Get("PublishPopupCreateLocal"); yield return new WaitForSeconds(0.5f); } - PopulatePublishTab(workshopItem, parentFrame); yield return CoroutineStatus.Success; @@ -500,7 +484,10 @@ namespace Barotrauma.Steam editor.SubmitAsync(), t => { - t.TryGetResult(out result); + if (t.TryGetResult(out Steamworks.Ugc.PublishResult publishResult)) + { + result = publishResult; + } resultException = t.Exception?.GetInnermost(); }); currentStepText.Text = TextManager.Get("PublishPopupSubmit"); @@ -523,6 +510,14 @@ namespace Barotrauma.Steam $"exception was {downloadTask.Exception?.GetInnermost()?.ToString().CleanupStackTrace() ?? "[NULL]"}"); } + ContentPackage? pkgToNuke + = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); + if (pkgToNuke != null) + { + Directory.Delete(pkgToNuke.Dir, recursive: true); + ContentPackageManager.WorkshopPackages.Refresh(); + } + bool installed = false; TaskPool.Add( "InstallNewlyPublished", @@ -537,13 +532,17 @@ namespace Barotrauma.Steam yield return new WaitForSeconds(0.5f); } + ContentPackageManager.WorkshopPackages.Refresh(); + ContentPackageManager.EnabledPackages.RefreshUpdatedMods(); + var localModProject = new ModProject(localPackage) { SteamWorkshopId = resultId }; + localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); ContentPackageManager.ReloadContentPackage(localPackage); - ContentPackageManager.WorkshopPackages.Refresh(); + DeselectPublishedItem(); if (result.Value.NeedsWorkshopAgreement) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs index fe499a4d9..3a90d6ea1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -8,6 +8,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Barotrauma.Extensions; namespace Barotrauma.Steam { @@ -117,7 +118,7 @@ namespace Barotrauma.Steam if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } var client = new RestClient(thumbnailUrl); var request = new RestRequest(".", Method.GET); - IRestResponse response = await client.ExecuteTaskAsync(request, cancellationToken); + IRestResponse response = await client.ExecuteAsync(request, cancellationToken); if (response is { StatusCode: System.Net.HttpStatusCode.OK, ResponseStatus: ResponseStatus.Completed }) { using var dataStream = new System.IO.MemoryStream(); @@ -207,6 +208,11 @@ namespace Barotrauma.Steam } string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; + if (File.Exists(newPath) || Directory.Exists(newPath)) + { + newPath += $"_{contentPackage.SteamWorkshopId}"; + } + if (File.Exists(newPath) || Directory.Exists(newPath)) { throw new Exception($"{newPath} already exists"); @@ -256,6 +262,18 @@ namespace Barotrauma.Steam } } + public static async Task Reinstall(Steamworks.Ugc.Item workshopItem) + { + NukeDownload(workshopItem); + var toUninstall + = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) + .ToHashSet(); + toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); + CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh()); + DownloadModThenEnqueueInstall(workshopItem); + await WaitForInstall(workshopItem); + } + public static async Task WaitForInstall(Steamworks.Ugc.Item item) => await WaitForInstall(item.Id); @@ -263,6 +281,7 @@ namespace Barotrauma.Steam { var installWaiter = new InstallWaiter(item); while (installWaiter.Waiting) { await Task.Delay(500); } + await Task.Delay(500); } public static void OnItemDownloadComplete(ulong id, bool forceInstall = false) @@ -276,7 +295,8 @@ namespace Barotrauma.Steam return; } else if (CanBeInstalled(id) - && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id)) + && !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id) + && !InstallTaskCounter.IsInstalling(id)) { TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs index feab2338c..59b42ef78 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs @@ -31,6 +31,7 @@ namespace Barotrauma.Steam private readonly GUIListBox enabledRegularModsList; private readonly GUIListBox disabledRegularModsList; private readonly Action onInstalledInfoButtonHit; + private readonly GUITextBox modsListFilter; private CancellationTokenSource taskCancelSrc = new CancellationTokenSource(); private readonly HashSet itemThumbnails = new HashSet(); @@ -47,7 +48,12 @@ namespace Barotrauma.Steam contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); - CreateInstalledModsTab(out enabledCoreDropdown, out enabledRegularModsList, out disabledRegularModsList, out onInstalledInfoButtonHit); + CreateInstalledModsTab( + out enabledCoreDropdown, + out enabledRegularModsList, + out disabledRegularModsList, + out onInstalledInfoButtonHit, + out modsListFilter); CreatePopularModsTab(out popularModsList); CreatePublishTab(out selfModsList); @@ -176,7 +182,8 @@ namespace Barotrauma.Steam out GUIDropDown enabledCoreDropdown, out GUIListBox enabledRegularModsList, out GUIListBox disabledRegularModsList, - out Action onInstalledInfoButtonHit) + out Action onInstalledInfoButtonHit, + out GUITextBox modsListFilter) { GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); @@ -287,7 +294,7 @@ namespace Barotrauma.Steam var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); searchRectT.RelativeSize = (0.5f, searchRectT.RelativeSize.Y); var searchHolder = new GUIFrame(searchRectT, style: null); - var searchBox = new GUITextBox(new RectTransform(Vector2.One, searchHolder.RectTransform), ""); + 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}, textColor: Color.DarkGray * 0.6f, text: TextManager.Get("Search") + "...", @@ -300,12 +307,10 @@ namespace Barotrauma.Steam searchBox.OnTextChanged += (sender, str) => { - enabledModsList.Content.Children.Concat(disabledModsList.Content.Children) - .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() - || (c.UserData is ContentPackage p - && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + UpdateModListItemVisibility(); return true; }; + modsListFilter = searchBox; new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), onUpdate: (f, component) => @@ -320,6 +325,15 @@ namespace Barotrauma.Steam }); } + private 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 PopulateInstalledModLists() { ContentPackageManager.UpdateContentPackageList(); @@ -344,15 +358,21 @@ namespace Barotrauma.Steam RelativeSpacing = 0.02f }; - var dragIndicator = new GUIButton(new RectTransform((0.1f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), + 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)); - var modName = new GUITextBlock(new RectTransform(Vector2.One, modNameScissor.Content.RectTransform), text: mod.Name); + 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 (ContentPackageManager.LocalPackages.Contains(mod)) { var editButton = new GUIButton(new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", @@ -418,6 +438,8 @@ namespace Barotrauma.Steam if (ContentPackageManager.EnabledPackages.Regular.Contains(mod)) { continue; } addRegularModToList(mod, disabledRegularModsList); } + + UpdateModListItemVisibility(); } private void CreatePopularModsTab(out GUIListBox popularModsList) diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 80a620bcc..65d8b3408 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico @@ -133,7 +133,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 20a82c153..d6beedd53 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico @@ -128,7 +128,7 @@ - + diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 35a96ffc0..539cc7653 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma ..\BarotraumaShared\Icon.ico @@ -135,7 +135,7 @@ - + diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index c599af014..dbda5fa53 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 036a8d02c..883d5184f 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 49405004e..2e5acdab7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -5,7 +5,7 @@ namespace Barotrauma { partial class Character { - public static Character Controlled = null; + public static Character Controlled => null; partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7d8e7791e..26a1fa3a3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -424,7 +424,7 @@ namespace Barotrauma msg.Write(owner == c && owner.Character == this); msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; - case StatusEventData _: + case CharacterStatusEventData _: WriteStatus(msg); break; case UpdateSkillsEventData _: @@ -657,6 +657,10 @@ namespace Barotrauma int infoLength = msg.LengthBytes - msgLengthBeforeInfo; msg.Write((byte)CampaignInteractionType); + if (CampaignInteractionType == CampaignMode.InteractionType.Store) + { + msg.Write(MerchantIdentifier); + } int msgLengthBeforeOrders = msg.LengthBytes; // Current orders diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 00190b2fd..484ebf03d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -7,51 +7,57 @@ namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(List itemsToSell, Client client = null) + public void SellBackPurchasedItems(Identifier storeIdentifier, List itemsToSell, Client client = null) { - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - Dictionary buyValues = GetBuyValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); - foreach (PurchasedItem item in itemsToSell) + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } + var storeSpecificItems = GetPurchasedItems(storeIdentifier); + foreach (var item in itemsToSell) { var itemValue = item.Quantity * buyValues[item.ItemPrefab]; - Location.StoreCurrentBalance -= itemValue; + store.Balance -= itemValue; campaign.GetWallet(client).Give(itemValue); - PurchasedItems.Remove(item); + storeSpecificItems?.Remove(item); } } - public void BuyBackSoldItems(List itemsToBuy, Client client) + public void BuyBackSoldItems(Identifier storeIdentifier, List itemsToBuy) { - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - var sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } + var storeSpecificItems = SoldItems.GetValueOrDefault(storeIdentifier); + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToBuy.Select(i => i.ItemPrefab)); foreach (var item in itemsToBuy) { int itemValue = sellValues[item.ItemPrefab]; - if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } - Location.StoreCurrentBalance += itemValue; + if (store.Balance < itemValue || item.Removed) { continue; } + store.Balance += itemValue; campaign.Bank.TryDeduct(itemValue); - SoldItems.Remove(item); + storeSpecificItems.Remove(item); } } - public void SellItems(List itemsToSell, Client client) + public void SellItems(Identifier storeIdentifier, List itemsToSell, Client client) { + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } bool canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; IEnumerable sellableItemsInSub = Enumerable.Empty(); if (canAddToRemoveQueue && itemsToSell.Any(i => i.Origin == SoldItem.SellOrigin.Submarine && i.ID == Entity.NullEntityID && !i.Removed)) { sellableItemsInSub = GetSellableItemsFromSub(); } - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - var sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); + var itemsSoldAtStore = SoldItems.GetValueOrDefault(storeIdentifier); + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); foreach (var item in itemsToSell) { int itemValue = sellValues[item.ItemPrefab]; // check if the store can afford the item and if the item hasn't been removed already - if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } + if (store.Balance < itemValue || item.Removed) { continue; } // Server determines the items that are sold from the sub in multiplayer if (item.Origin == SoldItem.SellOrigin.Submarine && item.ID == Entity.NullEntityID && !item.Removed) { @@ -66,8 +72,8 @@ namespace Barotrauma item.Removed = true; Entity.Spawner.AddItemToRemoveQueue(entity); } - SoldItems.Add(item); - Location.StoreCurrentBalance -= itemValue; + itemsSoldAtStore?.Add(item); + store.Balance -= itemValue; campaign.Bank.Give(itemValue); GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index fbd7d030e..d8cc3b09b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -616,8 +616,17 @@ namespace Barotrauma } // Store balance - msg.Write(true); - msg.Write((UInt16)map.CurrentLocation.StoreCurrentBalance); + 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 { @@ -626,36 +635,10 @@ namespace Barotrauma msg.Write(false); } - msg.Write((UInt16)CargoManager.ItemsInBuyCrate.Count); - foreach (PurchasedItem pi in CargoManager.ItemsInBuyCrate) - { - msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.ItemsInSellFromSubCrate.Count); - foreach (PurchasedItem pi in CargoManager.ItemsInSellFromSubCrate) - { - msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.PurchasedItems.Count); - foreach (PurchasedItem pi in CargoManager.PurchasedItems) - { - msg.Write(pi.ItemPrefab.Identifier); - msg.WriteRangedInteger(pi.Quantity, 0, CargoManager.MaxQuantity); - } - - msg.Write((UInt16)CargoManager.SoldItems.Count); - foreach (SoldItem si in CargoManager.SoldItems) - { - msg.Write(si.ItemPrefab.Identifier); - msg.Write((UInt16)si.ID); - msg.Write(si.Removed); - msg.Write(si.SellerID); - msg.Write((byte)si.Origin); - } + 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) @@ -700,44 +683,10 @@ namespace Barotrauma bool purchasedItemRepairs = msg.ReadBoolean(); bool purchasedLostShuttles = msg.ReadBoolean(); - UInt16 buyCrateItemCount = msg.ReadUInt16(); - List buyCrateItems = new List(); - for (int i = 0; i < buyCrateItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - buyCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); - } - - UInt16 subSellCrateItemCount = msg.ReadUInt16(); - List subSellCrateItems = new List(); - for (int i = 0; i < subSellCrateItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - subSellCrateItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); - } - - UInt16 purchasedItemCount = msg.ReadUInt16(); - List purchasedItems = new List(); - for (int i = 0; i < purchasedItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); - purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity, sender)); - } - - UInt16 soldItemCount = msg.ReadUInt16(); - List soldItems = new List(); - for (int i = 0; i < soldItemCount; i++) - { - string itemPrefabIdentifier = msg.ReadString(); - UInt16 id = msg.ReadUInt16(); - bool removed = msg.ReadBoolean(); - byte sellerId = msg.ReadByte(); - byte origin = msg.ReadByte(); - soldItems.Add(new SoldItem(ItemPrefab.Prefabs[itemPrefabIdentifier], id, removed, sellerId, (SoldItem.SellOrigin)origin)); - } + var buyCrateItems = ReadPurchasedItems(msg, sender); + var subSellCrateItems = ReadPurchasedItems(msg, sender); + var purchasedItems = ReadPurchasedItems(msg, sender); + var soldItems = ReadSoldItems(msg); ushort purchasedUpgradeCount = msg.ReadUInt16(); List purchasedUpgrades = new List(); @@ -839,42 +788,83 @@ namespace Barotrauma bool allowedToUseStore = AllowedToManageCampaign(sender, ClientPermissions.CampaignStore); if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) { - var currentBuyCrateItems = new List(CargoManager.ItemsInBuyCrate); - currentBuyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, -i.Quantity, sender)); - buyCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInBuyCrate(i.ItemPrefab, i.Quantity, sender)); - CargoManager.SellBackPurchasedItems(new List(CargoManager.PurchasedItems)); - CargoManager.PurchaseItems(purchasedItems, false, sender); + var prevBuyCrateItems = new Dictionary>(CargoManager.ItemsInBuyCrate); + foreach (var store in prevBuyCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); + } + } + foreach (var store in buyCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); + } + } + var prevPurchasedItems = new Dictionary>(CargoManager.PurchasedItems); + foreach (var store in prevPurchasedItems) + { + CargoManager.SellBackPurchasedItems(store.Key, store.Value); + } + foreach (var store in purchasedItems) + { + CargoManager.PurchaseItems(store.Key, store.Value, false, sender); + } } bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) { - var currentSubSellCrateItems = new List(CargoManager.ItemsInSellFromSubCrate); - currentSubSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, -i.Quantity, sender)); - subSellCrateItems.ForEach(i => CargoManager.ModifyItemQuantityInSubSellCrate(i.ItemPrefab, i.Quantity, sender)); + var prevSubSellCrateItems = new Dictionary>(CargoManager.ItemsInSellFromSubCrate); + foreach (var store in prevSubSellCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); + } + } + foreach (var store in subSellCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInSubSellCrate(store.Key, item.ItemPrefab, item.Quantity, sender); + } + } } - bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); if (allowedToManageCampaign || allowedToUseStore || (allowedToSellInventoryItems && allowedToSellSubItems)) { // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all // sold items that are removed so they should be discarded on the next message - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems), sender); - CargoManager.SellItems(soldItems, sender); + var prevSoldItems = new Dictionary>(CargoManager.SoldItems); + foreach (var store in prevSoldItems) + { + CargoManager.BuyBackSoldItems(store.Key, store.Value); + } + foreach (var store in soldItems) + { + CargoManager.SellItems(store.Key, store.Value, sender); + } } else if (allowedToSellInventoryItems || allowedToSellSubItems) { - if (allowedToSellInventoryItems) + var prevSoldItems = new Dictionary>(CargoManager.SoldItems); + foreach (var store in prevSoldItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Character)), sender); - soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Character); + store.Value.RemoveAll(predicate); + CargoManager.BuyBackSoldItems(store.Key, store.Value); } - else + foreach (var store in soldItems) { - CargoManager.BuyBackSoldItems(new List(CargoManager.SoldItems.Where(i => i.Origin == SoldItem.SellOrigin.Submarine)), sender); - soldItems.RemoveAll(i => i.Origin != SoldItem.SellOrigin.Submarine); + store.Value.RemoveAll(predicate); } - CargoManager.SellItems(soldItems, sender); + foreach (var store in soldItems) + { + CargoManager.SellItems(store.Key, store.Value, sender); + } + bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } if (allowedToManageCampaign) @@ -960,7 +950,7 @@ namespace Barotrauma public void ServerReadRewardDistribution(IReadMessage msg, Client sender) { - NetWalletSalaryUpdate update = INetSerializableStruct.Read(msg); + NetWalletSetSalaryUpdate update = INetSerializableStruct.Read(msg); if (!AllowedToManageCampaign(sender)) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 2310120a0..7bf233c95 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -42,8 +42,8 @@ namespace Barotrauma.Items.Components msg.Write(item.CurrentHull?.ID ?? Entity.NullEntityID); msg.Write(item.SimPosition.X); msg.Write(item.SimPosition.Y); - msg.Write(stickJoint.Axis.X); - msg.Write(stickJoint.Axis.Y); + msg.Write(jointAxis.X); + msg.Write(jointAxis.Y); if (StickTarget.UserData is Structure structure) { msg.Write(structure.ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 8f1783707..f7db7648e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -56,7 +56,6 @@ namespace Barotrauma if (containerIndex < 0) { throw error($"container index out of range ({containerIndex})"); - break; } if (!(components[containerIndex] is ItemContainer itemContainer)) { @@ -66,7 +65,7 @@ namespace Barotrauma msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); itemContainer.Inventory.ServerEventWrite(msg, c); break; - case StatusEventData _: + case ItemStatusEventData _: msg.Write(condition); break; case AssignCampaignInteractionEventData _: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index 0707bd526..9645311ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -1,12 +1,13 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; using System; -using System.Xml.Linq; namespace Barotrauma.MapCreatures.Behavior { partial class BallastFloraBehavior { + const float DamageUpdateInterval = 1.0f; + private float damageUpdateTimer; partial void LoadPrefab(ContentXElement element) @@ -31,16 +32,38 @@ namespace Barotrauma.MapCreatures.Behavior } } + partial void UpdateDamage(float deltaTime) + { + damageUpdateTimer -= deltaTime; + if (damageUpdateTimer > 0.0f) { return; } + + const int maxMessagesPerSecond = 10; + int messages = 0; + foreach (BallastFloraBranch branch in Branches) + { + //don't notify about minuscule amounts of damage (<= 1.0f) + if (branch.AccumulatedDamage > 1.0f) + { + CreateNetworkMessage(new BranchDamageEventData(branch)); + branch.AccumulatedDamage = 0.0f; + messages++; + //throttle a bit: if a large ballast flora is withering, it can lead to a very large number of events otherwise + if (messages > maxMessagesPerSecond) { break; } + } + } + damageUpdateTimer = DamageUpdateInterval; + } + public void ServerWrite(IWriteMessage msg, IEventData eventData) { msg.Write((byte)eventData.NetworkHeader); switch (eventData) { - case SpawnEventData spawnEventData: + case SpawnEventData _: ServerWriteSpawn(msg); break; - case KillEventData killEventData: + case KillEventData _: //do nothing break; case BranchCreateEventData branchCreateEventData: @@ -72,6 +95,7 @@ namespace Barotrauma.MapCreatures.Behavior var (x, y) = branch.Position; msg.Write(parentId); msg.Write((int)branch.ID); + msg.Write(branch.IsRootGrowth); msg.WriteRangedInteger((byte)branch.Type, 0b0000, 0b1111); msg.WriteRangedInteger((byte)branch.Sides, 0b0000, 0b1111); msg.WriteRangedInteger(branch.FlowerConfig.Serialize(), 0, 0xFFF); @@ -103,7 +127,7 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.ID); } - public void SendNetworkMessage(IEventData extraData) + public void CreateNetworkMessage(IEventData extraData) { GameMain.Server.CreateEntityEvent(Parent, new Hull.BallastFloraEventData(this, extraData)); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 7a8d742eb..93a36099e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -28,7 +28,11 @@ namespace Barotrauma.Networking public static string GetCompressedModPath(ContentPackage mod) { string dir = mod.Dir; - string resultFileName = dir.Replace('\\', '_').Replace('/', '_'); + string resultFileName + = dir.StartsWith(ContentPackage.LocalModsDir) + ? $"Local_{mod.Name}" + : $"Workshop_{mod.Name}"; + resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 804c11d40..4373de52d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -814,7 +814,7 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; - case ClientPacketHeader.MONEY: + case ClientPacketHeader.TRANSFER_MONEY: ReadMoneyMessage(inc, connectedClient); break; case ClientPacketHeader.REWARD_DISTRIBUTION: diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 334e62414..5580a12fe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -321,11 +321,16 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); + if (HiddenSubs.Any()) + { + UpdateFlag(NetFlags.HiddenSubs); + } SelectedSubmarine = SelectNonHiddenSubmarine(SelectedSubmarine); string[] defaultAllowedClientNameChars = - new string[] { + new string[] + { "32-33", "38-46", "48-57", diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs index 715ef5a3a..ba77838e6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Traitor.cs @@ -16,7 +16,7 @@ namespace Barotrauma Role = role; Character = character; Character.IsTraitor = true; - GameMain.NetworkMember.CreateEntityEvent(Character, new Character.StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(Character, new Character.CharacterStatusEventData()); } public delegate void MessageSender(string message); diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 989383c24..96429c964 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,8 +6,8 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.2.0 - Copyright © FakeFish 2018-2020 + 0.17.4.0 + Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer ..\BarotraumaShared\Icon.ico diff --git a/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub b/Barotrauma/BarotraumaShared/LocalMods/PowerTestSub/PowerTestSub.sub deleted file mode 100644 index cdd96a32b9a24bd1cf245f725b8808d2b2afb6ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9564 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 - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 48ef25dce..e01c32de2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -56,7 +56,7 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; - private readonly float attackLimbResetInterval = 2; + private readonly float attackLimbSelectionInterval = 3; // Min priority for the memorized targets. The actual value fades gradually, unless kept fresh by selecting the target. private const float minPriority = 10; @@ -65,10 +65,10 @@ namespace Barotrauma private float updateTargetsTimer; private float updateMemoriesTimer; - private float attackLimbResetTimer; + private float attackLimbSelectionTimer; - private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; - private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0; + private bool IsAttackRunning => AttackLimb != null && AttackLimb.attack.IsRunning; + private bool IsCoolDownRunning => AttackLimb != null && AttackLimb.attack.CoolDownTimer > 0 || _previousAttackLimb != null && _previousAttackLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; private float Hearing => AIParams.Hearing; @@ -77,25 +77,25 @@ namespace Barotrauma private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - private Limb _attackingLimb; - private Limb _previousAttackingLimb; - public Limb AttackingLimb + private Limb _attackLimb; + private Limb _previousAttackLimb; + public Limb AttackLimb { - get { return _attackingLimb; } + get { return _attackLimb; } private set { - attackLimbResetTimer = 0; - if (_attackingLimb != value) + if (_attackLimb != value) { - _previousAttackingLimb = _attackingLimb; + _previousAttackLimb = _attackLimb; + _previousAttackLimb?.AttachedRope?.Snap(); } - if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0) + else if (_attackLimb != null && _attackLimb.attack.CoolDownTimer <= 0) { - SetAimTimer(); + _attackLimb.AttachedRope?.Snap(); } - _attackingLimb = value; + _attackLimb = value; attackVector = null; - Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; + Reverse = _attackLimb != null && _attackLimb.attack.Reverse; } } @@ -425,7 +425,8 @@ namespace Barotrauma private void ReleaseDragTargets() { - if (Character.Inventory != null) + AttackLimb?.AttachedRope?.Snap(); + if (Character.Params.CanInteract && Character.Inventory != null) { Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); } @@ -600,7 +601,7 @@ namespace Barotrauma UpdatePatrol(deltaTime); break; case AIState.Attack: - run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; + run = !IsCoolDownRunning || AttackLimb != null && AttackLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); break; case AIState.Eat: @@ -620,7 +621,7 @@ namespace Barotrauma return; } float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); + var attackLimb = AttackLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; @@ -875,7 +876,10 @@ namespace Barotrauma if (followLastTarget) { var target = SelectedAiTarget ?? _lastAiTarget; - if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) + if (target?.Entity != null && !target.Entity.Removed && + PreviousState == AIState.Attack && Character.CurrentHull == null && + (_previousAttackLimb?.attack == null || + _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) { // Keep heading to the last known position of the target var memory = GetTargetMemory(target, false); @@ -1126,31 +1130,42 @@ namespace Barotrauma return; } } + + attackLimbSelectionTimer -= deltaTime; + if (AttackLimb == null || attackLimbSelectionTimer <= 0) + { + attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f); + if (!IsAttackRunning && !IsCoolDownRunning) + { + AttackLimb = GetAttackLimb(attackWorldPos); + } + } bool canAttack = true; bool pursue = false; - if (IsCoolDownRunning) + if (IsCoolDownRunning && (_previousAttackLimb == null || AttackLimb == null || AttackLimb.attack.CoolDownTimer > 0)) { - var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb; + var currentAttackLimb = AttackLimb ?? _previousAttackLimb; if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - switch (currentAttackLimb.attack.AfterAttack) + AIBehaviorAfterAttack activeBehavior = currentAttackLimb.attack.AfterAttack; + switch (activeBehavior) { case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (activeBehavior == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } } @@ -1162,13 +1177,13 @@ namespace Barotrauma if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) + if (activeBehavior == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } - AttackingLimb = null; + AttackLimb = null; } else { @@ -1177,19 +1192,19 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { // No new limb was found. - if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (activeBehavior == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; } else { - UpdateFallBack(attackWorldPos, deltaTime, true); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; } } @@ -1204,10 +1219,15 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: + case AIBehaviorAfterAttack.ReverseUntilCanAttack: + if (activeBehavior == AIBehaviorAfterAttack.ReverseUntilCanAttack) + { + Reverse = true; + } if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1217,7 +1237,7 @@ namespace Barotrauma // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else @@ -1227,12 +1247,12 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1240,7 +1260,7 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, activeBehavior == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1269,7 +1289,7 @@ namespace Barotrauma if (newLimb != null) { // Attack with the new limb - AttackingLimb = newLimb; + AttackLimb = newLimb; } else { @@ -1291,7 +1311,12 @@ namespace Barotrauma UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; case AIBehaviorAfterAttack.FallBack: + case AIBehaviorAfterAttack.Reverse: default: + if (activeBehavior == AIBehaviorAfterAttack.Reverse) + { + Reverse = true; + } UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); return; } @@ -1303,12 +1328,13 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable)) + if (AttackLimb == null || !IsValidAttack(AttackLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity)) { - AttackingLimb = GetAttackLimb(attackWorldPos); + AttackLimb = GetAttackLimb(attackWorldPos); } - canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; + canAttack = AttackLimb != null && AttackLimb.attack.CoolDownTimer <= 0; } + if (!AIParams.CanOpenDoors) { if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) @@ -1347,8 +1373,8 @@ namespace Barotrauma // Target a specific limb instead of the target center position if (wallTarget == null && targetCharacter != null) { - var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; - attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); + var targetLimbType = AttackLimb.Params.Attack.Attack.TargetLimbType; + attackTargetLimb = GetTargetLimb(AttackLimb, targetCharacter, targetLimbType); if (attackTargetLimb == null) { State = AIState.Idle; @@ -1361,7 +1387,7 @@ namespace Barotrauma } } - Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null && Character.Submarine == null) @@ -1389,23 +1415,23 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } - float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange; - if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; } + float diff = AttackLimb.attack.Range - AttackLimb.attack.DamageRange; + if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } - float distanceOffset = diff * AttackingLimb.attack.Duration; + float distanceOffset = diff * AttackLimb.attack.Duration; // Intentionally omit the unit conversion because we use distanceOffset as a multiplier. return targetVelocity * distanceOffset * dot; } // Check that we can reach the target distance = toTarget.Length(); - canAttack = distance < AttackingLimb.attack.Range; + canAttack = distance < AttackLimb.attack.Range; // Crouch if the target is down (only humanoids), so that we can reach it. - if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2) + if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2) { - if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range) + if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range) { humanoidAnimController.Crouching = true; } @@ -1413,14 +1439,14 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb.attack.Ranged) + if (AttackLimb.attack.Ranged) { // Check that is facing the target - float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float offset = AttackLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(AttackLimb.body.TransformedRotation - offset * Character.AnimController.Dir); float angle = VectorExtensions.Angle(forward, toTarget); - canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle); - if (canAttack && AttackingLimb.attack.AvoidFriendlyFire) + canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle); + if (canAttack && AttackLimb.attack.AvoidFriendlyFire) { float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; @@ -1434,11 +1460,11 @@ namespace Barotrauma } if (canAttack) { - canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range)); + canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range)); bool IsBlocked(Vector2 targetPosition) { - foreach (var body in Submarine.PickBodies(AttackingLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) { Character hitTarget = null; if (body.UserData is Character c) @@ -1460,22 +1486,8 @@ namespace Barotrauma } } } - else if (!IsAttackRunning && !IsCoolDownRunning) - { - // If not, reset the attacking limb, if the cooldown is not running - // Don't use the property, because we don't want cancel reversing, if we are reversing. - if (attackLimbResetTimer > attackLimbResetInterval) - { - _attackingLimb = null; - attackLimbResetTimer = 0; - } - else - { - attackLimbResetTimer += deltaTime; - } - } } - Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null; + Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null; if (steeringLimb == null) { // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. @@ -1490,9 +1502,9 @@ namespace Barotrauma var pathSteering = SteeringManager as IndoorsSteeringManager; - if (AttackingLimb != null && AttackingLimb.attack.Retreat) + if (AttackLimb != null && AttackLimb.attack.Retreat) { - UpdateFallBack(attackWorldPos, deltaTime, false); + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); } else { @@ -1527,7 +1539,7 @@ namespace Barotrauma } // When pursuing, we don't want to pursue too close float max = 300; - float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max; + float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max; if (!canAttack || distance > margin) { // Steer towards the target if in the same room and swimming @@ -1558,10 +1570,10 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.Ranged) + if (AttackLimb.attack.Ranged) { float dir = Character.AnimController.Dir; - if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin) + if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin) { SteeringManager.Reset(); } @@ -1658,9 +1670,9 @@ namespace Barotrauma } break; case CirclePhase.CloseIn: - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) { - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) @@ -1703,10 +1715,10 @@ namespace Barotrauma // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, // which makes it continue circling around the point (as supposed) // But when there is some offset and the offset is too near, this is not what we want. - if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + if (AttackLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) { CirclePhase = CirclePhase.Strike; - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; } else { @@ -1741,9 +1753,9 @@ namespace Barotrauma } } } - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + if (AttackLimb != null && distance > 0 && distance < AttackLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) { - strikeTimer = AttackingLimb.attack.CoolDown; + strikeTimer = AttackLimb.attack.CoolDown; CirclePhase = CirclePhase.Strike; } canAttack = false; @@ -1800,7 +1812,7 @@ namespace Barotrauma } } - if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100)) + if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100)) { if (pathSteering != null) { @@ -1811,7 +1823,7 @@ namespace Barotrauma SteeringManager.SteeringSeek(steerPos, 10); } } - else if (AttackingLimb.attack.Ranged) + else if (AttackLimb.attack.Ranged) { // Too close UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); @@ -1824,18 +1836,18 @@ namespace Barotrauma } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) + if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); } } else if (IsAttackRunning) { - AttackingLimb.attack.ResetAttackTimer(); + AttackLimb.attack.ResetAttackTimer(); } } - private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, IDamageable target) + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, Entity target) { if (attackingLimb == null) { return false; } if (target == null) { return false; } @@ -1854,10 +1866,11 @@ namespace Barotrauma // Check that is approximately facing the target Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; + if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) { return false; } float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); - float angle = VectorExtensions.Angle(forward, toTarget); - if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; } + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget)); + if (angle > attack.RequiredAngle) { return false; } } return true; } @@ -1867,7 +1880,7 @@ namespace Barotrauma private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); - IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; + Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; @@ -1901,12 +1914,13 @@ namespace Barotrauma float CalculatePriority(Limb limb, Vector2 attackPos) { - if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; } + float prio = 1 + limb.attack.Priority; + if (Character.AnimController.SimplePhysicsEnabled) { return prio; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. // We also need a max value that is more than the actual range. float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist)); - return (1 + limb.attack.Priority) * distanceFactor; + return prio * distanceFactor; } } @@ -1919,7 +1933,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } - if (Character.Params.CanInteract && attackResult.Damage > 10) + if (attackResult.Damage >= AIParams.DamageThreshold) { ReleaseDragTargets(); } @@ -2007,9 +2021,11 @@ namespace Barotrauma if (State == AIState.Attack && (IsAttackRunning || IsCoolDownRunning)) { - // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; - avoidGunFire = false; + if (IsAttackRunning) + { + avoidGunFire = false; + } } if (retaliate) { @@ -2022,7 +2038,7 @@ namespace Barotrauma } } } - else if (avoidGunFire) + else if (avoidGunFire && attackResult.Damage >= AIParams.DamageThreshold) { State = AIState.Escape; avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); @@ -2114,7 +2130,7 @@ namespace Barotrauma { if (attackingLimb.attack.CoolDownTimer > 0) { - SetAimTimer(); + SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f)); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; if (!(damageTarget is Character)) @@ -2245,19 +2261,19 @@ namespace Barotrauma // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } - Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); - if (!MathUtils.IsValid(attackDir)) + Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); + if (!MathUtils.IsValid(dir)) { - attackDir = Vector2.UnitY; + dir = Vector2.UnitY; } - steeringManager.SteeringManual(deltaTime, attackDir); - if (Character.AnimController.InWater) + steeringManager.SteeringManual(deltaTime, dir); + if (Character.AnimController.InWater && !Reverse) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } if (checkBlocking) { - return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2)); + return !IsBlocked(deltaTime, SimPosition + dir * (avoidLookAheadDistance / 2)); } return true; } @@ -2810,7 +2826,7 @@ namespace Barotrauma if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null) { - if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep) + if (targetParams.PrioritizeSubCenter || targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep) { if (!isAnyTargetClose) { @@ -2990,7 +3006,7 @@ namespace Barotrauma if (HasValidPath(requireNonDirty: true)) { return; } wallHits.Clear(); Structure wall = null; - Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition; + Vector2 rayStart = AttackLimb != null ? AttackLimb.SimPosition : SimPosition; if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { Vector2 rayEnd = SelectedAiTarget.SimPosition; @@ -3303,6 +3319,7 @@ namespace Barotrauma foreach (var triggerObject in activeTriggers) { AITrigger trigger = triggerObject.Key; + if (trigger.IsPermanent) { continue; } trigger.UpdateTimer(deltaTime); if (!trigger.IsActive) { @@ -3471,7 +3488,7 @@ namespace Barotrauma disableTailCoroutine = null; } Character.AnimController.ReleaseStuckLimbs(); - AttackingLimb = null; + AttackLimb = null; movementMargin = 0; ResetEscape(); if (isStateChanged && to == AIState.Idle && from != to) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index a98cef44d..5dc813a8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -148,14 +148,14 @@ namespace Barotrauma } if (TargetCharacter != null) { - if (enemyAI.AttackingLimb?.attack == null) + if (enemyAI.AttackLimb?.attack == null) { DeattachFromBody(reset: true, cooldown: 1); } else { - float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; - if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) + float range = enemyAI.AttackLimb.attack.DamageRange * 2f; + if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range) { DeattachFromBody(reset: true, cooldown: 1); } @@ -265,11 +265,11 @@ namespace Barotrauma if (enemyAI.IsSteeringThroughGap) { break; } if (_attachPos == Vector2.Zero) { break; } if (!AttachToSub && !AttachToCharacters) { break; } - if (enemyAI.AttackingLimb == null) { break; } + if (enemyAI.AttackLimb == null) { break; } if (targetBody == null) { break; } if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); - if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) + if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange) { AttachToBody(transformedAttachPos); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 4f818f175..5dcbdb17e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -112,6 +112,13 @@ namespace Barotrauma } foreach (FireSource fs in targetHull.FireSources) { + if (fs == null) { continue; } + if (fs.Removed) { continue; } + if (character.CurrentHull == null) + { + Abandon = true; + break; + } float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); bool inRange = xDist + yDist < extinguisher.Range; @@ -153,7 +160,7 @@ namespace Barotrauma onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref gotoObjective))) { - gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull); + gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 8a2f1ef0e..99adb5e1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -164,7 +164,7 @@ namespace Barotrauma TargetName = Leak.FlowTargetHull?.DisplayName, requiredCondition = () => Leak.Submarine == character.Submarine && - (Leak.FlowTargetHull != null && character.CurrentHull == Leak.FlowTargetHull || character.CanSeeTarget(Leak)), + Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h), // 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/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 65597452e..e282061e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -99,8 +99,7 @@ namespace Barotrauma { if (InWater || !CanWalk) { - float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; - return TargetMovement.LengthSquared() > avg * avg; + return TargetMovement.LengthSquared() > SwimSlowParams.MovementSpeed; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 699f94e37..f898ac8d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -448,21 +448,18 @@ namespace Barotrauma movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; var mainLimb = MainLimb; - if (isMoving) + float t = 0.5f; + if (isMoving && !SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { - float t = 0.5f; - if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) { - Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); - float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); - if (dot < 0) - { - // Reduce the linear movement speed when not facing the movement direction - t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); - } + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); } - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index b386fdd83..30fc791e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -443,8 +443,6 @@ namespace Barotrauma if (CurrentGroundedParams == null) { return; } Vector2 handPos; - //if you're allergic to magic numbers, stop reading now - Limb leftFoot = GetLimb(LimbType.LeftFoot); Limb rightFoot = GetLimb(LimbType.RightFoot); Limb head = GetLimb(LimbType.Head); @@ -599,16 +597,20 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } + if (Crouching && !movingHorizontally && !aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (HeadAngle.HasValue) + if (!aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque); } + else + { + RotateHead(head); + } if (!onGround) { @@ -883,23 +885,17 @@ namespace Barotrauma } } float targetSpeed = TargetMovement.Length(); - if (targetSpeed > 0.1f) + if (aiming) { - if (!aiming) - { - float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - torso.SimPosition) * Dir; + float newRotation = MathUtils.VectorToAngle(diff); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } - else + else if (targetSpeed > 0.1f) { - if (aiming) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); - Vector2 diff = (mousePos - torso.SimPosition) * Dir; - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); - } + float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); @@ -914,13 +910,14 @@ namespace Barotrauma { torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } - if (HeadAngle.HasValue) + + if (!aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue) { head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } else { - head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque); + RotateHead(head); } //dont try to move upwards if head is already out of water @@ -951,7 +948,18 @@ namespace Barotrauma if (isNotRemote) { - Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp); + float t = movementLerp; + if (targetSpeed > 0.00001f && !SimplePhysicsEnabled) + { + Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) + { + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + } + } + Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } WalkPos += movement.Length(); @@ -1227,7 +1235,11 @@ namespace Barotrauma //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); - if (!aiming) + if (aiming) + { + RotateHead(head); + } + else { float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); @@ -1710,6 +1722,24 @@ namespace Barotrauma } } + private void RotateHead(Limb head) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 dir = (mousePos - head.SimPosition) * Dir; + float rot = MathUtils.VectorToAngle(dir); + var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso); + if (neckJoint != null) + { + float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation); + float lowerLimit = neckJoint.LowerLimit + offset; + float upperLimit = neckJoint.UpperLimit + offset; + float min = Math.Min(lowerLimit, upperLimit); + float max = Math.Max(lowerLimit, upperLimit); + rot = Math.Clamp(rot, min, max); + } + head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque); + } + private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) { if (!MathUtils.IsValid(pos)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 874e6e000..9277a79ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -805,7 +805,7 @@ namespace Barotrauma SeverLimbJointProjSpecific(limbJoint, playSound: true); if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(character, new Character.CharacterStatusEventData()); } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index ff3af7ccf..e405a887e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -37,7 +37,9 @@ namespace Barotrauma Pursue, FollowThrough, FollowThroughUntilCanAttack, - IdleUntilCanAttack + IdleUntilCanAttack, + Reverse, + ReverseUntilCanAttack } struct AttackResult @@ -102,7 +104,7 @@ namespace Barotrauma public bool Retreat { get; private set; } private float _range; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float Range { get => _range * RangeMultiplier; @@ -110,13 +112,16 @@ namespace Barotrauma } private float _damageRange; - [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)] + [Serialize(0.0f, IsPropertySaveable.Yes, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float DamageRange { get => _damageRange * RangeMultiplier; set => _damageRange = value; } + [Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] + public float MinRange { get; private set; } + [Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)] public float Duration { get; private set; } @@ -356,17 +361,17 @@ namespace Barotrauma { Deserialize(element); - if (element.Attribute("damage") != null || - element.Attribute("bluntdamage") != null || - element.Attribute("burndamage") != null || - element.Attribute("bleedingdamage") != null) + if (element.GetAttribute("damage") != null || + element.GetAttribute("bluntdamage") != null || + element.GetAttribute("burndamage") != null || + element.GetAttribute("bleedingdamage") != null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. )."); } //if level wall damage is not defined, default to the structure damage - if (element.Attribute("LevelWallDamage") == null && - element.Attribute("levelwalldamage") == null) + if (element.GetAttribute("LevelWallDamage") == null && + element.GetAttribute("levelwalldamage") == null) { LevelWallDamage = StructureDamage; } @@ -382,7 +387,7 @@ namespace Barotrauma break; case "affliction": AfflictionPrefab afflictionPrefab; - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names."); string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); @@ -686,7 +691,7 @@ namespace Barotrauma public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; - public bool IsValidTarget(IDamageable target) + public bool IsValidTarget(Entity target) { return TargetType switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 3f6245bc7..c63af2b86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -893,6 +893,7 @@ namespace Barotrauma public bool GodMode = false; public CampaignMode.InteractionType CampaignInteractionType; + public Identifier MerchantIdentifier; private bool accessRemovedCharacterErrorShown; public override Vector2 SimPosition @@ -1885,7 +1886,7 @@ namespace Barotrauma if (!attack.IsValidContext(currentContexts)) { return false; } if (attackTarget != null) { - if (!attack.IsValidTarget(attackTarget)) { return false; } + if (!attack.IsValidTarget(attackTarget as Entity)) { return false; } if (attackTarget is ISerializableEntity se && attackTarget is Character) { if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } @@ -2011,6 +2012,8 @@ namespace Barotrauma public bool CanSeeCharacter(Character target) { + System.Diagnostics.Debug.Assert(target != null); + if (target == null) { return false; } if (target.Removed) { return false; } Limb seeingLimb = GetSeeingLimb(); if (CanSeeTarget(target, seeingLimb)) { return true; } @@ -2045,7 +2048,6 @@ namespace Barotrauma if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; } if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; } } - return false; } @@ -2056,6 +2058,8 @@ namespace Barotrauma public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null) { + System.Diagnostics.Debug.Assert(target != null); + if (target == null) { return false; } seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity; if (seeingEntity == null) { return false; } ISpatialEntity sourceEntity = seeingEntity ; @@ -3148,7 +3152,8 @@ namespace Barotrauma var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } - foreach (Item inventoryItem in Inventory.AllItemsMod) + List inventoryItems = new List(Inventory.AllItemsMod); + foreach (Item inventoryItem in inventoryItems) { if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null, createNetworkEvent: createNetworkEvents)) { @@ -3156,17 +3161,25 @@ namespace Barotrauma inventoryItem.Drop(dropper: this, createNetworkEvent: createNetworkEvents); } } + //this needs to happen after the items have been dropped (we can no longer sync dropping the items if the character has been removed) + Spawner.AddEntityToRemoveQueue(this); } } - - Spawner.AddEntityToRemoveQueue(this); + else + { + Spawner.AddEntityToRemoveQueue(this); + } } public void DespawnNow(bool createNetworkEvents = true) { despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay; UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents); - Spawner.Update(createNetworkEvents); + //update twice: first to spawn the duffel bag and move the items into it, then to remove the character + for (int i = 0; i < 2; i++) + { + Spawner.Update(createNetworkEvents); + } } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -4012,7 +4025,7 @@ namespace Barotrauma if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + GameMain.NetworkMember.CreateEntityEvent(this, new CharacterStatusEventData()); } isDead = true; @@ -4168,6 +4181,11 @@ namespace Barotrauma } DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")"); +#if CLIENT + //ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns + Inventory?.ApplyReceivedState(); +#endif + base.Remove(); foreach (Item heldItem in HeldItems.ToList()) @@ -4179,12 +4197,12 @@ namespace Barotrauma #if CLIENT GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false); + + if (Controlled == this) { Controlled = null; } #endif CharacterList.Remove(this); - if (Controlled == this) { Controlled = null; } - if (Inventory != null) { foreach (Item item in Inventory.AllItems) @@ -4266,7 +4284,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData()); + newItem.CreateStatusEvent(); } #if SERVER newItem.GetComponent()?.SyncHistory(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs index 11eab9eba..731461475 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterEventData.cs @@ -51,7 +51,7 @@ namespace Barotrauma } } - public struct StatusEventData : IEventData + public struct CharacterStatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index f2bc10d8b..df1cede68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -852,6 +852,19 @@ namespace Barotrauma #if CLIENT public void RecreateHead(MultiplayerPreferences characterSettings) { + if (characterSettings.HairIndex == -1 && + characterSettings.BeardIndex == -1 && + characterSettings.MoustacheIndex == -1 && + characterSettings.FaceAttachmentIndex == -1) + { + //randomize if nothing is set + SetAttachments(Rand.RandSync.Unsynced); + characterSettings.HairIndex = Head.HairIndex; + characterSettings.BeardIndex = Head.BeardIndex; + characterSettings.MoustacheIndex = Head.MoustacheIndex; + characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex; + } + RecreateHead( characterSettings.TagSet.ToImmutableHashSet(), characterSettings.HairIndex, @@ -859,9 +872,14 @@ namespace Barotrauma characterSettings.MoustacheIndex, characterSettings.FaceAttachmentIndex); - Head.SkinColor = characterSettings.SkinColor; - Head.HairColor = characterSettings.HairColor; - Head.FacialHairColor = characterSettings.FacialHairColor; + Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor); + Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor); + Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor); + + Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor) + { + return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced); + } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index da10f3719..065a7cc10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -287,7 +287,7 @@ namespace Barotrauma StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); } - if (element.Attribute("interval") != null) + if (element.GetAttribute("interval") != null) { MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f); } @@ -409,7 +409,7 @@ namespace Barotrauma HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0); - if (element.Attribute("nameidentifier") != null) + if (element.GetAttribute("nameidentifier") != null) { Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 320d295d0..86957c713 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -47,22 +47,24 @@ namespace Barotrauma HighlightSprite = new Sprite(subElement); break; case "vitalitymultiplier": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names."); continue; } - - Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); - Identifier afflictionType = subElement.GetAttributeIdentifier("type", ""); - float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); - if (!afflictionIdentifier.IsEmpty) + var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); + if (vitalityMultipliers == null) { - VitalityMultipliers.Add(afflictionIdentifier, multiplier); + vitalityMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); + } + if (vitalityMultipliers != null) + { + float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); + vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier)); } else { - VitalityTypeMultipliers.Add(afflictionType, multiplier); + DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b08cafe51..c430090d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -540,6 +540,8 @@ namespace Barotrauma private set; } + public Items.Components.Rope AttachedRope { get; set; } + public string Name => Params.Name; // These properties are exposed for status effects diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 2fdb19b98..a1920a820 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -103,6 +103,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] + public bool FixedHeadAngle { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation @@ -131,7 +134,7 @@ namespace Barotrauma get => MathHelper.ToDegrees(FootAngleInRadians); set { - FootAngleInRadians = MathHelper.ToRadians(value); + FootAngleInRadians = MathHelper.ToRadians(value); } } public float FootAngleInRadians { get; private set; } @@ -156,6 +159,9 @@ namespace Barotrauma [Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } + + [Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable] + public bool FixedHeadAngle { get; set; } } public interface IHumanAnimation @@ -166,5 +172,7 @@ namespace Barotrauma float ArmMoveStrength { get; set; } float HandMoveStrength { get; set; } + + bool FixedHeadAngle { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 640bf791c..b1314218f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -104,6 +104,9 @@ namespace Barotrauma [Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)] public float SoundInterval { get; set; } + [Serialize(false, IsPropertySaveable.Yes), Editable] + public bool DrawLast { get; set; } + public readonly CharacterFile File; public XDocument VariantFile { get; private set; } @@ -127,31 +130,36 @@ namespace Barotrauma public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement; - public static XElement CreateVariantXml(XElement selfXml, XElement parentXml) + public static XElement CreateVariantXml(XElement variantXML, XElement baseXML) { - XElement newXml = selfXml.CreateVariantXML(parentXml); - - XElement selfAi = selfXml.GetChildElement("ai"); - XElement parentAi = parentXml.GetChildElement("ai"); - - if (parentAi is null || parentAi.Elements().None() - || selfAi is null || selfAi.Elements().None()) + XElement newXml = variantXML.CreateVariantXML(baseXML); + XElement variantAi = variantXML.GetChildElement("ai"); + XElement baseAi = baseXML.GetChildElement("ai"); + if (baseAi is null || baseAi.Elements().None() + || variantAi is null || variantAi.Elements().None()) { return newXml; } - - //discard the inherited targets, just keep the new ones + // CreateVariantXML seems to merge the ai targets so that in the new xml we have both the old and the new target definitions. var finalAiElement = newXml.GetChildElement("ai"); - foreach (var finalTarget in finalAiElement!.Elements().ToArray()) + var processedTags = new HashSet(); + foreach (var aiTarget in finalAiElement.Elements().ToArray()) { - finalTarget.Remove(); + string tag = aiTarget.GetAttributeString("tag", null); + if (tag == null) { continue; } + if (processedTags.Contains(tag)) + { + aiTarget.Remove(); + continue; + } + processedTags.Add(tag); + var matchInSelf = variantAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag); + var matchInParent = baseAi.Elements().FirstOrDefault(e => e.GetAttributeString("tag", null) == tag); + if (matchInSelf != null && matchInParent != null) + { + aiTarget.ReplaceWith(new XElement(matchInSelf)); + } } - - foreach (var inheritorTarget in selfAi.Elements()) - { - finalAiElement.Add(new XElement(inheritorTarget)); - } - return newXml; } @@ -574,6 +582,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.Yes, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } + [Serialize(0f, IsPropertySaveable.Yes, description: "How much damage is required for single attack to trigger avoiding/releasing targets."), Editable(minValue: 0f, maxValue: 1000f)] + public float DamageThreshold { get; private set; } + [Serialize(3f, IsPropertySaveable.Yes, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] public float AvoidTime { get; private set; } @@ -785,6 +796,9 @@ namespace Barotrauma [Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable] public AttackPattern AttackPattern { get; set; } + [Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable] + public bool PrioritizeSubCenter { get; set; } + #region Sweep [Serialize(0f, IsPropertySaveable.Yes, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] public float SweepDistance { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs index 9bbb48bf5..f237deea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Abilities public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - if (conditionElement.Attribute("submarinetype") != null) + if (conditionElement.GetAttribute("submarinetype") != null) { submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index 59c8254b6..9e85d367e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -11,7 +11,7 @@ namespace Barotrauma.Abilities public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - if (conditionElement.Attribute("hasoutpost") != null) + if (conditionElement.GetAttribute("hasoutpost") != null) { hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs index 484e0c7bd..f602cd4c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs @@ -1,7 +1,4 @@ - -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionInHull : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs index a6191d470..9ffd65c6b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInWater.cs @@ -1,7 +1,4 @@ - -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class AbilityConditionInWater : AbilityConditionDataless { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index ad164626d..2e505890e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -45,7 +45,7 @@ namespace Barotrauma } } - if (element.Attribute("description") != null) + if (element.GetAttribute("description") != null) { string description = element.GetAttributeString("description", string.Empty); Description = Description.Fallback(TextManager.Get(description)).Fallback(description); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 73e07dcf1..35865bb05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -30,6 +30,7 @@ namespace Barotrauma public readonly bool NotSyncedInMultiplayer; public readonly ImmutableHashSet? AlternativeTypes; public readonly ImmutableHashSet Names; + private readonly MethodInfo? contentPathMutator; public TypeInfo(Type type) { @@ -40,9 +41,11 @@ namespace Barotrauma var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; + contentPathMutator + = Type.GetMethod(nameof(MutateContentPath), BindingFlags.Static | BindingFlags.Public); HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; - if (type.GetCustomAttribute()?.Names is { } altNames) + if (type.GetCustomAttribute(inherit: false)?.Names is { } altNames) { names.UnionWith(altNames); } @@ -50,6 +53,10 @@ namespace Barotrauma Names = names.ToImmutableHashSet(); } + public ContentPath MutateContentPath(ContentPath path) + => (ContentPath?)contentPathMutator?.Invoke(null, new object[] { path }) + ?? path; + public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); } @@ -80,6 +87,7 @@ namespace Barotrauma } try { + filePath = type.MutateContentPath(filePath); if (!File.Exists(filePath.FullPath)) { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs new file mode 100644 index 000000000..85f6a6fa1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ServerExecutableFile.cs @@ -0,0 +1,28 @@ +using System; +using Barotrauma.IO; + +namespace Barotrauma +{ + sealed class ServerExecutableFile : OtherFile + { + //This content type doesn't do very much on its own, it's handled manually by the Host Server menu + public ServerExecutableFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } + + public static ContentPath MutateContentPath(ContentPath path) + { + if (File.Exists(path.FullPath)) { return path; } + + string rawValueWithoutExtension() + => Barotrauma.IO.Path.Combine( + Barotrauma.IO.Path.GetDirectoryName(path.RawValue ?? ""), + Barotrauma.IO.Path.GetFileNameWithoutExtension(path.RawValue ?? "")).CleanUpPath(); + + path = ContentPath.FromRaw(path.ContentPackage, rawValueWithoutExtension()); + if (File.Exists(path.FullPath)) { return path; } + + path = ContentPath.FromRaw(path.ContentPackage, + rawValueWithoutExtension() + ".exe"); + return path; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index dae572835..586da6857 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -110,7 +110,7 @@ namespace Barotrauma !expectedHash.IsNullOrWhiteSpace() && !expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase); - public IEnumerable GetFiles() where T : ContentFile => Files.Where(f => f is T).Cast(); + public IEnumerable GetFiles() where T : ContentFile => Files.OfType(); public IEnumerable GetFiles(Type type) => !type.IsSubclassOf(typeof(ContentFile)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs index 33daaec39..02a6f4401 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/CorePackage.cs @@ -8,7 +8,7 @@ using System.Xml.Linq; namespace Barotrauma { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class RequiredByCorePackage : Attribute { public readonly ImmutableHashSet AlternativeTypes; @@ -18,7 +18,7 @@ namespace Barotrauma } } - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class AlternativeContentTypeNames : Attribute { public readonly ImmutableHashSet Names; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index bf2faed95..0758b7042 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -51,7 +51,7 @@ namespace Barotrauma Core?.UnloadPackage(); Core = newCore; foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; } - ThrowIfDuplicates(All); + SortContent(); yield return new LoadProgress(1.0f); } @@ -60,7 +60,7 @@ namespace Barotrauma if (Core == null) { return; } Core.UnloadPackage(); Core.LoadPackage(); - ThrowIfDuplicates(All); + SortContent(); } public static void EnableRegular(RegularPackage p) @@ -133,7 +133,7 @@ namespace Barotrauma } } - public static void SortContent() + private static void SortContent() { ThrowIfDuplicates(All); All @@ -165,6 +165,20 @@ namespace Barotrauma SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray()); } + public static void RefreshUpdatedMods() + { + if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) + { + SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ?? + ContentPackageManager.CorePackages.First()); + } + SetRegular(Regular + .Select(p => ContentPackageManager.RegularPackages.Contains(p) + ? p + : ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 => p2.SteamWorkshopId == p.SteamWorkshopId)) + .ToArray()); + } + public static void BackUp() { if (BackupPackages.Core != null || BackupPackages.Regular != null) @@ -327,7 +341,8 @@ namespace Barotrauma => LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular); public static IEnumerable AllPackages - => LocalPackages.CollectionConcat(WorkshopPackages); + => VanillaCorePackage.ToEnumerable().CollectionConcat(LocalPackages).CollectionConcat(WorkshopPackages) + .OfType(); public static void UpdateContentPackageList() { @@ -447,13 +462,13 @@ namespace Barotrauma int pkgCount = 1 + enabledRegularPackages.Count; //core + regular - loadingRange = new Range(0.01f, 1.0f / pkgCount); + loadingRange = new Range(0.01f, 0.01f + (0.99f / pkgCount)); foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage)) { yield return p.Transform(loadingRange); } - loadingRange = new Range(1.0f / pkgCount, 1.0f); + loadingRange = new Range(0.01f + (0.99f / pkgCount), 1.0f); foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages)) { yield return p.Transform(loadingRange); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 8b328672a..d105e09cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -49,7 +49,7 @@ namespace Barotrauma .Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase); } } - var allPackages = ContentPackageManager.AllPackages; + var allPackages = ContentPackageManager.EnabledPackages.All; 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 60f048a4e..5e378bcb0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -53,8 +53,6 @@ namespace Barotrauma public IEnumerable GetChildElements(string name) => Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase)); - public XAttribute? Attribute(string name) => Element.Attribute(name); - public XAttribute? GetAttribute(string name) => Element.GetAttribute(name); public IEnumerable Attributes() => Element.Attributes(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/MissingContentPackageException.cs index 2c6d0df5e..f6fb91198 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 installed."; + $"that is not currently enabled."; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ModProject.cs deleted file mode 100644 index e69de29bb..000000000 diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 8562d6800..f9b027f33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -168,25 +168,34 @@ namespace Barotrauma }; })); + void printMapEntityPrefabs(IEnumerable prefabs) where T : MapEntityPrefab + { + NewMessage("***************", Color.Cyan); + foreach (T prefab in prefabs) + { + if (prefab.Name.IsNullOrEmpty()) { continue; } + string text = $"- {prefab.Name}"; + if (prefab.Tags.Any()) + { + text += $" ({string.Join(", ", prefab.Tags)})"; + } + if (prefab.AllowedLinks?.Any() ?? false) + { + text += $", Links: {string.Join(", ", prefab.AllowedLinks)}"; + } + NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple); + } + NewMessage("***************", Color.Cyan); + } commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) => { - NewMessage("***************", Color.Cyan); - foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) - { - if (itemPrefab.Name.IsNullOrEmpty()) { continue; } - string text = $"- {itemPrefab.Name}"; - if (itemPrefab.Tags.Any()) - { - text += $" ({string.Join(", ", itemPrefab.Tags)})"; - } - if (itemPrefab.AllowedLinks.Any()) - { - text += $", Links: {string.Join(", ", itemPrefab.AllowedLinks)}"; - } - NewMessage(text, Color.Cyan); - } - NewMessage("***************", Color.Cyan); + printMapEntityPrefabs(ItemPrefab.Prefabs); + })); + + commands.Add(new Command("itemassemblies", "itemassemblies: List all the item assemblies available for spawning.", (string[] args) => + { + printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs); })); @@ -202,6 +211,7 @@ namespace Barotrauma string[] creatureAndJobNames = CharacterPrefab.Prefabs.Select(p => p.Identifier.Value) .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value)) + .OrderBy(s => s) .ToArray(); return new string[][] @@ -732,9 +742,16 @@ namespace Barotrauma { #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { return; } - Character.Controlled = null; - GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; - GameMain.Client?.SendConsoleCommand("freecam"); + + if (GameMain.Client == null) + { + Character.Controlled = null; + GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; + } + else + { + GameMain.Client?.SendConsoleCommand("freecam"); + } #endif }, isCheat: true)); @@ -1786,15 +1803,26 @@ namespace Barotrauma { if (GameMain.GameSession?.Map?.CurrentLocation is Location location) { - - var msg = "--- Location: " + location.Name + " ---"; - msg += "\nBalance: " + location.StoreCurrentBalance; - msg += "\nPrice modifier: " + location.StorePriceModifier + "%"; - msg += "\nDaily specials:"; - location.DailySpecials.ForEach(i => msg += "\n - " + i.Name.Value); - msg += "\nRequested goods:"; - location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name.Value); - NewMessage(msg); + if (location.Stores != null) + { + var msg = "--- Location: " + location.Name + " ---"; + foreach (var store in location.Stores) + { + msg += $"\nStore identifier: {store.Value.Identifier}"; + msg += $"\nBalance: {store.Value.Balance}"; + msg += $"\nPrice modifier: {store.Value.PriceModifier}%"; + msg += "\nDaily specials:"; + store.Value.DailySpecials.ForEach(i => msg += $"\n - {i.Name}"); + msg += "\nRequested goods:"; + store.Value.RequestedGoods.ForEach(i => msg += $"\n - {i.Name}"); + + } + NewMessage(msg); + } + else + { + NewMessage($"No stores at {location}, can't show store info."); + } } else { @@ -2382,7 +2410,7 @@ namespace Barotrauma public static void ThrowError(LocalizedString error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) { - ThrowError(error.Value); + ThrowError(error.Value, e, createMessageBox, appendStackTrace); } public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index eb6756fe0..3c8e23fd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -32,7 +32,7 @@ namespace Barotrauma public ArtifactEvent(EventPrefab prefab) : base(prefab) { - if (prefab.ConfigElement.Attribute("itemname") != null) + if (prefab.ConfigElement.GetAttribute("itemname") != null) { DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item."); string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs index edd2baefa..b01ede512 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/EventAction.cs @@ -30,7 +30,7 @@ namespace Barotrauma public SubactionGroup(ScriptedEvent scriptedEvent, ContentXElement elem) { - Text = elem.Attribute("text")?.Value ?? ""; + Text = elem.GetAttribute("text")?.Value ?? ""; Actions = new List(); EndConversation = elem.GetAttributeBool("endconversation", false); foreach (var e in elem.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 6b09ce371..d7d042774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -153,6 +153,13 @@ namespace Barotrauma swarmSpawned = true; } +#if DEBUG || UNSTABLE + if (State == 1 && !level.CheckBeaconActive()) + { + DebugConsole.ThrowError("Beacon became inactive!"); + State = 2; + } +#endif } public override void End() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 934b9df66..8a02928df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -248,7 +248,7 @@ namespace Barotrauma { if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit) { - int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f); + int deliveredItemCount = items.Count(it => IsItemDelivered(it)); if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount) { GiveReward(); @@ -267,5 +267,12 @@ namespace Barotrauma items.Clear(); failed = !completed; } + + private bool IsItemDelivered(Item item) + { + if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; } + var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine; + return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index a525764bd..413e835e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -419,17 +419,17 @@ namespace Barotrauma public static int DistributeRewardsToCrew(IEnumerable crew, int totalReward) { int remainingRewards = totalReward; - HashSet nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet(); - float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution); - if (sum == 0) { return remainingRewards; } - foreach (Character character in nonBotCrew) + float sum = GetRewardDistibutionSum(crew); + if (MathUtils.NearlyEqual(sum, 0)) { return remainingRewards; } + foreach (Character character in crew) { - float rewardWeight = character.Wallet.RewardDistribution / sum; - int reward = (int)Math.Floor(totalReward * rewardWeight); - reward = Math.Max(remainingRewards, reward); + int rewardDistribution = character.Wallet.RewardDistribution; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; + int reward = (int)(totalReward * rewardWeight); + reward = Math.Min(remainingRewards, reward); character.Wallet.Give(reward); remainingRewards -= reward; - if (0 >= remainingRewards) { break; } + if (remainingRewards <= 0) { break; } } return remainingRewards; @@ -442,26 +442,27 @@ namespace Barotrauma IEnumerable characters = crewManager.GetCharacters(); #if SERVER - return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters); + return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters); #elif CLIENT return characters; #endif - static bool IsAlive(Character c) { return c?.Info != null && !c.IsDead; } } + public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) + + public static (int Amount, int Percentage, float Sum) GetRewardShare(int rewardDistribution, IEnumerable crew, Option reward) { - float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; - if (sum == 0) { return (0, 0); } + float sum = GetRewardDistibutionSum(crew, rewardDistribution); + if (MathUtils.NearlyEqual(sum, 0)) { return (0, 0, sum); } - float rewardWeight = rewardDistribution / sum; + float rewardWeight = sum > 100 ? rewardDistribution / sum : rewardDistribution / 100f; int rewardPercentage = (int)(rewardWeight * 100); return reward switch { - Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage), - None _ => (0, rewardPercentage), + Some { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage, sum), + None _ => (0, rewardPercentage, sum), _ => throw new ArgumentOutOfRangeException() }; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 8b6c7e1a1..192830b91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -209,7 +209,7 @@ namespace Barotrauma break; case "locationtype": case "connectiontype": - if (subElement.Attribute("identifier") != null) + if (subElement.GetAttribute("identifier") != null) { AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", "")); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index ec2aea487..c51ad855c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -228,7 +228,7 @@ namespace Barotrauma } GiveReward(); completed = true; - if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))) + if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))) { level.LevelData.HasHuntingGrounds = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 7526d1b25..858e8a74d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -48,7 +48,7 @@ namespace Barotrauma { containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); - if (prefab.ConfigElement.Attribute("itemname") != null) + if (prefab.ConfigElement.GetAttribute("itemname") != null) { DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); string itemName = prefab.ConfigElement.GetAttributeString("itemname", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 585de85c8..23a121111 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -30,23 +30,21 @@ namespace Barotrauma } #if CLIENT - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null) + public PurchasedItem(ItemPrefab itemPrefab, int quantity) + : this(itemPrefab, quantity, buyer: null) { } +#endif + public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) { ItemPrefab = itemPrefab; Quantity = quantity; IsStoreComponentEnabled = null; BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0; } -#elif SERVER - public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer) - { - ItemPrefab = itemPrefab; - Quantity = quantity; - IsStoreComponentEnabled = null; - BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0; - } -#endif + public override string ToString() + { + return $"{ItemPrefab.Name} ({Quantity})"; + } } class SoldItem @@ -133,11 +131,11 @@ namespace Barotrauma public const int MaxQuantity = 100; - public List ItemsInBuyCrate { get; } = new List(); - public List ItemsInSellCrate { get; } = new List(); - public List ItemsInSellFromSubCrate { get; } = new List(); - public List PurchasedItems { get; } = new List(); - public List SoldItems { get; } = new List(); + public Dictionary> ItemsInBuyCrate { get; } = new Dictionary>(); + public Dictionary> ItemsInSellCrate { get; } = new Dictionary>(); + public Dictionary> ItemsInSellFromSubCrate { get; } = new Dictionary>(); + public Dictionary> PurchasedItems { get; } = new Dictionary>(); + public Dictionary> SoldItems { get; } = new Dictionary>(); private readonly CampaignMode campaign; @@ -154,6 +152,60 @@ namespace Barotrauma this.campaign = campaign; } + private List GetItems(Identifier identifier, Dictionary> items, bool create = false) + { + if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null) + { + return storeSpecificItems; + } + else if (create) + { + storeSpecificItems = new List(); + items.Add(identifier, storeSpecificItems); + return storeSpecificItems; + } + else + { + return new List(); + } + } + + public List GetBuyCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInBuyCrate, create); + + public List GetBuyCrateItems(Location.StoreInfo store, bool create = false) => GetBuyCrateItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetBuyCrateItem(Identifier identifier, ItemPrefab prefab) => GetBuyCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetBuyCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetBuyCrateItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetSellCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellCrate, create); + + public List GetSellCrateItems(Location.StoreInfo store, bool create = false) => GetSellCrateItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab) => GetSellCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetSellCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSellCrateItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetSubCrateItems(Identifier identifier, bool create = false) => GetItems(identifier, ItemsInSellFromSubCrate, create); + + public List GetSubCrateItems(Location.StoreInfo store, bool create = false) => GetSubCrateItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetSubCrateItem(Identifier identifier, ItemPrefab prefab) => GetSubCrateItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetSubCrateItem(Location.StoreInfo store, ItemPrefab prefab) => GetSubCrateItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetPurchasedItems(Identifier identifier, bool create = false) => GetItems(identifier, PurchasedItems, create); + + public List GetPurchasedItems(Location.StoreInfo store, bool create = false) => GetPurchasedItems(store?.Identifier ?? Identifier.Empty, create); + + public PurchasedItem GetPurchasedItem(Identifier identifier, ItemPrefab prefab) => GetPurchasedItems(identifier)?.FirstOrDefault(i => i.ItemPrefab == prefab); + + public PurchasedItem GetPurchasedItem(Location.StoreInfo store, ItemPrefab prefab) => GetPurchasedItem(store?.Identifier ?? Identifier.Empty, prefab); + + public List GetSoldItems(Identifier identifier, bool create = false) => GetItems(identifier, SoldItems, create); + + public List GetSoldItems(Location.StoreInfo store, bool create = false) => GetSoldItems(store?.Identifier ?? Identifier.Empty, create); + public void ClearItemsInBuyCrate() { ItemsInBuyCrate.Clear(); @@ -172,61 +224,62 @@ namespace Barotrauma OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void SetPurchasedItems(List items) + public void SetPurchasedItems(Dictionary> purchasedItems) { PurchasedItems.Clear(); - PurchasedItems.AddRange(items); + foreach (var entry in purchasedItems) + { + PurchasedItems.Add(entry.Key, entry.Value); + } OnPurchasedItemsChanged?.Invoke(); } - public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) + public void ModifyItemQuantityInBuyCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { - var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemInCrate != null) + if (GetBuyCrateItem(storeIdentifier, itemPrefab) is { } item) { - itemInCrate.Quantity += changeInQuantity; - if (itemInCrate.Quantity < 1) + item.Quantity += changeInQuantity; + if (item.Quantity < 1) { - ItemsInBuyCrate.Remove(itemInCrate); + GetBuyCrateItems(storeIdentifier, create: true).Remove(item); } } else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); - ItemsInBuyCrate.Add(itemInCrate); + GetBuyCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client)); } OnItemsInBuyCrateChanged?.Invoke(); } - public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null) + public void ModifyItemQuantityInSubSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client = null) { - var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); - if (itemInCrate != null) + if (GetSubCrateItem(storeIdentifier, itemPrefab) is { } item) { - itemInCrate.Quantity += changeInQuantity; - if (itemInCrate.Quantity < 1) + item.Quantity += changeInQuantity; + if (item.Quantity < 1) { - ItemsInSellFromSubCrate.Remove(itemInCrate); + GetSubCrateItems(storeIdentifier)?.Remove(item); } } else if (changeInQuantity > 0) { - itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client); - ItemsInSellFromSubCrate.Add(itemInCrate); + GetSubCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity, client)); } OnItemsInSellFromSubCrateChanged?.Invoke(); } - public void PurchaseItems(List itemsToPurchase, bool removeFromCrate, Client client = null) + public void PurchaseItems(Identifier storeIdentifier, List itemsToPurchase, bool removeFromCrate, Client client = null) { - // Check all the prices before starting the transaction - // to make sure the modifiers stay the same for the whole transaction - Dictionary buyValues = GetBuyValuesAtCurrentLocation(itemsToPurchase.Select(i => i.ItemPrefab)); - + var store = Location.GetStore(storeIdentifier); + if (store == null) { return; } + var itemsPurchasedFromStore = GetPurchasedItems(storeIdentifier, create: true); + // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction + var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab)); + var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true); foreach (PurchasedItem item in itemsToPurchase) { // Add to the purchased items - var purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item.ItemPrefab); + var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab); if (purchasedItem != null) { purchasedItem.Quantity += item.Quantity; @@ -234,53 +287,54 @@ namespace Barotrauma else { purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client); - PurchasedItems.Add(purchasedItem); + itemsPurchasedFromStore.Add(purchasedItem); } - // Exchange money - var itemValue = item.Quantity * buyValues[item.ItemPrefab]; + int itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.GetWallet(client).TryDeduct(itemValue); GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value); - Location.StoreCurrentBalance += itemValue; - + store.Balance += itemValue; if (removeFromCrate) { // Remove from the shopping crate - var crateItem = ItemsInBuyCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab); - if (crateItem != null) + if (itemsInStoreCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } crateItem) { crateItem.Quantity -= item.Quantity; - if (crateItem.Quantity < 1) { ItemsInBuyCrate.Remove(crateItem); } + if (crateItem.Quantity < 1) { itemsInStoreCrate.Remove(crateItem); } } } } OnPurchasedItemsChanged?.Invoke(); } - public Dictionary GetBuyValuesAtCurrentLocation(IEnumerable items) + public Dictionary GetBuyValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable items) { var buyValues = new Dictionary(); + var store = Location?.GetStore(storeIdentifier); + if (store == null) { return buyValues; } foreach (var item in items) { if (item == null) { continue; } if (!buyValues.ContainsKey(item)) { - var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0; + int buyValue = store?.GetAdjustedItemBuyPrice(item) ?? 0; buyValues.Add(item, buyValue); } } return buyValues; } - public Dictionary GetSellValuesAtCurrentLocation(IEnumerable items) + public Dictionary GetSellValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable items) { var sellValues = new Dictionary(); + var store = Location?.GetStore(storeIdentifier); + if (store == null) { return sellValues; } foreach (var item in items) { if (item == null) { continue; } if (!sellValues.ContainsKey(item)) { - var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0; + int sellValue = store?.GetAdjustedItemSellPrice(item) ?? 0; sellValues.Add(item, sellValue); } } @@ -289,7 +343,13 @@ namespace Barotrauma public void CreatePurchasedItems() { - CreateItems(PurchasedItems, Submarine.MainSub); + var items = new List(); + foreach (var storeSpecificItems in PurchasedItems) + { + items.AddRange(storeSpecificItems.Value); + } + CreateItems(items, Submarine.MainSub); + PurchasedItems.Clear(); OnPurchasedItemsChanged?.Invoke(); } @@ -509,33 +569,41 @@ namespace Barotrauma public void SavePurchasedItems(XElement parentElement) { var itemsElement = new XElement("cargo"); - foreach (PurchasedItem item in PurchasedItems) + foreach (var storeSpecificItems in PurchasedItems) { - if (item?.ItemPrefab == null) { continue; } - itemsElement.Add(new XElement("item", - new XAttribute("id", item.ItemPrefab.Identifier), - new XAttribute("qty", item.Quantity), - new XAttribute("buyer", item.BuyerCharacterInfoId))); + foreach (var item in storeSpecificItems.Value) + { + if (item?.ItemPrefab == null) { continue; } + itemsElement.Add(new XElement("item", + new XAttribute("id", item.ItemPrefab.Identifier), + new XAttribute("qty", item.Quantity), + new XAttribute("storeid", storeSpecificItems.Key), + new XAttribute("buyer", item.BuyerCharacterInfoId))); + } } parentElement.Add(itemsElement); } public void LoadPurchasedItems(XElement element) { - var purchasedItems = new List(); + var purchasedItems = new Dictionary>(); if (element != null) { foreach (XElement itemElement in element.GetChildElements("item")) { - string id = itemElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); + string prefabId = itemElement.GetAttributeString("id", null); + if (string.IsNullOrWhiteSpace(prefabId)) { continue; } + var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == prefabId); if (prefab == null) { continue; } int qty = itemElement.GetAttributeInt("qty", 0); + Identifier storeId = itemElement.GetAttributeIdentifier("storeid", "merchant"); int buyerId = itemElement.GetAttributeInt("buyer", 0); - - purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId)); - + if (!purchasedItems.TryGetValue(storeId, out var storeItems)) + { + storeItems = new List(); + purchasedItems.Add(storeId, storeItems); + } + storeItems.Add(new PurchasedItem(prefab, qty, buyerId)); } } SetPurchasedItems(purchasedItems); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index d32c25575..abd84ef03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -25,12 +25,18 @@ namespace Barotrauma public int Balance; } + /// + /// Network message for the server to update wallet values to clients + /// internal struct NetWalletUpdate : INetSerializableStruct { [NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)] public NetWalletTransaction[] Transactions; } + /// + /// Network message for the client to transfer money between wallets + /// [NetworkSerialize] internal struct NetWalletTransfer : INetSerializableStruct { @@ -39,7 +45,10 @@ namespace Barotrauma public int Amount; } - internal struct NetWalletSalaryUpdate : INetSerializableStruct + /// + /// Network message for the client to set the salary of someone + /// + internal struct NetWalletSetSalaryUpdate : INetSerializableStruct { [NetworkSerialize] public ushort Target; @@ -48,6 +57,10 @@ namespace Barotrauma public int NewRewardDistribution; } + /// + /// Represents the difference in balance and salary when a wallet gets updated + /// Not really used right now but could be used for notifications when receiving funds similar to how talents do it + /// [NetworkSerialize] internal struct WalletChangedData : INetSerializableStruct { @@ -82,6 +95,9 @@ namespace Barotrauma } } + /// + /// Represents an update that changed the amount of money or salary of the wallet + /// [NetworkSerialize] internal struct NetWalletTransaction : INetSerializableStruct { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index dbde33069..64bd03cb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -321,15 +321,32 @@ namespace Barotrauma } if (levelData.HasHuntingGrounds) { - var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase))); + var huntingGroundsMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))); if (!huntingGroundsMissionPrefabs.Any()) { - DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found."); + DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggrounds\" found."); } else { Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed)); - var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, p => (float)Math.Max(p.Commonness, 0.1f), rand); + // Adjust the prefab commonness based on the difficulty tag + var prefabs = huntingGroundsMissionPrefabs.ToList(); + var weights = prefabs.Select(p => (float)Math.Max(p.Commonness, 1)).ToList(); + for (int i = 0; i < prefabs.Count; i++) + { + var prefab = prefabs[i]; + var weight = weights[i]; + if (prefab.Tags.Contains("easy")) + { + weight *= MathHelper.Lerp(0.2f, 2f, MathUtils.InverseLerp(80, LevelData.HuntingGroundsDifficultyThreshold, levelData.Difficulty)); + } + else if (prefab.Tags.Contains("hard")) + { + weight *= MathHelper.Lerp(0.5f, 1.5f, MathUtils.InverseLerp(LevelData.HuntingGroundsDifficultyThreshold + 10, 80, levelData.Difficulty)); + } + weights[i] = weight; + } + var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(prefabs, weights, rand); if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase)))) { extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub)); @@ -596,22 +613,22 @@ namespace Barotrauma if (map != null && CargoManager != null) { map.CurrentLocation.RegisterTakenItems(takenItems); - map.CurrentLocation.AddToStock(CargoManager.SoldItems); + map.CurrentLocation.AddStock(CargoManager.SoldItems); CargoManager.ClearSoldItemsProjSpecific(); - map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems); + map.CurrentLocation.RemoveStock(CargoManager.PurchasedItems); } if (GameMain.NetworkMember == null) { - CargoManager.ClearItemsInBuyCrate(); - CargoManager.ClearItemsInSellCrate(); - CargoManager.ClearItemsInSellFromSubCrate(); + CargoManager?.ClearItemsInBuyCrate(); + CargoManager?.ClearItemsInSellCrate(); + CargoManager?.ClearItemsInSellFromSubCrate(); } else { if (GameMain.NetworkMember.IsServer) { CargoManager?.ClearItemsInBuyCrate(); - // TODO: CargoManager?.ClearItemsInSellFromSubCrate(); + CargoManager?.ClearItemsInSellFromSubCrate(); } else if (GameMain.NetworkMember.IsClient) { @@ -772,6 +789,11 @@ namespace Barotrauma public void AssignNPCMenuInteraction(Character character, InteractionType interactionType) { character.CampaignInteractionType = interactionType; + if (character.CampaignInteractionType == InteractionType.Store && + character.HumanPrefab is { Identifier: var merchantId }) + { + character.MerchantIdentifier = merchantId; + } character.DisableHealthWindow = interactionType != InteractionType.None && interactionType != InteractionType.Examine && diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 81c8cfbf2..970f2eaff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ using Barotrauma.IO; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -211,7 +212,6 @@ namespace Barotrauma #endif } - public static List GetCampaignSubs() { bool isSubmarineVisible(SubmarineInfo s) @@ -242,5 +242,78 @@ namespace Barotrauma return availableSubs; } + private static void WriteItems(IWriteMessage msg, Dictionary> purchasedItems) + { + msg.Write((byte)purchasedItems.Count); + foreach (var storeItems in purchasedItems) + { + msg.Write(storeItems.Key); + msg.Write((UInt16)storeItems.Value.Count); + foreach (var item in storeItems.Value) + { + msg.Write(item.ItemPrefab.Identifier); + msg.WriteRangedInteger(item.Quantity, 0, CargoManager.MaxQuantity); + } + } + } + + private static Dictionary> ReadPurchasedItems(IReadMessage msg, Client sender) + { + var items = new Dictionary>(); + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier storeId = msg.ReadIdentifier(); + items.Add(storeId, new List()); + UInt16 itemCount = msg.ReadUInt16(); + for (int j = 0; j < itemCount; j++) + { + Identifier itemId = msg.ReadIdentifier(); + int quantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); + items[storeId].Add(new PurchasedItem(ItemPrefab.Prefabs[itemId], quantity, sender)); + } + } + return items; + } + + private static void WriteItems(IWriteMessage msg, Dictionary> soldItems) + { + msg.Write((byte)soldItems.Count); + foreach (var storeItems in soldItems) + { + msg.Write(storeItems.Key); + msg.Write((UInt16)storeItems.Value.Count); + foreach (var item in storeItems.Value) + { + msg.Write(item.ItemPrefab.Identifier); + msg.Write((UInt16)item.ID); + msg.Write(item.Removed); + msg.Write(item.SellerID); + msg.Write((byte)item.Origin); + } + } + } + + private static Dictionary> ReadSoldItems(IReadMessage msg) + { + var soldItems = new Dictionary>(); + byte storeCount = msg.ReadByte(); + for (int i = 0; i < storeCount; i++) + { + Identifier storeId = msg.ReadIdentifier(); + soldItems.Add(storeId, new List()); + UInt16 itemCount = msg.ReadUInt16(); + for (int j = 0; j < storeCount; j++) + { + Identifier prefabId = msg.ReadIdentifier(); + UInt16 itemId = msg.ReadUInt16(); + bool removed = msg.ReadBoolean(); + byte sellerId = msg.ReadByte(); + byte origin = msg.ReadByte(); + soldItems[storeId].Add(new SoldItem(ItemPrefab.Prefabs[prefabId], itemId, removed, sellerId, (SoldItem.SellOrigin)origin)); + } + } + return soldItems; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 0da770a21..e6fc1f3e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -754,9 +754,9 @@ namespace Barotrauma try { - IEnumerable crewCharacters = GetSessionCrewCharacters(); + ImmutableArray crewCharacters = GetSessionCrewCharacters().ToImmutableArray(); - int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution + int prevMoney = GetAmountOfMoney(crewCharacters); foreach (Mission mission in missions) { @@ -828,7 +828,7 @@ namespace Barotrauma LogEndRoundStats(eventId); if (GameMode is CampaignMode campaignMode) { - GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton + GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney); campaignMode.TotalPlayTime += roundDuration; } #if CLIENT @@ -840,6 +840,17 @@ namespace Barotrauma { RoundEnding = false; } + + int GetAmountOfMoney(IEnumerable crew) + { + if (!(GameMode is CampaignMode campaign)) { return 0; } + + return GameMain.NetworkMember switch + { + null => campaign.Bank.Balance, + _ => crew.Sum(c => c.Wallet.Balance) + campaign.Bank.Balance + }; + } } public void LogEndRoundStats(string eventId) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 6a8df4f5e..f85b6bafe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -216,7 +216,7 @@ namespace Barotrauma price = 0; } - if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets + if (Campaign.GetWallet(client).TryDeduct(price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index e34ef8f1b..f8249369e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] + [Editable, Serialize(true, IsPropertySaveable.No, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")] public bool ApplyEffectsOnDocking { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 6f3383913..384b88db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components { int index = i - 1; string attributeName = "handle" + i; - var attribute = element.Attribute(attributeName); + var attribute = element.GetAttribute(attributeName); // If no value is defind for handle2, use the value of handle1. var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue; handlePos[index] = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 9477d5a76..1319254d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -129,7 +129,7 @@ namespace Barotrauma.Items.Components { this.item = item; - if (element.Attribute("limbfixamount") != null) + if (element.GetAttribute("limbfixamount") != null) { DebugConsole.ThrowError("Error in item \"" + item.Name + "\" - RepairTool damage should be configured using a StatusEffect with Afflictions, not the limbfixamount attribute."); } @@ -140,10 +140,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "fixable": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in RepairTool " + item.Name + " - use identifiers instead of names to configure fixable entities."); - fixableEntities.Add(subElement.Attribute("name").Value.ToIdentifier()); + fixableEntities.Add(subElement.GetAttribute("name").Value.ToIdentifier()); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8726d8462..845ae5efe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -344,7 +344,7 @@ namespace Barotrauma.Items.Components break; case "requiredskill": case "requiredskills": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill."); continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 40bd8a5b0..891fc3fce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -424,6 +424,15 @@ namespace Barotrauma.Items.Components } } + public override void UpdateBroken(float deltaTime, Camera cam) + { + //update when the item is broken too to get OnContaining effects to execute and contained item positions to update + if (IsActive) + { + Update(deltaTime, cam); + } + } + public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null) { return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 03c061552..7d03844eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components { if (subElement.Name != "limbposition") { continue; } string limbStr = subElement.GetAttributeString("limb", ""); - if (!Enum.TryParse(subElement.Attribute("limb").Value, out LimbType limbType)) + if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType)) { DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type."); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index d0ad2a519..27b9ae460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -186,8 +186,19 @@ namespace Barotrauma.Items.Components if (!isClient) { MoveIngredientsToInputContainer(selectedItem); + if (selectedItem.RequiredMoney > 0) + { + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + { + user.Wallet.Deduct(selectedItem.RequiredMoney); + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + campaign.Bank.Deduct(selectedItem.RequiredMoney); + } + } } - + requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; @@ -508,6 +519,22 @@ namespace Barotrauma.Items.Components if (fabricableItem == null) { return false; } if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } + if (fabricableItem.RequiredMoney > 0) + { + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) + { + if (character?.Wallet == null || character.Wallet.Balance < fabricableItem.RequiredMoney) { return false; } + } + else if (GameMain.GameSession?.GameMode is CampaignMode campaign) + { + if (campaign.Bank.Balance < fabricableItem.RequiredMoney) { return false; } + } + else + { + return false; + } + } + return fabricableItem.RequiredItems.All(requiredItem => { int availablePrefabsAmount = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index b375eb205..0ee0d9d50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER - hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData()); + hull.BallastFlora.CreateNetworkMessage(new BallastFloraBehavior.SpawnEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index be83e2267..0a0d2af0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; using Microsoft.Xna.Framework; using System.Collections.Generic; #if CLIENT @@ -151,20 +150,6 @@ namespace Barotrauma.Items.Components } set { - if (powerIn != null) - { - if (powerIn.Grid != null) - { - powerIn.Grid.Voltage = Math.Max(0.0f, value); - } - } - else if (powerOut != null) - { - if (powerOut.Grid != null) - { - powerOut.Grid.Voltage = Math.Max(0.0f, value); - } - } voltage = Math.Max(0.0f, value); } } @@ -213,11 +198,6 @@ namespace Barotrauma.Items.Components powerOnSoundPlayed = false; } #endif - if (powerIn == null) - { - //power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic) - Voltage -= deltaTime; - } } public override void Update(float deltaTime, Camera cam) @@ -470,6 +450,11 @@ namespace Barotrauma.Items.Components //Determine if devices are adding a load or providing power, also resolve solo nodes foreach (Powered powered in poweredList) { + //Make voltage decay to ensure the device powers down. + //This only effects devices with no power input (whose voltage is set by other means, e.g. status effects from a contained battery) + //or devices that have been disconnected from the power grid - other devices use the voltage of the grid instead. + powered.Voltage -= deltaTime; + //Handle the device if it's got a power connection if (powered.powerIn != null && powered.powerOut != powered.powerIn) { @@ -500,7 +485,7 @@ namespace Barotrauma.Items.Components } else { - powered.CurrPowerConsumption = powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); + powered.CurrPowerConsumption = -powered.GetConnectionPowerOut(powered.powerIn, 0, powered.MinMaxPowerOut(powered.powerIn, 0), 0); powered.GridResolved(powered.powerIn); } } @@ -541,7 +526,7 @@ namespace Barotrauma.Items.Components else { //Perform power calculations for the singular connection - float loadOut = powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); + float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0); if (powered is PowerTransfer pt2) { pt2.PowerLoad = loadOut; @@ -667,7 +652,7 @@ namespace Barotrauma.Items.Components public static bool ValidPowerConnection(Connection conn1, Connection conn2) { - return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.IsOutput != conn2.IsOutput || (conn1.Item.HasTag("dock") && conn2.Item.HasTag("dock"))); + return conn1.IsPower && conn2.IsPower && (conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index fcddb442b..fe52d7480 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -7,7 +7,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; using Voronoi2; namespace Barotrauma.Items.Components @@ -49,9 +48,8 @@ namespace Barotrauma.Items.Components //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; - //a duration during which the projectile won't drop from the body it's stuck to - private const float PersistentStickJointDuration = 1.0f; - private PrismaticJoint stickJoint; + private Joint stickJoint; + private Vector2 jointAxis; public Attack Attack { get; private set; } @@ -86,8 +84,6 @@ namespace Barotrauma.Items.Components get { return hits; } } - private float persistentStickJointTimer; - [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] public float LaunchImpulse { get; set; } @@ -116,13 +112,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] - public bool StickPermanently - { - get; - set; - } - [Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { @@ -151,6 +140,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.No, description: "")] + public bool StickToLightTargets + { + get; + set; + } + [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan @@ -212,16 +208,36 @@ namespace Barotrauma.Items.Components set; } + private float stickTimer; + [Serialize(0f, IsPropertySaveable.No)] + public float StickDuration + { + get; + set; + } + + [Serialize(-1f, IsPropertySaveable.No)] + public float MaxJointTranslation + { + get; + set; + } + private float maxJointTranslationInSimUnits = -1; + + [Serialize(true, IsPropertySaveable.No)] + public bool Prismatic + { + get; + set; + } + public Body StickTarget { get; private set; } - public bool IsStuckToTarget - { - get { return StickTarget != null; } - } + public bool IsStuckToTarget => StickTarget != null; private Category originalCollisionCategories; private Category originalCollisionTargets; @@ -660,23 +676,22 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - if (persistentStickJointTimer > 0.0f && !StickPermanently) + if (StickDuration > 0 && stickTimer > 0) { - persistentStickJointTimer -= deltaTime; + stickTimer -= deltaTime; return; } + float absoluteMaxTranslation = 100; // Update the item's transform to make sure it's inside the same sub as the target (or outside) - if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || Math.Abs(stickJoint.JointTranslation) > 100.0f) + if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || stickJoint is PrismaticJoint prismaticJoint && Math.Abs(prismaticJoint.JointTranslation) > absoluteMaxTranslation) { item.UpdateTransform(); } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (StickTargetRemoved() || - (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f)) || - Math.Abs(stickJoint.JointTranslation) > 100.0f) //failsafe unstick if the target is still extremely far + if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits) { Unstick(); #if SERVER @@ -706,7 +721,7 @@ namespace Barotrauma.Items.Components if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine) { - if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; } + if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; } } else if (target.Body.UserData is Limb limb) { @@ -778,7 +793,10 @@ namespace Barotrauma.Items.Components Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); - if (hits.Contains(target.Body)) { return true; } + if (hits.Contains(target.Body)) + { + return true; + } } else { @@ -936,14 +954,12 @@ namespace Barotrauma.Items.Components { item.body.LinearVelocity *= deflectedSpeedMultiplier; } - else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick - //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && - hits.Count() >= MaxTargetsToHit && - target.Body.Mass > item.body.Mass * 0.5f && + else if ( stickJoint == null && StickTarget == null && + StickToStructures && target.Body.UserData is Structure || + ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) && (DoesStick || (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || - (StickToStructures && target.Body.UserData is Structure) || - (StickToItems && target.Body.UserData is Item))) + (StickToItems && target.Body.UserData is Item)))) { Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), @@ -1025,30 +1041,39 @@ namespace Barotrauma.Items.Components private void StickToTarget(Body targetBody, Vector2 axis) { if (stickJoint != null) { return; } - - stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true) + jointAxis = axis; + item.body.ResetDynamics(); + if (Prismatic) { - MotorEnabled = true, - MaxMotorForce = 30.0f, - LimitEnabled = true, - Breakpoint = 1000.0f - }; + stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, useWorldCoordinates: true) + { + MotorEnabled = true, + MaxMotorForce = 30.0f, + LimitEnabled = true, + Breakpoint = 1000.0f + }; - if (StickPermanently) - { - stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; - item.body.ResetDynamics(); + if (maxJointTranslationInSimUnits == -1) + { + if (item.Sprite != null && MaxJointTranslation < 0) + { + MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale; + } + MaxJointTranslation = Math.Min(MaxJointTranslation, 1000); + maxJointTranslationInSimUnits = ConvertUnits.ToSimUnits(MaxJointTranslation); + } } - else if (item.Sprite != null) + else { - stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale); - stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale); + stickJoint = new WeldJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, item.body.SimPosition, useWorldCoordinates: true) + { + FrequencyHz = 10.0f, + DampingRatio = 0.5f + }; } - - persistentStickJointTimer = PersistentStickJointDuration; + stickTimer = StickDuration; StickTarget = targetBody; GameMain.World.Add(stickJoint); - IsActive = true; if (targetBody.UserData is Limb limb) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 7c5b320ba..6d1c899b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -1,9 +1,9 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -12,8 +12,36 @@ namespace Barotrauma.Items.Components private ISpatialEntity source; private Item target; + private Vector2? launchDir; + + private void SetSource(ISpatialEntity source) + { + this.source = source; + if (source is Limb sourceLimb) + { + sourceLimb.AttachedRope = this; + float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir); + } + } + + private void ResetSource() + { + if (source is Limb sourceLimb && sourceLimb.AttachedRope == this) + { + sourceLimb.AttachedRope = null; + } + source = null; + } + private float snapTimer; - private const float SnapAnimDuration = 1.0f; + + [Serialize(1.0f, IsPropertySaveable.No, description: "")] + public float SnapAnimDuration + { + get; + set; + } private float raycastTimer; private const float RayCastInterval = 0.2f; @@ -46,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")] + public float MaxAngle + { + get; + set; + } + [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] public bool SnapOnCollision { @@ -115,8 +150,8 @@ namespace Barotrauma.Items.Components { System.Diagnostics.Debug.Assert(source != null); System.Diagnostics.Debug.Assert(target != null); - this.source = source; this.target = target; + SetSource(source); Snapped = false; ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); IsActive = true; @@ -127,7 +162,7 @@ namespace Barotrauma.Items.Components if (source == null || target == null || target.Removed || (source is Entity sourceEntity && sourceEntity.Removed)) { - source = null; + ResetSource(); target = null; IsActive = false; return; @@ -144,12 +179,27 @@ namespace Barotrauma.Items.Components } Vector2 diff = target.WorldPosition - source.WorldPosition; - if (diff.LengthSquared() > MaxLength * MaxLength) + float lengthSqr = diff.LengthSquared(); + if (lengthSqr > MaxLength * MaxLength) { Snap(); return; } + if (MaxAngle < 180 && lengthSqr > 2500) + { + if (launchDir == null) + { + launchDir = diff; + } + float angle = MathHelper.ToDegrees(VectorExtensions.Angle(launchDir.Value, diff)); + if (angle > MaxAngle) + { + Snap(); + return; + } + } + #if CLIENT item.ResetCachedVisibleSize(); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index f9c057603..579e1d49c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components string displayNameTag = "", fallbackTag = ""; //if displayname is not present, attempt to find it from the prefab - if (element.Attribute("displayname") == null) + if (element.GetAttribute("displayname") == null) { foreach (var subElement in item.Prefab.ConfigElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index cdeb9bf46..92149904e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -66,7 +66,7 @@ namespace Barotrauma.Items.Components HasPropertyName = !PropertyName.IsEmpty; IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; - if (element.Attribute("signal") is XAttribute attribute) + if (element.GetAttribute("signal") is XAttribute attribute) { Signal = attribute.Value; ShouldSetProperty = HasPropertyName; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index e274d06eb..6cbb632a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -154,7 +154,7 @@ namespace Barotrauma.Items.Components IsActive = true; //backwards compatibility - if (element.Attribute("range") != null) + if (element.GetAttribute("range") != null) { rangeX = rangeY = element.GetAttributeFloat("range", 0.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 8c148b411..b783a1126 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -323,7 +323,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - if (subElement.Attribute("texture") == null) + if (subElement.GetAttribute("texture") == null) { DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!"); return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 1e2638509..93c68e6ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1389,7 +1389,10 @@ namespace Barotrauma /// public static void UpdateHulls() { - foreach (Item item in ItemList) item.FindHull(); + foreach (Item item in ItemList) + { + item.FindHull(); + } } public Hull FindHull() @@ -1677,12 +1680,17 @@ namespace Barotrauma if (!(GameMain.NetworkMember is { IsServer: true })) { return; } if (!conditionUpdatePending) { return; } - GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData()); + CreateStatusEvent(); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } + public void CreateStatusEvent() + { + GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData()); + } + private bool isActive = true; public override void Update(float deltaTime, Camera cam) @@ -2955,7 +2963,7 @@ namespace Barotrauma /// public static Item Load(ContentXElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) { - string name = element.Attribute("name").Value; + string name = element.GetAttribute("name").Value; Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); if (string.IsNullOrWhiteSpace(name) && identifier.IsEmpty) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index a4a939944..e0c65a747 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -63,7 +63,7 @@ namespace Barotrauma } } - private readonly struct StatusEventData : IEventData + private readonly struct ItemStatusEventData : IEventData { public EventType EventType => EventType.Status; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index eaa18470a..33d5b6289 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -106,12 +106,13 @@ namespace Barotrauma public readonly Identifier TargetItemPrefabIdentifier; public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier]; - private Lazy displayName; + private readonly Lazy displayName; public LocalizedString DisplayName => ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : ""; public readonly ImmutableArray RequiredItems; public readonly ImmutableArray SuitableFabricatorIdentifiers; public readonly float RequiredTime; + public readonly int RequiredMoney; public readonly bool RequiresRecipe; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly ImmutableArray RequiredSkills; @@ -130,6 +131,7 @@ namespace Barotrauma var requiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); + RequiredMoney = element.GetAttributeInt("requiredmoney", 0); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); if (OutCondition > 1.0f) { @@ -322,8 +324,13 @@ namespace Barotrauma private PriceInfo defaultPrice; public PriceInfo DefaultPrice => defaultPrice; - - private ImmutableDictionary locationPrices; + private ImmutableDictionary StorePrices { get; set; } + public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || + (StorePrices != null && StorePrices.Any(p => p.Value.CanBeBought)); + /// + /// Any item with a Price element in the definition can be sold everywhere. + /// + public bool CanBeSold => DefaultPrice != null; /// /// Defines areas where the item can be interacted with. If RequireBodyInsideTrigger is set to true, the character @@ -393,13 +400,6 @@ namespace Barotrauma /// public bool? AllowAsExtraCargo { get; private set; } - public bool CanBeBought => (DefaultPrice != null && DefaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); - - /// - /// Any item with a Price element in the definition can be sold everywhere. - /// - public bool CanBeSold => DefaultPrice != null; - public bool RandomDeconstructionOutput { get; private set; } public int RandomDeconstructionOutputAmount { get; private set; } @@ -675,11 +675,11 @@ namespace Barotrauma var deconstructItems = new List(); var fabricationRecipes = new Dictionary(); var treatmentSuitability = new Dictionary(); - var locationPrices = new Dictionary(); + var storePrices = new Dictionary(); var preferredContainers = new List(); DeconstructTime = 1.0f; - if (ConfigElement.Attribute("allowasextracargo") != null) + if (ConfigElement.GetAttribute("allowasextracargo") != null) { AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false); } @@ -690,7 +690,7 @@ namespace Barotrauma this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty()).ToImmutableHashSet(); } - if (ConfigElement.Attribute("cargocontainername") != null) + if (ConfigElement.GetAttribute("cargocontainername") != null) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - cargo container should be configured using the item's identifier, not the name."); } @@ -731,39 +731,46 @@ namespace Barotrauma CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); sprite = new Sprite(subElement, spriteFolder, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && - subElement.Attribute("sheetindex") == null) + if (subElement.GetAttribute("sourcerect") == null && + subElement.GetAttribute("sheetindex") == null) { DebugConsole.ThrowError($"Warning - sprite sourcerect not configured for item \"{ToString()}\"!"); } Size = Sprite.size; - if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) + if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace()) { Sprite.Name = Name.Value; } Sprite.EntityIdentifier = Identifier; break; case "price": - if (subElement.Attribute("baseprice") != null) + if (subElement.GetAttribute("baseprice") != null) { - foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out this.defaultPrice)) + foreach (var priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice)) { - if (priceInfo == null) { continue; } - locationPrices.Add(priceInfo.Item1, priceInfo.Item2); + if (priceInfo.StoreIdentifier.IsEmpty) { continue; } + if (storePrices.ContainsKey(priceInfo.StoreIdentifier)) + { + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the store \"{priceInfo.StoreIdentifier}\" defined more than once."); + storePrices[priceInfo.StoreIdentifier] = priceInfo; + } + else + { + storePrices.Add(priceInfo.StoreIdentifier, priceInfo); + } } } - else if (subElement.Attribute("buyprice") != null) + else if (subElement.GetAttribute("buyprice") != null && subElement.GetAttributeIdentifier("locationtype", "") is { IsEmpty: false } locationType) // Backwards compatibility { - Identifier locationType = subElement.GetAttributeIdentifier("locationtype", ""); - if (locationPrices.ContainsKey(locationType)) + if (storePrices.ContainsKey(locationType)) { - DebugConsole.AddWarning($"Error in item prefab \"{ToString()}\": price for the location type \"{locationType}\" defined more than once."); - locationPrices[locationType] = new PriceInfo(subElement); + DebugConsole.AddWarning($"Error in item prefab \"{this}\": price for the location type \"{locationType}\" defined more than once."); + storePrices[locationType] = new PriceInfo(subElement); } else { - locationPrices.Add(locationType, new PriceInfo(subElement)); + storePrices.Add(locationType, new PriceInfo(subElement)); } } break; @@ -851,7 +858,7 @@ namespace Barotrauma } break; case "suitabletreatment": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError($"Error in item prefab \"{ToString()}\" - suitable treatments should be defined using item identifiers, not item names."); } @@ -870,15 +877,15 @@ namespace Barotrauma this.DeconstructItems = deconstructItems.ToImmutableArray(); this.FabricationRecipes = fabricationRecipes.ToImmutableDictionary(); this.treatmentSuitability = treatmentSuitability.ToImmutableDictionary(); - this.locationPrices = locationPrices.ToImmutableDictionary(); + StorePrices = storePrices.ToImmutableDictionary(); this.PreferredContainers = preferredContainers.ToImmutableArray(); this.LevelCommonness = levelCommonness.ToImmutableDictionary(); this.LevelQuantity = levelQuantity.ToImmutableDictionary(); // Backwards compatibility - if (locationPrices != null && locationPrices.Any()) + if (storePrices.Any()) { - this.defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); } HideConditionInTooltip = ConfigElement.GetAttributeBool("hideconditionintooltip", HideConditionBar); @@ -930,31 +937,109 @@ namespace Barotrauma return treatmentSuitability.TryGetValue(treatmentIdentifier, out float suitability) ? suitability : 0.0f; } - public PriceInfo GetPriceInfo(Location location) + #region Pricing + + public PriceInfo GetPriceInfo(Location.StoreInfo store) { - if (location?.Type == null) { return null; } - var locationTypeId = location.Type.Identifier; - if (locationPrices != null && locationPrices.ContainsKey(locationTypeId)) + if (!store.Identifier.IsEmpty && StorePrices != null && StorePrices.TryGetValue(store.Identifier, out var storePriceInfo)) { - return locationPrices[locationTypeId]; + return storePriceInfo; } else { return DefaultPrice; } } - - public bool CanBeBoughtAtLocation(Location location, out PriceInfo priceInfo) + + public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { - priceInfo = null; - if (location?.Type == null) { return false; } - priceInfo = GetPriceInfo(location); - return - priceInfo != null && - priceInfo.CanBeBought && - (location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + priceInfo = GetPriceInfo(store); + return priceInfo != null && priceInfo.CanBeBought && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } + public bool CanBeBoughtFrom(Location location) + { + if (location?.Stores == null) { return false; } + foreach (var store in location.Stores) + { + var priceInfo = GetPriceInfo(store.Value); + if (priceInfo == null) { continue; } + if (!priceInfo.CanBeBought) { continue; } + if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; } + return true; + } + return false; + } + + public int? GetMinPrice() + { + int? minPrice = StorePrices.Values.Min(p => p.Price); + if (minPrice.HasValue) + { + if (DefaultPrice != null) + { + return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; + } + else + { + return minPrice.Value; + } + } + else + { + return DefaultPrice?.Price; + } + } + + public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) + { + var prices = new Dictionary(); + if (StorePrices != null) + { + foreach (var storePrice in StorePrices) + { + var priceInfo = storePrice.Value; + if (priceInfo == null) + { + continue; + } + if (!priceInfo.CanBeBought) + { + continue; + } + if (priceInfo.Price < maxCost || maxCost == 0) + { + prices.Add(storePrice.Key, priceInfo); + } + } + } + return prices.ToImmutableDictionary(); + } + + public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) + { + var prices = new Dictionary(); + if (!CanBeSold && sellingImportant) + { + return prices.ToImmutableDictionary(); + } + foreach (var storePrice in StorePrices) + { + var priceInfo = storePrice.Value; + if (priceInfo == null) + { + continue; + } + if (priceInfo.Price > minCost) + { + prices.Add(storePrice.Key, priceInfo); + } + } + return prices.ToImmutableDictionary(); + } + + #endregion + public static ItemPrefab Find(string name, Identifier identifier) { if (string.IsNullOrEmpty(name) && identifier.IsEmpty) @@ -988,77 +1073,6 @@ namespace Barotrauma return prefab; } - public int? GetMinPrice() - { - int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null; - if (minPrice.HasValue) - { - if (DefaultPrice != null) - { - return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; - } - else - { - return minPrice.Value; - } - } - else - { - return DefaultPrice?.Price; - } - } - - public ImmutableDictionary GetBuyPricesUnder(int maxCost = 0) - { - Dictionary priceLocations = new Dictionary(); - if (locationPrices != null) - { - foreach (KeyValuePair locationPrice in locationPrices) - { - PriceInfo priceInfo = locationPrice.Value; - - if (priceInfo == null) - { - continue; - } - if (!priceInfo.CanBeBought) - { - continue; - } - if (priceInfo.Price < maxCost || maxCost == 0) - { - priceLocations.Add(locationPrice.Key, priceInfo); - } - } - } - return priceLocations.ToImmutableDictionary(); - } - - public ImmutableDictionary GetSellPricesOver(int minCost = 0, bool sellingImportant = true) - { - Dictionary priceLocations = new Dictionary(); - - if (!CanBeSold && sellingImportant) - { - return priceLocations.ToImmutableDictionary(); - } - - foreach (KeyValuePair locationPrice in locationPrices) - { - PriceInfo priceInfo = locationPrice.Value; - - if (priceInfo == null) - { - continue; - } - if (priceInfo.Price > minCost) - { - priceLocations.Add(locationPrice.Key, priceInfo); - } - } - return priceLocations.ToImmutableDictionary(); - } - public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false) { isPreferencesDefined = PreferredContainers.Any(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 136178582..46ae8392c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -167,7 +167,7 @@ namespace Barotrauma public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName) { Identifier[] identifiers; - if (element.Attribute("name") != null) + if (element.GetAttribute("name") != null) { //backwards compatibility + a console warning DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 9708fd2e4..2be6ddb93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -520,8 +520,9 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.Health > branch.MaxHealth * 0.9f || branch.DisconnectedFromRoot) { continue; } float branchHealAmount = (float)(MaxBranchHealthRegenDistance - branch.BranchDepth) / MaxBranchHealthRegenDistance * healAmount; if (branchHealAmount <= 0.0f) { continue; } + float prevHealth = branch.Health; branch.Health += branchHealAmount; - branch.AccumulatedDamage -= branchHealAmount; + branch.AccumulatedDamage += (prevHealth - branch.Health); } } StateMachine.Update(deltaTime); @@ -633,7 +634,8 @@ namespace Barotrauma.MapCreatures.Behavior { if (branch.ParentBranch != null && (branch.ParentBranch.DisconnectedFromRoot || branch.ParentBranch.Health <= 0.0f)) { - DamageBranch(branch, deltaTime * MathHelper.Lerp(10.0f, 0.01f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth), AttackType.CutFromRoot); + float speed = MathHelper.Lerp(5.0f, 0.1f, branch.ParentBranch.Health / branch.ParentBranch.MaxHealth); + DamageBranch(branch, speed * speed * deltaTime, AttackType.CutFromRoot); } if (branch.Health <= 0.0f) { @@ -836,7 +838,7 @@ namespace Barotrauma.MapCreatures.Behavior } #if SERVER - SendNetworkMessage(new BranchCreateEventData(newBranch, parent)); + CreateNetworkMessage(new BranchCreateEventData(newBranch, parent)); #endif return true; } @@ -878,7 +880,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - SendNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); + CreateNetworkMessage(new InfectEventData(target, InfectEventData.InfectState.Yes, branch)); } #endif } @@ -955,8 +957,6 @@ namespace Barotrauma.MapCreatures.Behavior /// private void CreateBody(BallastFloraBranch branch) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - Rectangle rect = branch.Rect; Vector2 pos = Parent.Position + Offset + branch.Position; @@ -975,6 +975,14 @@ 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) { @@ -983,8 +991,28 @@ namespace Barotrauma.MapCreatures.Behavior if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } - // damage is handled server side currently - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (type != AttackType.Other && type != AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + Anger += damage * 0.001f; + } + + if (GameMain.NetworkMember != null) + { + // damage is handled server side + if (GameMain.NetworkMember.IsClient) + { + return; + } + else + { + //accumulate damage on the server's side to ensure clients get notified + if (type == AttackType.Other || type == AttackType.CutFromRoot) + { + branch.AccumulatedDamage += damage; + } + } + } if (attacker != null && toxinsCooldown <= 0) { @@ -1014,11 +1042,6 @@ namespace Barotrauma.MapCreatures.Behavior } branch.Health -= damage; - if (type != AttackType.Other && type != AttackType.CutFromRoot) - { - branch.AccumulatedDamage += damage; - Anger += damage * 0.001f; - } #if SERVER GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); @@ -1110,7 +1133,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!wasRemoved) { - SendNetworkMessage(new BranchRemoveEventData(branch)); + CreateNetworkMessage(new BranchRemoveEventData(branch)); } #endif } @@ -1141,7 +1164,7 @@ namespace Barotrauma.MapCreatures.Behavior } }); #if SERVER - SendNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); + CreateNetworkMessage(new InfectEventData(item, InfectEventData.InfectState.No, null)); #endif } @@ -1159,7 +1182,7 @@ namespace Barotrauma.MapCreatures.Behavior StateMachine?.State?.Exit(); #if SERVER - SendNetworkMessage(new KillEventData()); + CreateNetworkMessage(new KillEventData()); #endif } @@ -1181,7 +1204,7 @@ namespace Barotrauma.MapCreatures.Behavior _entityList.Remove(this); #if SERVER - SendNetworkMessage(new KillEventData()); + CreateNetworkMessage(new KillEventData()); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index a26a12e44..89ee34a92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -88,7 +88,7 @@ namespace Barotrauma flash = element.GetAttributeBool("flash", showEffects); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); - if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } + if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow); EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 698152a24..d0c9e6d0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -739,7 +739,7 @@ namespace Barotrauma { Rectangle rect = Rectangle.Empty; - if (element.Attribute("rect") != null) + if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); } @@ -747,15 +747,15 @@ namespace Barotrauma { //backwards compatibility rect = new Rectangle( - int.Parse(element.Attribute("x").Value), - int.Parse(element.Attribute("y").Value), - int.Parse(element.Attribute("width").Value), - int.Parse(element.Attribute("height").Value)); + int.Parse(element.GetAttribute("x").Value), + int.Parse(element.GetAttribute("y").Value), + int.Parse(element.GetAttribute("width").Value), + int.Parse(element.GetAttribute("height").Value)); } bool isHorizontal = rect.Height > rect.Width; - var horizontalAttribute = element.Attribute("horizontal"); + var horizontalAttribute = element.GetAttribute("horizontal"); if (horizontalAttribute != null) { isHorizontal = horizontalAttribute.Value.ToString() == "true"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index c83684db8..e531500bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1564,7 +1564,7 @@ namespace Barotrauma public static Hull Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect; - if (element.Attribute("rect") != null) + if (element.GetAttribute("rect") != null) { rect = element.GetAttributeRect("rect", Rectangle.Empty); } @@ -1572,10 +1572,10 @@ namespace Barotrauma { //backwards compatibility rect = new Rectangle( - int.Parse(element.Attribute("x").Value), - int.Parse(element.Attribute("y").Value), - int.Parse(element.Attribute("width").Value), - int.Parse(element.Attribute("height").Value)); + int.Parse(element.GetAttribute("x").Value), + int.Parse(element.GetAttribute("y").Value), + int.Parse(element.GetAttribute("width").Value), + int.Parse(element.GetAttribute("height").Value)); } var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element)) @@ -1639,7 +1639,7 @@ namespace Barotrauma } SerializableProperty.DeserializeProperties(hull, element); - if (element.Attribute("oxygen") == null) { hull.Oxygen = hull.Volume; } + if (element.GetAttribute("oxygen") == null) { hull.Oxygen = hull.Volume; } return hull; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 13d72bc6a..05539c6e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -160,17 +160,20 @@ namespace Barotrauma public void Delete() { Dispose(); - if (File.Exists(ContentFile.Path)) + try { - try + if (ContentPackage is { Files: { Length: 1 } } + && ContentPackageManager.LocalPackages.Contains(ContentPackage)) { - File.Delete(ContentFile.Path); - } - catch (Exception e) - { - DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + Directory.Delete(ContentPackage.Dir, recursive: true); + ContentPackageManager.LocalPackages.Refresh(); + ContentPackageManager.EnabledPackages.DisableRemovedMods(); } } + catch (Exception e) + { + DebugConsole.ThrowError("Deleting item assembly \"" + Name + "\" failed.", e); + } } public override void Dispose() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 42b982c38..f184f08f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -3826,12 +3826,12 @@ namespace Barotrauma if (location != null) { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } else { DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})"); - outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); + outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost); } foreach (string categoryToHide in locationType.HideEntitySubcategories) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 4fd6d7166..8356a9461 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -31,8 +31,20 @@ namespace Barotrauma public bool HasHuntingGrounds, OriginallyHadHuntingGrounds; + /// + /// Minimum difficulty of the level before hunting grounds can appear. + /// + public const float HuntingGroundsDifficultyThreshold = 25; + + /// + /// Probability of hunting grounds appearing in 100% difficulty levels. + /// + public const float MaxHuntingGroundsProbability = 0.3f; + public OutpostGenerationParams ForceOutpostGenerationParams; + public bool AllowInvalidOutpost; + public readonly Point Size; public readonly int InitialDepth; @@ -150,11 +162,7 @@ namespace Barotrauma } else { - //minimum difficulty of the level before hunting grounds can appear - float huntingGroundsDifficultyThreshold = 25; - //probability of hunting grounds appearing in 100% difficulty levels - float maxHuntingGroundsProbability = 0.3f; - HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(huntingGroundsDifficultyThreshold, 100.0f, Difficulty) * maxHuntingGroundsProbability; + HasHuntingGrounds = OriginallyHadHuntingGrounds = rand.NextDouble() < MathUtils.InverseLerp(HuntingGroundsDifficultyThreshold, 100.0f, Difficulty) * MaxHuntingGroundsProbability; HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); } IsBeaconActive = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index ec1d9fbe4..02140f9a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -235,7 +235,7 @@ namespace Barotrauma UseNetworkSyncing = element.GetAttributeBool("networksyncing", false); unrotatedForce = - element.Attribute("force") != null && element.Attribute("force").Value.Contains(',') ? + element.GetAttribute("force") != null && element.GetAttribute("force").Value.Contains(',') ? element.GetAttributeVector2("force", Vector2.Zero) : new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index b70413c68..4b77d08fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -89,39 +89,272 @@ namespace Barotrauma public int TurnsInRadiation { get; set; } #region Store + + public class StoreInfo + { + private int balance; + + public Identifier Identifier { get; } + public int Balance + { + get + { + return balance; + } + set + { + balance = value; + ActiveBalanceStatus = Location.GetStoreBalanceStatus(value); + } + } + public List Stock { get; } = new List(); + public List DailySpecials { get; } = new List(); + public List RequestedGoods { get; } = new List(); + /// + /// 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) + { + Location = location; + } + + /// + /// Create new StoreInfo + /// + public StoreInfo(Location location, Identifier identifier) : this(location) + { + Identifier = identifier; + Balance = location.StoreInitialBalance; + Stock = CreateStock(); + GenerateSpecials(); + GeneratePriceModifier(); + } + + /// + /// Load previously saved StoreInfo + /// + public StoreInfo(Location location, XElement storeElement) : this(location) + { + Identifier = storeElement.GetAttributeIdentifier("identifier", ""); + Balance = storeElement.GetAttributeInt("balance", location.StoreInitialBalance); + PriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); + // Backwards compatibility: before introducing support for multiple stores, this value was saved as a store element attribute + if (storeElement.Attribute("stepssincespecialsupdated") != null) + { + location.StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); + } + foreach (var stockElement in storeElement.GetChildElements("stock")) + { + var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty); + if (identifier.IsEmpty || !(ItemPrefab.FindByIdentifier(identifier) is ItemPrefab prefab)) { continue; } + int qty = stockElement.GetAttributeInt("qty", 0); + if (qty < 1) { continue; } + Stock.Add(new PurchasedItem(prefab, qty, buyer: null)); + } + if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement) + { + var loadedDailySpecials = LoadStoreSpecials(specialsElement); + DailySpecials.AddRange(loadedDailySpecials); + } + if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement) + { + var loadedRequestedGoods = LoadStoreSpecials(goodsElement); + RequestedGoods.AddRange(loadedRequestedGoods); + } + + static List LoadStoreSpecials(XElement element) + { + var specials = new List(); + foreach (var childElement in element.GetChildElements("item")) + { + var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); + if (id.IsEmpty || !(ItemPrefab.FindByIdentifier(id) is ItemPrefab prefab)) { continue; } + specials.Add(prefab); + } + return specials; + } + } + + public List CreateStock() + { + var stock = new List(); + foreach (var prefab in ItemPrefab.Prefabs) + { + if (!prefab.CanBeBoughtFrom(this, out var priceInfo)) { continue; } + int quantity = PriceInfo.DefaultAmount; + if (priceInfo.MaxAvailableAmount > 0) + { + if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) + { + quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); + } + else + { + quantity = priceInfo.MaxAvailableAmount; + } + } + else if (priceInfo.MinAvailableAmount > 0) + { + quantity = priceInfo.MinAvailableAmount; + } + stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); + } + return stock; + } + + public void AddStock(List items) + { + if (items == null || items.None()) { return; } + DebugConsole.NewMessage($"Adding items to stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true); + foreach (var item in items) + { + if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) + { + stockItem.Quantity += 1; + DebugConsole.NewMessage($"Added 1x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true); + } + else + { + DebugConsole.NewMessage($"{item.ItemPrefab.Name} not sold at location, can't add", Color.Cyan, debugOnly: true); + } + } + } + + public void RemoveStock(List items) + { + if (items == null || items.None()) { return; } + DebugConsole.NewMessage($"Removing items from stock for \"{Identifier}\" at \"{Location}\"", Color.Purple, debugOnly: true); + foreach (PurchasedItem item in items) + { + if (Stock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) + { + stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0); + DebugConsole.NewMessage($"Removed {item.Quantity}x {item.ItemPrefab.Name}, new total: {stockItem.Quantity}", Color.Cyan, debugOnly: true); + } + } + } + + public void GenerateSpecials() + { + var availableStock = new Dictionary(); + foreach (var stockItem in Stock) + { + if (stockItem.Quantity < 1) { continue; } + float weight = 1.0f; + if (stockItem.ItemPrefab.GetPriceInfo(this) is PriceInfo priceInfo) + { + if (!priceInfo.CanBeSpecial) { continue; } + var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount; + weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity; + if (weight < 0.0f) { continue; } + } + availableStock.Add(stockItem.ItemPrefab, weight); + } + DailySpecials.Clear(); + int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount(); + for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) + { + if (availableStock.None()) { break; } + var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); + if (item == null) { break; } + DailySpecials.Add(item); + availableStock.Remove(item); + } + RequestedGoods.Clear(); + for (int i = 0; i < RequestedGoodsCount; i++) + { + var item = ItemPrefab.Prefabs.GetRandom(p => + p.CanBeSold && !RequestedGoods.Contains(p) && + p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced); + if (item == null) { break; } + RequestedGoods.Add(item); + } + Location.StepsSinceSpecialsUpdated = 0; + } + + public void GeneratePriceModifier() + { + PriceModifier = Rand.Range(-Location.StorePriceModifierRange, Location.StorePriceModifierRange + 1); + } + + /// If null, item.GetPriceInfo() will be used to get it. + /// /// If false, the price won't be affected by + public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true) + { + priceInfo ??= item?.GetPriceInfo(this); + if (priceInfo == null) { return 0; } + float price = priceInfo.Price; + // Adjust by random price modifier + price = (100 + PriceModifier) / 100.0f * price; + price *= priceInfo.BuyingPriceMultiplier; + // Adjust by daily special status + if (considerDailySpecials && DailySpecials.Contains(item)) + { + 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 should never go below 1 mk + return Math.Max((int)price, 1); + } + + /// If null, item.GetPriceInfo() will be used to get it. + /// If false, the price won't be affected by + public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true) + { + priceInfo ??= item?.GetPriceInfo(this); + if (priceInfo == null) { return 0; } + 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 should never go below 1 mk + return Math.Max((int)price, 1); + } + + public override string ToString() + { + return Identifier.Value; + } + } + + public Dictionary Stores { get; set; } + private float StoreMaxReputationModifier => Type.StoreMaxReputationModifier; private float StoreSellPriceModifier => Type.StoreSellPriceModifier; private float DailySpecialPriceModifier => Type.DailySpecialPriceModifier; private float RequestGoodPriceModifier => Type.RequestGoodPriceModifier; public int StoreInitialBalance => Type.StoreInitialBalance; private int StorePriceModifierRange => Type.StorePriceModifierRange; - /// - /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. - /// - public int StorePriceModifier { get; private set; } - - public Color BalanceColor => ActiveStoreBalanceStatus.Color; - public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; } private List StoreBalanceStatuses => Type.StoreBalanceStatuses; - private int storeCurrentBalance; - public int StoreCurrentBalance - { - get - { - return storeCurrentBalance; - } - set - { - storeCurrentBalance = value; - ActiveStoreBalanceStatus = GetStoreBalanceStatus(value); - } - } - - public List StoreStock { get; set; } - public List DailySpecials { get; } = new List(); - public List RequestedGoods { get; } = new List(); - /// /// How many map progress steps it takes before the discounts should be updated. /// @@ -129,6 +362,7 @@ namespace Barotrauma private const int DailySpecialsCount = 3; private const int RequestedGoodsCount = 3; private int StepsSinceSpecialsUpdated { get; set; } + public HashSet StoreIdentifiers { get; } = new HashSet(); #endregion @@ -261,33 +495,18 @@ namespace Barotrauma Connections = new List(); } + /// + /// Create a location from save data + /// public Location(XElement element) { - Identifier locationType = element.GetAttributeIdentifier("type", ""); - Type = LocationType.Prefabs[locationType]; - bool typeNotFound = false; - if (Type == null) - { - //turn lairs into abandoned outposts - if (locationType == "lair") - { - Type ??= LocationType.Prefabs["Abandoned"]; - addInitialMissionsForType = Type; - } - if (Type == null) - { - DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead."); - Type ??= LocationType.Prefabs["None"] ?? LocationType.Prefabs.First(); - } - if (Type != null) - { - element.SetAttributeValue("type", Type.Identifier); - } - typeNotFound = true; - } + Identifier locationTypeId = element.GetAttributeIdentifier("type", ""); + bool typeNotFound = GetTypeOrFallback(locationTypeId, out LocationType type); + Type = type; - Identifier originalLocationType = element.GetAttributeIdentifier("originaltype", locationType); - OriginalType = LocationType.Prefabs[locationType]; + Identifier originalLocationTypeId = element.GetAttributeIdentifier("originaltype", locationTypeId); + GetTypeOrFallback(originalLocationTypeId, out LocationType originalType); + OriginalType = originalType; baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); @@ -297,6 +516,7 @@ namespace Barotrauma IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); + StepsSinceSpecialsUpdated = element.GetAttributeInt("stepssincespecialsupdated", 0); if (!typeNotFound) { @@ -340,7 +560,7 @@ namespace Barotrauma killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty()).ToHashSet(); - System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationType}\"!"); + System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!"); if (Type == null) { Type = LocationType.Prefabs.First(); @@ -350,8 +570,36 @@ namespace Barotrauma PortraitId = ToolBox.StringToInt(Name); - LoadStore(element); + LoadStores(element); LoadMissions(element); + + bool GetTypeOrFallback(Identifier identifier, out LocationType type) + { + if (!LocationType.Prefabs.TryGet(identifier, out type)) + { + //turn lairs into abandoned outposts + if (identifier == "lair") + { + LocationType.Prefabs.TryGet("Abandoned".ToIdentifier(), out type); + addInitialMissionsForType = Type; + } + if (type == null) + { + DebugConsole.AddWarning($"Could not find location type \"{identifier}\". Using location type \"None\" instead."); + LocationType.Prefabs.TryGet("None".ToIdentifier(), out type); + if (type == null) + { + type = LocationType.Prefabs.First(); + } + } + if (type != null) + { + element.SetAttributeValue("type", type.Identifier); + } + return false; + } + return true; + } } public void LoadLocationTypeChange(XElement locationElement) @@ -408,7 +656,6 @@ namespace Barotrauma } } - public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable existingLocations = null) { return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); @@ -427,7 +674,7 @@ namespace Barotrauma DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; - Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); if (Type.MissionIdentifiers.Any()) { @@ -438,7 +685,7 @@ namespace Barotrauma UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced()); } - CreateStore(force: true); + CreateStores(force: true); } public void UnlockInitialMissions() @@ -714,90 +961,50 @@ namespace Barotrauma return type.NameFormats[nameFormatIndex].Replace("[name]", baseName); } - public void LoadStore(XElement locationElement) + public void LoadStores(XElement locationElement) { - StoreStock?.Clear(); - DailySpecials.Clear(); - RequestedGoods.Clear(); - - if (locationElement.GetChildElement("store") is XElement storeElement) + UpdateStoreIdentifiers(); + Stores?.Clear(); + foreach (var storeElement in locationElement.GetChildElements("store")) { - StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance); - StorePriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); - - StoreStock ??= new List(); - foreach (XElement stockElement in storeElement.GetChildElements("stock")) + Stores ??= new Dictionary(); + var identifier = storeElement.GetAttributeIdentifier("identifier", ""); + if (identifier.IsEmpty) { - var id = stockElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var qty = stockElement.GetAttributeInt("qty", 0); - if (qty < 1) { continue; } - StoreStock.Add(new PurchasedItem(prefab, qty, buyer: null)); + // Previously saved store data (with no identifier) is discarded and new store data will be created + continue; } - - StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); - - if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement) + if (StoreIdentifiers.Contains(identifier)) { - var loadedDailySpecials = LoadStoreSpecials(specialsElement); - DailySpecials.AddRange(loadedDailySpecials); - } - - if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement) - { - var loadedRequestedGoods = LoadStoreSpecials(goodsElement); - RequestedGoods.AddRange(loadedRequestedGoods); - } - - static List LoadStoreSpecials(XElement element) - { - List specials = new List(); - foreach (var childElement in element.GetChildElements("item")) + if (!Stores.ContainsKey(identifier)) { - var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); - if (id.IsEmpty) { continue; } - var prefab = ItemPrefab.Find(null, id); - if (prefab == null) { continue; } - specials.Add(prefab); + Stores.Add(identifier, new StoreInfo(this, storeElement)); + } + else + { + string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier."; + DebugConsole.ThrowError(msg); + GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg); + continue; } - return specials; } + else + { + string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier."; + DebugConsole.ThrowError(msg); + GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg); + continue; + } + } + // Backwards compatibility: create new stores for any identifiers not present in the save data + foreach (var id in StoreIdentifiers) + { + AddNewStore(id); } } public bool IsRadiated() => GameMain.GameSession?.Map?.Radiation != null && GameMain.GameSession.Map.Radiation.Enabled && GameMain.GameSession.Map.Radiation.Contains(this); - private List CreateStoreStock() - { - var stock = new List(); - foreach (ItemPrefab prefab in ItemPrefab.Prefabs) - { - if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo)) - { - int quantity = PriceInfo.DefaultAmount; - if (priceInfo.MaxAvailableAmount > 0) - { - if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) - { - quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount + 1); - } - else - { - quantity = priceInfo.MaxAvailableAmount; - } - } - else if (priceInfo.MinAvailableAmount > 0) - { - quantity = priceInfo.MinAvailableAmount; - } - stock.Add(new PurchasedItem(prefab, quantity, buyer: null)); - } - } - return stock; - } - /// /// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost /// @@ -836,73 +1043,6 @@ namespace Barotrauma } } - /// If null, item.GetPriceInfo() will be used to get it. - /// /// If false, the price won't be affected by - public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true) - { - priceInfo ??= item?.GetPriceInfo(this); - if (priceInfo == null) { return 0; } - float price = priceInfo.Price; - - // Adjust by random price modifier - price = ((100 + StorePriceModifier) / 100.0f) * price; - - price *= priceInfo.BuyingPriceMultiplier; - - // Adjust by daily special status - if (considerDailySpecials && DailySpecials.Contains(item)) - { - price = DailySpecialPriceModifier * price; - } - - // Adjust by current location reputation - if (Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price; - } - - // Price should never go below 1 mk - return Math.Max((int)price, 1); - } - - /// If null, item.GetPriceInfo() will be used to get it. - /// If false, the price won't be affected by - public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true) - { - priceInfo ??= item?.GetPriceInfo(this); - if (priceInfo == null) { return 0; } - float price = StoreSellPriceModifier * priceInfo.Price; - - // Adjust by random price modifier - price = ((100 - StorePriceModifier) / 100.0f) * price; - - // Adjust by current store balance - price = ActiveStoreBalanceStatus.SellPriceModifier * price; - - // Adjust by requested good status - if (considerRequestedGoods && RequestedGoods.Contains(item)) - { - price = RequestGoodPriceModifier * price; - } - - // Adjust by current location reputation - if (Reputation.Value > 0.0f) - { - price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price; - } - else - { - price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price; - } - - // Price should never go below 1 mk - return Math.Max((int)price, 1); - } - public int GetAdjustedMechanicalCost(int cost) { float discount = Reputation.Value / Reputation.MaxReputation * (MechanicalMaxDiscountPercentage / 100.0f); @@ -915,187 +1055,182 @@ namespace Barotrauma return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); } - /// If true, the store will be recreated if it already exists. - public void CreateStore(bool force = false) + public StoreInfo GetStore(Identifier identifier) + { + if (Stores != null && Stores.TryGetValue(identifier, out var store)) + { + return store; + } + return null; + } + + /// If true, the stores will be recreated if they already exists. + public void CreateStores(bool force = false) { // In multiplayer, stores should be created by the server and loaded from save data by clients if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - - if (!force && StoreStock != null) { return; } - - if (StoreStock != null) + if (!force && Stores != null) { return; } + UpdateStoreIdentifiers(); + if (Stores != null) { - StoreCurrentBalance = Math.Max(StoreCurrentBalance, StoreInitialBalance); - var newStock = CreateStoreStock(); - foreach (PurchasedItem oldStockItem in StoreStock) + // Remove any stores with no corresponding merchants at the location + foreach (var storeIdentifier in Stores.Keys) { - if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is PurchasedItem newStockItem) + if (!StoreIdentifiers.Contains(storeIdentifier)) { - if (oldStockItem.Quantity > newStockItem.Quantity) - { - newStockItem.Quantity = oldStockItem.Quantity; - } + Stores.Remove(storeIdentifier); } } - StoreStock = newStock; - } - else - { - StoreCurrentBalance = StoreInitialBalance; - StoreStock = CreateStoreStock(); - } - - GenerateRandomPriceModifier(); - CreateStoreSpecials(); - } - - public void UpdateStore() - { - // In multiplayer, stores should be updated by the server and loaded from save data by clients - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - - if (StoreStock == null) - { - CreateStore(); - return; - } - - if (StoreCurrentBalance < StoreInitialBalance) - { - StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance); - } - - GenerateRandomPriceModifier(); - - var stock = StoreStock; - var stockToRemove = new List(); - foreach (PurchasedItem item in stock) - { - if (item.ItemPrefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo)) + foreach (var identifier in StoreIdentifiers) { - item.Quantity += 1; - if (priceInfo.MaxAvailableAmount > 0) + if (Stores.TryGetValue(identifier, out var store)) { - item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount); + store.Balance = Math.Max(store.Balance, StoreInitialBalance); + var newStock = store.CreateStock(); + foreach (var oldStockItem in store.Stock) + { + if (newStock.Find(i => i.ItemPrefab == oldStockItem.ItemPrefab) is { } newStockItem) + { + if (oldStockItem.Quantity > newStockItem.Quantity) + { + newStockItem.Quantity = oldStockItem.Quantity; + } + } + } + store.Stock.Clear(); + store.Stock.AddRange(newStock); + store.GenerateSpecials(); + store.GeneratePriceModifier(); } else { - item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity); + AddNewStore(identifier); } } - else - { - stockToRemove.Add(item); - } } - stockToRemove.ForEach(i => stock.Remove(i)); - StoreStock = stock; - - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || - DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) + else { - CreateStoreSpecials(); + foreach (var identifier in StoreIdentifiers) + { + AddNewStore(identifier); + } } } - private int GetExtraSpecialSalesCount() + public void UpdateStores() + { + // In multiplayer, stores should be updated by the server and loaded from save data by clients + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (Stores == null) + { + CreateStores(); + return; + } + var storesToRemove = new HashSet(); + foreach (var store in Stores.Values) + { + if (!StoreIdentifiers.Contains(store.Identifier)) + { + storesToRemove.Add(store.Identifier); + continue; + } + if (store.Balance < StoreInitialBalance) + { + store.Balance = Math.Min(store.Balance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance); + } + var stock = store.Stock; + var stockToRemove = new List(); + foreach (var item in stock) + { + if (item.ItemPrefab.CanBeBoughtFrom(store, out PriceInfo priceInfo)) + { + item.Quantity += 1; + if (priceInfo.MaxAvailableAmount > 0) + { + item.Quantity = Math.Min(item.Quantity, priceInfo.MaxAvailableAmount); + } + else + { + item.Quantity = Math.Min(item.Quantity, CargoManager.MaxQuantity); + } + } + else + { + stockToRemove.Add(item); + } + } + stockToRemove.ForEach(i => stock.Remove(i)); + store.Stock.Clear(); + store.Stock.AddRange(stock); + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || store.DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) + { + store.GenerateSpecials(); + } + store.GeneratePriceModifier(); + } + foreach (var identifier in storesToRemove) + { + Stores.Remove(identifier); + } + foreach (var identifier in StoreIdentifiers) + { + AddNewStore(identifier); + } + } + + private void UpdateStoreIdentifiers() + { + StoreIdentifiers.Clear(); + foreach (var outpostParam in OutpostGenerationParams.OutpostParams) + { + if (!outpostParam.AllowedLocationTypes.Contains(Type.Identifier)) { continue; } + foreach (var identifier in outpostParam.GetStoreIdentifiers()) + { + StoreIdentifiers.Add(identifier); + } + } + } + + private void AddNewStore(Identifier identifier) + { + Stores ??= new Dictionary(); + if (Stores.ContainsKey(identifier)) { return; } + var newStore = new StoreInfo(this, identifier); + Stores.Add(identifier, newStore); + } + + public void AddStock(Dictionary> items) + { + if (items == null) { return; } + foreach (var storeItems in items) + { + if (GetStore(storeItems.Key) is { } store) + { + store.AddStock(storeItems.Value); + } + } + } + + public void RemoveStock(Dictionary> items) + { + if (items == null) { return; } + foreach (var storeItems in items) + { + if (GetStore(storeItems.Key) is { } store) + { + store.RemoveStock(storeItems.Value); + } + } + } + + public int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(); if (!characters.Any()) { return 0; } return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } - private void GenerateRandomPriceModifier() - { - StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange + 1); - } - - private void CreateStoreSpecials() - { - DailySpecials.Clear(); - var availableStock = new Dictionary(); - foreach (var stockItem in StoreStock) - { - if (stockItem.Quantity < 1) { continue; } - var weight = 1.0f; - var priceInfo = stockItem.ItemPrefab.GetPriceInfo(this); - if (priceInfo != null) - { - if (!priceInfo.CanBeSpecial) { continue; } - var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount; - weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity; - if (weight < 0.0f) { continue; } - } - availableStock.Add(stockItem.ItemPrefab, weight); - } - - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) - { - if (availableStock.None()) { break; } - var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); - if (item == null) { break; } - DailySpecials.Add(item); - availableStock.Remove(item); - } - - RequestedGoods.Clear(); - for (int i = 0; i < RequestedGoodsCount; i++) - { - var item = ItemPrefab.Prefabs.GetRandom(p => - p.CanBeSold && !RequestedGoods.Contains(p) && - p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial, Rand.RandSync.Unsynced); - if (item == null) { break; } - RequestedGoods.Add(item); - } - - StepsSinceSpecialsUpdated = 0; - } - - public void AddToStock(List items) - { - if (StoreStock == null || items == null) { return; } -#if DEBUG - if (items.Any()) { DebugConsole.NewMessage("Adding items to stock at " + Name, Color.Purple); } -#endif - foreach (SoldItem item in items) - { - if (StoreStock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) - { - stockItem.Quantity += 1; -#if DEBUG - DebugConsole.NewMessage("Added 1x " + item.ItemPrefab.Name + ", new total: " + stockItem.Quantity, Color.Cyan); -#endif - } -#if DEBUG - else - { - DebugConsole.NewMessage(item.ItemPrefab.Name + " not sold at location, can't add", Color.Cyan); - } -#endif - } - } - - public void RemoveFromStock(List items) - { - if (StoreStock == null || items == null) { return; } -#if DEBUG - if (items.Any()) { DebugConsole.NewMessage("Removing items from stock at " + Name, Color.Purple); } -#endif - foreach (PurchasedItem item in items) - { - if (StoreStock.FirstOrDefault(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem stockItem) - { - stockItem.Quantity = Math.Max(stockItem.Quantity - item.Quantity, 0); -#if DEBUG - DebugConsole.NewMessage("Removed " + item.Quantity + "x " + item.ItemPrefab.Name + ", new total: " + stockItem.Quantity, Color.Cyan); -#endif - } - } - } - public StoreBalanceStatus GetStoreBalanceStatus(int balance) { StoreBalanceStatus nextStatus = StoreBalanceStatuses[0]; @@ -1128,7 +1263,7 @@ namespace Barotrauma ChangeType(OriginalType); PendingLocationTypeChange = null; } - CreateStore(force: true); + CreateStores(force: true); ClearMissions(); LevelData?.EventHistory?.Clear(); UnlockInitialMissions(); @@ -1148,7 +1283,8 @@ namespace Barotrauma new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier), new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), - new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation)); + new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation), + new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); LevelData.Save(locationElement); for (int i = 0; i < Type.CanChangeTo.Count; i++) @@ -1201,44 +1337,43 @@ namespace Barotrauma locationElement.Add(new XAttribute("killedcharacters", string.Join(',', killedCharacterIdentifiers))); } - if (StoreStock != null) + if (Stores != null) { - var storeElement = new XElement("store", - new XAttribute("balance", StoreCurrentBalance), - new XAttribute("pricemodifier", StorePriceModifier), - new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); - - foreach (PurchasedItem item in StoreStock) + foreach (var store in Stores.Values) { - if (item?.ItemPrefab == null) { continue; } - storeElement.Add(new XElement("stock", - new XAttribute("id", item.ItemPrefab.Identifier), - new XAttribute("qty", item.Quantity))); - } - - if (DailySpecials.Any()) - { - var dailySpecialElement = new XElement("dailyspecials"); - foreach (var item in DailySpecials) + var storeElement = new XElement("store", + new XAttribute("identifier", store.Identifier.Value), + new XAttribute("balance", store.Balance), + new XAttribute("pricemodifier", store.PriceModifier)); + foreach (PurchasedItem item in store.Stock) { - dailySpecialElement.Add(new XElement("item", - new XAttribute("id", item.Identifier))); + if (item?.ItemPrefab == null) { continue; } + storeElement.Add(new XElement("stock", + new XAttribute("id", item.ItemPrefab.Identifier), + new XAttribute("qty", item.Quantity))); } - storeElement.Add(dailySpecialElement); - } - - if (RequestedGoods.Any()) - { - var requestedGoodsElement = new XElement("requestedgoods"); - foreach (var item in RequestedGoods) + if (store.DailySpecials.Any()) { - requestedGoodsElement.Add(new XElement("item", - new XAttribute("id", item.Identifier))); + var dailySpecialElement = new XElement("dailyspecials"); + foreach (var item in store.DailySpecials) + { + dailySpecialElement.Add(new XElement("item", + new XAttribute("id", item.Identifier))); + } + storeElement.Add(dailySpecialElement); } - storeElement.Add(requestedGoodsElement); + if (store.RequestedGoods.Any()) + { + var requestedGoodsElement = new XElement("requestedgoods"); + foreach (var item in store.RequestedGoods) + { + requestedGoodsElement.Add(new XElement("item", + new XAttribute("id", item.Identifier))); + } + storeElement.Add(requestedGoodsElement); + } + locationElement.Add(storeElement); } - - locationElement.Add(storeElement); } if (AvailableMissions is List missions && missions.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index f7b648463..0a793e304 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -182,8 +182,7 @@ namespace Barotrauma { for (int i = 0; i < Connections.Count; i++) { - float maxHuntingGroundsProbability = 0.3f; - Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * maxHuntingGroundsProbability; + Connections[i].LevelData.HasHuntingGrounds = Rand.Range(0.0f, 1.0f) < Connections[i].Difficulty / 100.0f * LevelData.MaxHuntingGroundsProbability; connectionElements[i].SetAttributeValue("hashuntinggrounds", true); } } @@ -232,7 +231,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); CurrentLocation.Discover(true); - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); InitProjectSpecific(); } @@ -680,7 +679,7 @@ namespace Barotrauma CurrentLocation.Discover(); SelectedLocation = null; - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } }) @@ -719,7 +718,7 @@ namespace Barotrauma } } - CurrentLocation.CreateStore(); + CurrentLocation.CreateStores(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); } @@ -857,16 +856,14 @@ namespace Barotrauma if (location == CurrentLocation || location == SelectedLocation || location.IsGateBetweenBiomes) { continue; } - ProgressLocationTypeChanges(location); - - if (location.Discovered) + if (!ProgressLocationTypeChanges(location) && location.Discovered) { - location.UpdateStore(); + location.UpdateStores(); } } } - private void ProgressLocationTypeChanges(Location location) + private bool ProgressLocationTypeChanges(Location location) { location.TimeSinceLastTypeChange++; location.LocationTypeChangeCooldown--; @@ -886,9 +883,8 @@ namespace Barotrauma location.PendingLocationTypeChange.Value.parentMission); if (location.PendingLocationTypeChange.Value.delay <= 0) { - ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); + return ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); } - return; } } @@ -920,9 +916,9 @@ namespace Barotrauma } else { - ChangeLocationType(location, selectedTypeChange); + return ChangeLocationType(location, selectedTypeChange); } - return; + return false; } } @@ -941,6 +937,8 @@ namespace Barotrauma } } } + + return false; } public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) @@ -988,7 +986,7 @@ namespace Barotrauma return distance; } - private void ChangeLocationType(Location location, LocationTypeChange change) + private bool ChangeLocationType(Location location, LocationTypeChange change) { string prevName = location.Name; @@ -996,7 +994,7 @@ namespace Barotrauma if (newType == null) { DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found."); - return; + return false; } if (newType.OutpostTeam != location.Type.OutpostTeam || @@ -1013,6 +1011,7 @@ namespace Barotrauma location.TimeSinceLastTypeChange = 0; location.LocationTypeChangeCooldown = change.CooldownAfterChange; location.PendingLocationTypeChange = null; + return true; } partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); @@ -1091,7 +1090,7 @@ namespace Barotrauma } } - location.LoadStore(subElement); + location.LoadStores(subElement); location.LoadMissions(subElement); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index ba0c54911..9fdc1e786 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -59,8 +59,19 @@ namespace Barotrauma } public static readonly Md5Hash Blank = new Md5Hash(new string('0', 32)); - - private static readonly Regex removeWhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static string RemoveWhitespace(string s) + { + StringBuilder sb = new StringBuilder(s.Length / 2); // Reserve half the size of the original string because + // that's probably close enough to the size of the result + for (int i = 0; i < s.Length; i++) + { + if (char.IsWhiteSpace(s[i])) { continue; } + sb.Append(s[i]); + } + return sb.ToString(); + } + //thanks to Jlobblet for this regex private static readonly Regex stringHashRegex = new Regex(@"^[0-9a-fA-F]{7,32}$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); @@ -175,7 +186,7 @@ namespace Barotrauma } if (options.HasFlag(StringHashOptions.IgnoreWhitespace)) { - str = removeWhitespaceRegex.Replace(str, ""); + str = RemoveWhitespace(str); } byte[] bytes = Encoding.UTF8.GetBytes(str); return CalculateForBytes(bytes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 3ceac779c..6d75e6916 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -156,6 +156,8 @@ namespace Barotrauma public Dictionary SerializableProperties { get; private set; } + private ImmutableHashSet StoreIdentifiers { get; set; } + #warning TODO: this shouldn't really accept any ContentFile, issue is that RuinConfigFile and OutpostConfigFile are separate derived classes public OutpostGenerationParams(ContentXElement element, ContentFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { @@ -230,6 +232,26 @@ namespace Barotrauma return humanPrefabCollections.GetRandom(randSync); } + public ImmutableHashSet GetStoreIdentifiers() + { + if (StoreIdentifiers == null) + { + var storeIdentifiers = new HashSet(); + foreach (var collection in humanPrefabCollections) + { + foreach (var prefab in collection) + { + if (prefab?.CampaignInteractionType == CampaignMode.InteractionType.Store) + { + storeIdentifiers.Add(prefab.Identifier); + } + } + } + StoreIdentifiers = storeIdentifiers.ToImmutableHashSet(); + } + return StoreIdentifiers; + } + public override void Dispose() { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 8a92048f3..336462b62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -57,17 +57,17 @@ namespace Barotrauma } } - public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false) + public static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, bool onlyEntrance = false, bool allowInvalidOutpost = false) { - return Generate(generationParams, locationType, location: null, onlyEntrance); + return Generate(generationParams, locationType, location: null, onlyEntrance, allowInvalidOutpost); } - public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false) + public static Submarine Generate(OutpostGenerationParams generationParams, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) { - return Generate(generationParams, location.Type, location, onlyEntrance); + return Generate(generationParams, location.Type, location, onlyEntrance, allowInvalidOutpost); } - private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false) + private static Submarine Generate(OutpostGenerationParams generationParams, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false) { var outpostModuleFiles = ContentPackageManager.EnabledPackages.All .SelectMany(p => p.GetFiles()) @@ -197,12 +197,19 @@ namespace Barotrauma AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); if (pendingModuleFlags.Any(flag => flag != "none")) { - remainingTries--; - if (remainingTries <= 0) + if (!allowInvalidOutpost) { - DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough doors at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); + remainingTries--; + if (remainingTries <= 0) + { + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags)); + } + continue; + } + else + { + DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed."); } - continue; } var outpostInfo = new SubmarineInfo() @@ -328,7 +335,10 @@ namespace Barotrauma selectedModule.Offset = (selectedModule.PreviousGap.WorldPosition + selectedModule.PreviousModule.Offset) - selectedModule.ThisGap.WorldPosition; - selectedModule.Offset += moveDir * generationParams.MinHallwayLength; + if (selectedModule.PreviousGap.ConnectedDoor != null || selectedModule.ThisGap.ConnectedDoor != null) + { + selectedModule.Offset += moveDir * generationParams.MinHallwayLength; + } } entities[selectedModule] = moduleEntities; } @@ -464,6 +474,16 @@ namespace Barotrauma pendingModuleFlags.Remove(initialModuleFlag); pendingModuleFlags.Insert(0, initialModuleFlag); + if (pendingModuleFlags.Count > totalModuleCount) + { + DebugConsole.ThrowError($"Error during outpost generation. {pendingModuleFlags.Count} modules set to be used the outpost, but total module count is only {totalModuleCount}. Leaving out some of the modules..."); + int removeCount = pendingModuleFlags.Count - totalModuleCount; + for (int i = 0; i < removeCount; i++) + { + pendingModuleFlags.Remove(pendingModuleFlags.Last()); + } + } + return pendingModuleFlags; } @@ -486,46 +506,71 @@ namespace Barotrauma if (pendingModuleFlags.Count == 0) { return true; } List placedModules = new List(); - foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) + for (int i = 0; i < 2; i++) { - if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - if (!allowExtendBelowInitialModule) + //try placing a module meant for this location type first, and if that fails, try choosing whatever fits + bool allowDifferentLocationType = i > 0; + foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient)) { - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } - } - if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) - { - var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType); - if (newModule != null) { placedModules.Add(newModule); } - if (pendingModuleFlags.Count == 0) { return true; } + if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + } + + PlacedModule newModule = null; + //try appending to the current module if possible + if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) + { + newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + } + + if (newModule != null) + { + placedModules.Add(newModule); + } + else + { + //couldn't append to current module, try one of the other placed modules + foreach (PlacedModule otherModule in selectedModules) + { + if (otherModule == currentModule) { continue; } + foreach (OutpostModuleInfo.GapPosition otherGapPosition in + GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g))) + { + newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType); + if (newModule != null) + { + placedModules.Add(newModule); + break; + } + } + if (newModule != null) { break; } + } + } + if (pendingModuleFlags.Count == 0) { return true; } } } - //couldn't place anything, retry - if (placedModules.Count == 0 && retry && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule.PreviousModule)) + //couldn't place a module anywhere, we're probably fucked! + if (placedModules.Count == 0 && retry && currentModule.PreviousModule != null && !selectedModules.Any(m => m != currentModule && m.PreviousModule == currentModule)) { - //try to append to some other module first - foreach (PlacedModule otherModule in selectedModules) - { - if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) - { - return true; - } - } //try to replace the previously placed module with something else that we can append to - var failedModule = currentModule; for (int i = 0; i < 10; i++) { selectedModules.Remove(currentModule); + assertAllPreviousModulesPresent(); //readd the module types that the previous module was supposed to fulfill to the pending module types pendingModuleFlags.AddRange(currentModule.FulfilledModuleTypes); if (!availableModules.Contains(currentModule.Info)) { availableModules.Add(currentModule.Info); } //retry - currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType); + currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType: true); + assertAllPreviousModulesPresent(); if (currentModule == null) { break; } if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { + assertAllPreviousModulesPresent(); return true; } } @@ -534,9 +579,14 @@ namespace Barotrauma foreach (PlacedModule placedModule in placedModules) { - AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType); + AppendToModule(placedModule, availableModules, pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: allowExtendBelowInitialModule); } return placedModules.Count > 0; + + void assertAllPreviousModulesPresent() + { + System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule))); + } } /// @@ -553,7 +603,8 @@ namespace Barotrauma List availableModules, List pendingModuleFlags, List selectedModules, - LocationType locationType) + LocationType locationType, + bool allowDifferentLocationType) { if (pendingModuleFlags.Count == 0) { return null; } @@ -562,7 +613,7 @@ namespace Barotrauma foreach (Identifier moduleFlag in pendingModuleFlags) { flagToPlace = moduleFlag; - nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType); + nextModule = GetRandomModule(currentModule?.Info?.OutpostModuleInfo, availableModules, flagToPlace, gapPosition, locationType, allowDifferentLocationType); if (nextModule != null) { break; } } @@ -603,6 +654,7 @@ namespace Barotrauma foreach (PlacedModule otherModule in modules2) { if (module == otherModule) { continue; } + if (module.PreviousModule == otherModule && module.PreviousGap.ConnectedDoor == null && module.ThisGap.ConnectedDoor == null) { continue; } if (ModulesOverlap(module, otherModule)) { module1 = module; @@ -775,22 +827,25 @@ namespace Barotrauma } } - private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType) + private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType) { IEnumerable availableModules = null; if (moduleFlag.IsEmpty || moduleFlag.Equals("none")) { availableModules = modules - .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier()))); } else { availableModules = modules - .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag) && m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)); + .Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); } + + availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition)); + if (prevModule != null) { - availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule) && CanAttachTo(prevModule, m.OutpostModuleInfo)); + availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo)); } if (availableModules.Count() == 0) { return null; } @@ -800,15 +855,22 @@ namespace Barotrauma availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier)); //if not found, search for modules suitable for any location type - if (!modulesSuitableForLocationType.Any()) + if (allowDifferentLocationType && !modulesSuitableForLocationType.Any()) { modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); } if (!modulesSuitableForLocationType.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); + if (allowDifferentLocationType) + { + 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); + } + else + { + return null; + } } else { @@ -1039,16 +1101,20 @@ namespace Barotrauma if (hallwayLength <= 1.0f) { continue; } - var suitableModules = availableModules.Where(m => - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && - m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); - if (suitableModules.Count() == 0) + Identifier moduleFlag = (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(); + var hallwayModules = availableModules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); + + var suitableHallwayModules = hallwayModules.Where(m => + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.Info.OutpostModuleInfo.ModuleFlags.Contains(s)) && + m.OutpostModuleInfo.AllowAttachToModules.Any(s => module.PreviousModule.Info.OutpostModuleInfo.ModuleFlags.Contains(s))); + if (suitableHallwayModules.Count() == 0) { - suitableModules = availableModules.Where(m => + suitableHallwayModules = hallwayModules.Where(m => !m.OutpostModuleInfo.AllowAttachToModules.Any() || m.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any")); } - var hallwayInfo = GetRandomModule(suitableModules, (isHorizontal ? "hallwayhorizontal" : "hallwayvertical").ToIdentifier(), locationType); + + var hallwayInfo = GetRandomModule(suitableHallwayModules, moduleFlag, locationType); if (hallwayInfo == null) { DebugConsole.ThrowError($"Generating hallways between outpost modules failed. No {(isHorizontal ? "horizontal" : "vertical")} hallway modules suitable for use between the modules \"{module.Info.DisplayName}\" and \"{module.PreviousModule.Info.DisplayName}\"."); @@ -1170,7 +1236,7 @@ namespace Barotrauma var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.ThisGap); if (startWaypoint == null) { - DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); + DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {module.ThisGapPosition.ToString().ToLower()} gap of the module \"{module.Info.Name}\"."); continue; } var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == module.PreviousGap); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index c79a10a64..794a9671b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -45,6 +45,9 @@ namespace Barotrauma [Serialize(GapPosition.None, IsPropertySaveable.Yes, description: "Which sides of the module have gaps on them (i.e. from which sides the module can be attached to other modules). Center = no gaps available.")] public GapPosition GapPositions { get; set; } + [Serialize(GapPosition.Right | GapPosition.Left | GapPosition.Bottom | GapPosition.Top, IsPropertySaveable.Yes, description: "Which sides of this module are allowed to attach to the previously placed module. E.g. if you want a module to always attach to the left side of the docking module, you could set this to Right.")] + public GapPosition CanAttachToPrevious { get; set; } + public string Name { get; private set; } public Dictionary SerializableProperties { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 0037d0a9f..7e9901866 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -6,30 +6,31 @@ namespace Barotrauma { class PriceInfo { - public readonly int Price; - public readonly bool CanBeBought; + public int Price { get; } + public bool CanBeBought { get; } //minimum number of items available at a given store - public readonly int MinAvailableAmount; + public int MinAvailableAmount { get; } //maximum number of items available at a given store - public readonly int MaxAvailableAmount; + public int MaxAvailableAmount { get; } + /// + /// Can the item be a Daily Special or a Requested Good + /// + public bool CanBeSpecial { get; } + /// + /// The item isn't available in stores unless the level's difficulty is above this value + /// + public int MinLevelDifficulty { get; } + /// + /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. + /// + public float BuyingPriceMultiplier { get; } = 1f; + public bool DisplayNonEmpty { get; } = false; + public Identifier StoreIdentifier { get; } + /// /// Used when both and are set to 0. /// public const int DefaultAmount = 5; - /// - /// Can the item be a Daily Special or a Requested Good - /// - public readonly bool CanBeSpecial; - /// - /// The item isn't available in stores unless the level's difficulty is above this value - /// - public readonly int MinLevelDifficulty; - /// - /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. - /// - public readonly float BuyingPriceMultiplier = 1f; - public bool DisplayNonEmpty { get; } = false; - /// /// Support for the old style of determining item prices @@ -42,14 +43,16 @@ namespace Barotrauma MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); CanBeBought = true; - var minAmount = GetMinAmount(element); + int minAmount = GetMinAmount(element); MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); - var maxAmount = GetMaxAmount(element); + int maxAmount = GetMaxAmount(element); maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, bool displayNonEmpty = false) + public PriceInfo(int price, bool canBeBought, + int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f, + bool displayNonEmpty = false, string storeIdentifier = null) { Price = price; CanBeBought = canBeBought; @@ -60,48 +63,67 @@ namespace Barotrauma MinLevelDifficulty = minLevelDifficulty; CanBeSpecial = canBeSpecial; DisplayNonEmpty = displayNonEmpty; + StoreIdentifier = new Identifier(storeIdentifier); } - public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) + public static List CreatePriceInfos(XElement element, out PriceInfo defaultPrice) { + var priceInfos = new List(); defaultPrice = null; int basePrice = element.GetAttributeInt("baseprice", 0); - bool soldByDefault = element.GetAttributeBool("soldbydefault", true); int minAmount = GetMinAmount(element); int maxAmount = GetMaxAmount(element); int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); bool canBeSpecial = element.GetAttributeBool("canbespecial", true); float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false); - var priceInfos = new List>(); - + bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true)); foreach (XElement childElement in element.GetChildElements("price")) { float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f); - bool sold = childElement.GetAttributeBool("sold", soldByDefault); - priceInfos.Add(new Tuple(childElement.GetAttributeIdentifier("locationtype", ""), - new PriceInfo((int)(priceMultiplier * basePrice), sold, + bool sold = childElement.GetAttributeBool("sold", soldByDefault); + int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty); + float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier); + string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", ""); + if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier)) + { + backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}"; + } + string[] storeIdentifiers = childElement.GetAttributeStringArray("storeidentifiers", new string[1] { backwardsCompatibleIdentifier }); + foreach (string id in storeIdentifiers) + { + if (string.IsNullOrEmpty(id)) { continue; } + // TODO: Add some error messages if we have defined the min or max amount while the item is not sold + var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), + sold, sold ? GetMinAmount(childElement, minAmount) : 0, sold ? GetMaxAmount(childElement, maxAmount) : 0, canBeSpecial, - childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), - childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier), - displayNonEmpty))); + storeMinLevelDifficulty, + storeBuyingMultiplier, + displayNonEmpty, + id); + priceInfos.Add(priceInfo); + } } - - bool canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); - defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations, - canBeBoughtAtOtherLocations ? minAmount : 0, - canBeBoughtAtOtherLocations ? maxAmount : 0, - canBeSpecial, minLevelDifficulty, buyingPriceMultiplier, displayNonEmpty); - + bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); + defaultPrice = new PriceInfo(basePrice, + soldElsewhere, + soldElsewhere ? minAmount : 0, + soldElsewhere ? maxAmount : 0, + canBeSpecial, + minLevelDifficulty, + buyingPriceMultiplier, + displayNonEmpty); return priceInfos; } private static int GetMinAmount(XElement element, int defaultValue = 0) => element != null ? - element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : defaultValue; + element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) : + defaultValue; private static int GetMaxAmount(XElement element, int defaultValue = 0) => element != null ? - element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : defaultValue; + element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) : + defaultValue; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index ae5e402d5..1fb8afbae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -48,11 +48,11 @@ namespace Barotrauma { if (!subs.Any()) yield return CoroutineStatus.Success; - Character.Controlled = null; - cam.TargetPos = Vector2.Zero; #if CLIENT + Character.Controlled = null; GameMain.LightManager.LosEnabled = false; #endif + cam.TargetPos = Vector2.Zero; Level.Loaded.TopBarrier.Enabled = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 7556b281a..d3901b682 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -1374,7 +1374,7 @@ namespace Barotrauma public static Structure Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { - string name = element.Attribute("name").Value; + string name = element.GetAttribute("name").Value; Identifier identifier = element.GetAttributeIdentifier("identifier", ""); StructurePrefab prefab = FindPrefab(name, identifier); @@ -1440,12 +1440,12 @@ namespace Barotrauma if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); } //structures with a body drop a shadow by default - if (element.Attribute("usedropshadow") == null) + if (element.GetAttribute("usedropshadow") == null) { s.UseDropShadow = prefab.Body; } - if (element.Attribute("noaitarget") == null) + if (element.GetAttribute("noaitarget") == null) { s.NoAITarget = prefab.NoAITarget; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 84bf74588..26c2f92e0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -162,7 +162,7 @@ namespace Barotrauma tags.Add(tag.Trim().ToIdentifier()); } - if (element.Attribute("ishorizontal") != null) + if (element.GetAttribute("ishorizontal") != null) { IsHorizontal = element.GetAttributeBool("ishorizontal", false); } @@ -177,8 +177,8 @@ namespace Barotrauma { case "sprite": Sprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && - subElement.Attribute("sheetindex") == null) + if (subElement.GetAttribute("sourcerect") == null && + subElement.GetAttribute("sheetindex") == null) { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + Name + "\"!"); } @@ -191,7 +191,7 @@ namespace Barotrauma CanSpriteFlipX = subElement.GetAttributeBool("canflipx", true); CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true); - if (subElement.Attribute("name") == null && !Name.IsNullOrWhiteSpace()) + if (subElement.GetAttribute("name") == null && !Name.IsNullOrWhiteSpace()) { Sprite.Name = Name.Value; } @@ -199,7 +199,7 @@ namespace Barotrauma break; case "backgroundsprite": BackgroundSprite = new Sprite(subElement, lazyLoad: true); - if (subElement.Attribute("sourcerect") == null && Sprite != null) + if (subElement.GetAttribute("sourcerect") == null && Sprite != null) { BackgroundSprite.SourceRect = Sprite.SourceRect; BackgroundSprite.size = Sprite.size; @@ -223,7 +223,7 @@ namespace Barotrauma int groupID = 0; DecorativeSprite decorativeSprite = null; - if (subElement.Attribute("texture") == null) + if (subElement.GetAttribute("texture") == null) { groupID = subElement.GetAttributeInt("randomgroupid", 0); } @@ -284,10 +284,10 @@ namespace Barotrauma } //backwards compatibility - if (element.Attribute("size") == null) + if (element.GetAttribute("size") == null) { Size = Vector2.Zero; - if (element.Attribute("width") == null && element.Attribute("height") == null) + if (element.GetAttribute("width") == null && element.GetAttribute("height") == null) { Size = Sprite.SourceRect.Size.ToVector2(); } @@ -322,7 +322,7 @@ namespace Barotrauma #endif Tags = tags.ToImmutableHashSet(); - AllowedLinks = Enumerable.Empty().ToImmutableHashSet(); + AllowedLinks = ImmutableHashSet.Empty; } protected override void CreateInstance(Rectangle rect) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index b205c4af5..ec08d2c38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -253,7 +253,7 @@ namespace Barotrauma get { return subBody == null ? Vector2.Zero : subBody.Velocity; } set { - if (subBody == null) return; + if (subBody == null) { return; } subBody.Velocity = value; } } @@ -969,8 +969,6 @@ namespace Barotrauma public void Update(float deltaTime) { - //if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX(); - if (Info.IsWreck) { WreckAI?.Update(deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index cc6881f34..8dde595c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -266,7 +266,7 @@ namespace Barotrauma GameVersion = original.GameVersion; Type = original.Type; SubmarineClass = original.SubmarineClass; - hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null; + hash = !string.IsNullOrEmpty(original.FilePath) && File.Exists(original.FilePath) ? original.MD5Hash : null; Dimensions = original.Dimensions; CargoCapacity = original.CargoCapacity; FilePath = original.FilePath; @@ -299,7 +299,7 @@ namespace Barotrauma DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms..."); Thread.Sleep(250); } - if (doc == null || doc.Root == null) + if (doc?.Root == null) { IsFileCorrupted = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 6b22e9acf..11b32da05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -987,8 +987,8 @@ namespace Barotrauma public static WayPoint Load(ContentXElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = new Rectangle( - int.Parse(element.Attribute("x").Value), - int.Parse(element.Attribute("y").Value), + int.Parse(element.GetAttribute("x").Value), + int.Parse(element.GetAttribute("y").Value), (int)Submarine.GridSize.X, (int)Submarine.GridSize.Y); @@ -1024,9 +1024,9 @@ namespace Barotrauma w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0)); int i = 0; - while (element.Attribute("linkedto" + i) != null) + while (element.GetAttribute("linkedto" + i) != null) { - int srcId = int.Parse(element.Attribute("linkedto" + i).Value); + int srcId = int.Parse(element.GetAttribute("linkedto" + i).Value); int destId = idRemap.GetOffsetId(srcId); if (destId > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 90f123c95..e405ecb4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking ERROR, //tell the server that an error occurred CREW, //hiring UI MEDICAL, //medical clinic - MONEY, //wallet updates + TRANSFER_MONEY, // wallet transfers REWARD_DISTRIBUTION, // wallet reward distribution READY_CHECK, READY_TO_SPAWN diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 00fe33585..f844f406a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Globalization; using Barotrauma.IO; @@ -975,18 +976,33 @@ namespace Barotrauma.Networking private void InitMonstersEnabled() { //monster spawn settings - MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + if (MonsterEnabled is null || MonsterEnabled.Count != CharacterPrefab.Prefabs.Count()) + { + MonsterEnabled = CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); + } } + private static IReadOnlyList ExtractAndSortKeys(IReadOnlyDictionary monsterEnabled) + => monsterEnabled.Keys + .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) + .ToImmutableArray(); + public void ReadMonsterEnabled(IReadMessage inc) { InitMonstersEnabled(); - List monsterNames = MonsterEnabled.Keys - .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) - .ToList(); - foreach (Identifier s in monsterNames) + var monsterNames = ExtractAndSortKeys(MonsterEnabled); + uint receivedMonsterCount = inc.ReadVariableUInt32(); + if (monsterNames.Count != receivedMonsterCount) { - MonsterEnabled[s] = inc.ReadBoolean(); + inc.BitPosition += (int)receivedMonsterCount; + DebugConsole.AddWarning($"Expected monster count {monsterNames.Count}, got {receivedMonsterCount}"); + } + else + { + foreach (Identifier s in monsterNames) + { + MonsterEnabled[s] = inc.ReadBoolean(); + } } inc.ReadPadBits(); } @@ -994,11 +1010,10 @@ namespace Barotrauma.Networking public void WriteMonsterEnabled(IWriteMessage msg, Dictionary monsterEnabled = null) { //monster spawn settings - if (monsterEnabled == null) { monsterEnabled = MonsterEnabled; } - - List monsterNames = monsterEnabled.Keys - .OrderBy(k => CharacterPrefab.Prefabs[k].UintIdentifier) - .ToList(); + InitMonstersEnabled(); + monsterEnabled ??= MonsterEnabled; + var monsterNames = ExtractAndSortKeys(monsterEnabled); + msg.WriteVariableUInt32((uint)monsterNames.Count); foreach (Identifier s in monsterNames) { msg.Write(monsterEnabled[s]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index dacc2c8a7..89c8bc1bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -172,11 +172,11 @@ namespace Barotrauma return !texName.IsNullOrEmpty() & !texName.Contains("/") && !texName.Contains("%ModDir", StringComparison.OrdinalIgnoreCase); } - public static ContentPath GetAttributeContentPath(this XElement element, string name, - ContentPackage contentPackage) + public static ContentPath GetAttributeContentPath(this XElement element, string name, ContentPackage contentPackage) { - if (element?.GetAttribute(name) == null) { return null; } - return ContentPath.FromRaw(contentPackage, GetAttributeString(element.GetAttribute(name), null)); + var attribute = element?.GetAttribute(name); + if (attribute == null) { return null; } + return ContentPath.FromRaw(contentPackage, GetAttributeString(attribute, null)); } public static Identifier GetAttributeIdentifier(this XElement element, string name, string defaultValue) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index b917b844a..551b48fdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -10,6 +10,7 @@ using System.Xml.Linq; using Barotrauma.IO; #if CLIENT using Barotrauma.ClientSource.Settings; +using Barotrauma.Networking; using Microsoft.Xna.Framework.Input; #endif @@ -453,8 +454,12 @@ namespace Barotrauma 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); + currentConfig = newConfig; -#warning TODO: Implement program state updates; #if CLIENT if (setGraphicsMode) @@ -462,6 +467,24 @@ namespace Barotrauma GameMain.Instance.ApplyGraphicsSettings(); } + if (audioOutputChanged) + { + GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice); + } + + if (voiceCaptureChanged) + { + VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice); + } + + if (textScaleChanged) + { + foreach (var font in GUIStyle.Fonts.Values) + { + font.Prefabs.ForEach(p => p.LoadFont()); + } + } + GameMain.SoundManager?.ApplySettings(); #endif if (languageChanged) { TextManager.ClearCache(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index bd86e6618..f7445abed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -72,6 +72,8 @@ namespace Barotrauma case "targetitemcomponent": case "targetself": case "targetcontainer": + case "targetgrandparent": + case "targetcontaineditem": return false; default: return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 688ebbfbb..b630c8b11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -68,15 +68,21 @@ namespace Barotrauma public bool IsTriggered { get; private set; } - public float Timer { get; private set; } = -1; + public float Timer { get; private set; } public bool IsActive { get; private set; } + public bool IsPermanent { get; private set; } + public void Launch() { IsTriggered = true; IsActive = true; - Timer = Duration; + IsPermanent = Duration <= 0; + if (!IsPermanent) + { + Timer = Duration; + } } public void Reset() @@ -88,6 +94,7 @@ namespace Barotrauma public void UpdateTimer(float deltaTime) { + if (IsPermanent) { return; } Timer -= deltaTime; if (Timer < 0) { @@ -410,7 +417,7 @@ namespace Barotrauma public static StatusEffect Load(ContentXElement element, string parentDebugName) { - if (element.Attribute("delay") != null || element.Attribute("delaytype") != null) + if (element.GetAttribute("delay") != null || element.GetAttribute("delaytype") != null) { return new DelayedEffect(element, parentDebugName); } @@ -642,7 +649,7 @@ namespace Barotrauma break; case "affliction": AfflictionPrefab afflictionPrefab; - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers instead of names."); string afflictionName = subElement.GetAttributeString("name", ""); @@ -670,7 +677,7 @@ namespace Barotrauma break; case "reduceaffliction": - if (subElement.Attribute("name") != null) + if (subElement.GetAttribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); ReduceAffliction.Add(( @@ -1243,24 +1250,27 @@ namespace Barotrauma { for (int i = 0; i < targets.Count; i++) { - if (targets[i] is Character character) + var target = targets[i]; + Limb targetLimb = target as Limb; + if (targetLimb == null && target is Character character) { foreach (Limb limb in character.AnimController.Limbs) { if (limb.body == sourceBody) { + targetLimb = limb; if (breakLimb) { character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); } - else - { - limb.HideAndDisable(hideLimbTimer); - } break; } } } + if (hideLimb) + { + targetLimb?.HideAndDisable(hideLimbTimer); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 382b21282..9684fbe89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -173,10 +173,13 @@ namespace Barotrauma.Steam private InstallTaskCounter(UInt64 id) { itemId = id; } public static bool IsInstalling(Steamworks.Ugc.Item item) + => IsInstalling(item.Id); + + public static bool IsInstalling(ulong itemId) { lock (mutex) { - return installers.Any(i => i.itemId == item.Id); + return installers.Any(i => i.itemId == itemId); } } @@ -193,9 +196,9 @@ namespace Barotrauma.Steam } } - public static async Task Create(Steamworks.Ugc.Item item) + public static async Task Create(ulong itemId) { - var retVal = new InstallTaskCounter(item.Id); + var retVal = new InstallTaskCounter(itemId); await retVal.Init(); return retVal; } @@ -260,19 +263,15 @@ namespace Barotrauma.Steam public static bool IsInstalling(Steamworks.Ugc.Item item) => InstallTaskCounter.IsInstalling(item); - + private static async Task InstallMod(ulong id) { - var item = await GetItem(id); - if (item is null) { return; } - await InstallMod(item.Value); - } - - private static async Task InstallMod(Steamworks.Ugc.Item item) - { - await Task.Yield(); - using var installCounter = await InstallTaskCounter.Create(item); + using var installCounter = await InstallTaskCounter.Create(id); + var itemNullable = await GetItem(id); + if (!(itemNullable is { } item)) { return; } + await Task.Yield(); + string itemTitle = item.Title.Trim(); UInt64 itemId = item.Id; string itemDirectory = item.Directory; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 7f6743010..30054648d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -8,6 +8,7 @@ using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; +using System.Globalization; namespace Barotrauma { @@ -349,6 +350,11 @@ namespace Barotrauma description += extraDescriptionLine; } + public static LocalizedString FormatCurrency(int amount) + { + return GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount)); + } + public static LocalizedString GetServerMessage(string serverMessage) { return new ServerMsgLString(serverMessage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index fefee9d61..40f135e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -2,19 +2,19 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace Barotrauma.IO { static class Validation { - private static readonly string[] unwritableDirs = new string[] { "Content" }; - private static readonly string[] unwritableExtensions = new string[] + private static readonly ImmutableArray unwritableDirs = new[] { "Content".ToIdentifier() }.ToImmutableArray(); + private static readonly ImmutableArray unwritableExtensions = new[] { - ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe and .dll handled separately in CanWrite) + ".pdb", ".com", ".scr", ".dylib", ".so", ".a", ".app", //executables and libraries (.exe, .dll and .json handled separately in CanWrite) ".bat", ".sh", //shell scripts - ".json" //deps.json - }; + }.ToIdentifiers().ToImmutableArray(); /// /// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds. @@ -24,25 +24,27 @@ namespace Barotrauma.IO public static bool CanWrite(string path, bool isDirectory) { path = System.IO.Path.GetFullPath(path).CleanUpPath(); + string localModsDir = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); + string workshopModsDir = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); if (!isDirectory) { - string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); - if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + Identifier extension = System.IO.Path.GetExtension(path).Replace(" ", "").ToIdentifier(); + if (unwritableExtensions.Any(e => e == extension)) { return false; } - if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) - && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) + if (!path.StartsWith(workshopModsDir, StringComparison.OrdinalIgnoreCase) + && !path.StartsWith(localModsDir, StringComparison.OrdinalIgnoreCase) + && (extension == ".dll" || extension == ".exe" || extension == ".json")) { return false; } } - foreach (string unwritableDir in unwritableDirs) + foreach (var unwritableDir in unwritableDirs) { - string dir = System.IO.Path.GetFullPath(unwritableDir).CleanUpPath(); + string dir = System.IO.Path.GetFullPath(unwritableDir.Value).CleanUpPath(); if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index f5663e4fd..f939ea059 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,104 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.4.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Split outposts stores into several different vendors who sell different types of items. +- Made DockingPort.ApplyEffectsOnDocking editable in the sub editor. +- Some UX improvements in the installed mods lists (unstable only). +- Items can be purchased from vending machines using the personal wallet (WIP). + +Fixes: +- Attempt to fix a crash in AIObjectiveExtinguishFire. +- Fixed bots not being able to repair leaks between rooms (leaks that are not in the outer walls). +- Fixed "deep sea slayer" always giving you a 50% buff to harpoons regardless if you're inside or not. +- Fixed text scale not being taken into account on scrolling text displays. + +Fixes (unstable only): +- Fixed Workshop items not getting uninstalled when they're removed through the Publish tab. +- Fixed mods being disabled when they're updated. +- Fixed the weird sitting animation for the new office chair. +- Fixed a race condition in Workshop item installer. +- Mod list filter works when coming back from looking at an installed mod's info. +- Fixed new submarines being disabled upon restart. +- Fixed stat types being displayed incorrectly in talent tooltips (e.g. "stattypenames.repairspeed" instead of "Repair Speed"). +- Fixed crashing when selecting a client who's not controlling a character in the tab menu. +- Fixed ContentPackageManager.Init failing if more than 100 mods are enabled. +- Fixed structures' "use drop shadow" setting being forced on in the sub editor. +- Fixed tutorials being in a random order in the main menu and some of the tutorials spawning the character with an incorrect job. +- Raised the file transfer size limit for mods from 50 MB to 500 MB. +- Fixed the new abyss monster's tongue sometimes sliding and deattaching from the sub. +- Fixed ServerSettings.MonsterEnabled not syncing correctly in a modded server. +- Fixed crashing when selecting something in the "allow attaching to" dropdown of a module's saving prompt. +- Fixed group names not showing up in the sub editor's "Send selection to group..." context menu. +- Fixed crashing when trying to load a save that contains locations not present in the current content package(s). +- Fixed crashing if a mod adds locations that don't have any name formats specified. +- Fixed crash when saving, immediately deleting, and resaving Unnamed.sub. +- Fixed ai target overrides not working, because only the definitions in the variant file were used. Fixes e.g. Mudraptor Veteran not targeting anything but Tigerthreshers. + +Modding: +- Fixed "targetcontaineditem" still not working correctly. +- Fixed crashing when trying to remove fog of war at the very edges of the campaign map. Doesn't affect the vanilla game because there's enough padding at the edges of the map. + +--------------------------------------------------------------------------------------------------------- +v0.17.3.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- A new abyss monster (placeholder art and sounds, name pending) +- Overhauled colonies: completely new modules, improved layouts, new structures and items, new events. +- Adjustments to reactors and supercapacitors to prevent the increased supercapacitor loads from crippling the subs on default recharge rates: slightly increased Humpback reactor output and decreased the default recharge rate of the capacitors, reduced recharge rates in the 3 new subs and set the supercapacitor efficiency to match the rest of the subs. +- Throw an error in debug and unstable builds if beacon station becomes inactive after it's been activated (hopefully helps us diagnose why beacon missions sometimes fail after the beacon's been activated). +- Reimplemented ServerExecutable to be usable in non-core packages. Now, players must select a server executable from a dropdown in the "Host server" menu if multiple are available. +- Animation adjustment: The head now rotates towards the mouse cursor while aiming or swimming. +- Swim animation adjustment: The body now rotates towards the aim target also when the character is moving, and not only while staying still. Moving while not facing the movement direction results in reduced movement speed. +- Set the bottom hole probability to 0 in Cold Caverns, which reduces the size and the frequency of holes in the level bottom. +- Adjusted the probabilities for spawning the Thalamus in the wrecks. +- Added a (WIP) difficulty hierarchy for the abyss monsters. Easier monsters should spawn more frequently on an easier difficulty level, the harder should spawn more frequently on higher difficulty levels. Currently the new abyss monster is defined as the easiest, and Endworm the hardest. Charybdis is in between. + +Fixes: +- Re-filled Typhon 2 oxygen tank shelves. +- Fixed spawnpoint editing panel being too small on large resolutions. +- Fixed inability to equip one-handed items when there's a suitable container in the other hand (e.g. flashlight when there's a storage container in the other hand). +- Cargo missions don't require the cargo to be inside a hull: being in the sub is enough. Fixes inability to complete cargo missions with unconventional subs where the cargo is stored outside hulls. +- Fixed non-equipped items that can't be put into a duffel bag disappearing when a character despawns. +- Fixed incorrect animation parameters being used for swimming while wearing a regular diving suit. +- Fixed projectiles sometimes staying attached to the target even when they are far from it. +- Fixed monsters sometimes trying to follow targets after losing the track of them even when they should be falling back from them (according to the after attack behavior). +- Fixed monsters sometimes using the after attack behavior of the current attack even when the cooldown of that attack is not active. +- Fixed monsters sometimes being unable to target the submarine, because their attack was incorrectly considered invalid. +- Fixed fractal guardians fleeing to a shelter immediatedly after taking some damage when they have targeted the guardian pod once and have not changed the target yet (e.g. if you shoot a guardian that is returning from the pod and if it has not yet spotted you). + +Fixes (unstable only): +- Fixed text scale slider not working. +- Fixed audio capture and output settings changes not being applied until the game was restarted. +- Fixed numerous circumstances where the Publish tab could cause a crash or softlock. +- Fixed creating and deleting item assemblies in the submarine editor. +- Fixed deleting submarines in the submarine editor. +- Trimmed down the filenames of mods transferred from the server to the clients. +- Fixed ballast flora's damage visualizations (particles, branches shaking, healthbar) not working in multiplayer (unstable only). +- Fixed occasional "invalid SetAttackTarget/ExecuteAttack" errors in multiplayer (unstable only). +- Fixed clients not taking control off the previous character properly when using freecam, preventing the character from moving if another player or AI takes control of it (unstable only). +- Fixed "event data was of the wrong type" error when characters spawn with items with depleted condition in their inventory (unstable only). +- Fixed incorrect power displayed on the reactor when unwired (unstable only). +- Fixed devices not powering down if they're disconnected from the grid when their voltage has been set above 0. Could be reproduced by powering up the sub in mp campaign, entering a new level and disconnecting e.g. the oxygen generator from the grid (unstable). +- Fixed hidden subs resetting client-side when restarting a server (unstable only). +- Randomize character appearance in server lobby if it hasn't been set (= when launching the game or v0.17 for the first time). Unstable only. + +Modding: +- ItemContainers apply the OnContaining effects even when the item is broken. Doesn't affect any vanilla items. +- Ropes attached to limbs now automatically snap when another attack is chosen. +- Ropes can now be set to break from the end instead of always breaking from the middle (see the new abyss monster for an example). +- Ropes can be set to break if they are in too steep angle to the target. +- Projectiles always stick permanently unless a stick duration is defined. +- Characters (with deformable sprites) can be set to be drawn after (on top of) other characters. Normally characters are drawn in the order of spawning. +- AI Triggers can now be permanent. +- Added a generic damage threshold that currently defines how much damage the character needs to take from a single hit to hit the avoiding and releasing captured targets. +- Added a support for multiple identifiers and types in the limb health definitions. +- Added a support for min range for ranged attacks. +- Fixed monsters not being able to shoot faster than every ~1.5 second if they change the attacking limb. +- Added new after attack behaviors: Reverse and ReverseUntilCanAttack. + --------------------------------------------------------------------------------------------------------- v0.17.2.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt index f29df7c55..a2c5918d6 100644 --- a/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt +++ b/Libraries/webm_mem_playback/LICENSE_webm_mem_playback.txt @@ -1,4 +1,4 @@ -Copyright (c) 2019 FakeFish Games +Copyright (c) 2019 FakeFish Ltd. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/README.md b/README.md index 99597222e..7943f78d2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,6 @@ If you're interested in working on the code, either to develop mods or to contri ### Windows - [Visual Studio](https://www.visualstudio.com/vs/community/) with C# 8.0 support (VS 2019 or later recommended) ### Linux -- [.NET Core 3.0 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) +- [.NET Core 3.1 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux-package-manager-ubuntu-1904) ### macOS - [Visual Studio 2019 for Mac](https://visualstudio.microsoft.com/vs/mac/) From 44ded0225a193452d9256a1b665ea0de5d3747d7 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Wed, 30 Mar 2022 01:20:59 +0900 Subject: [PATCH 7/9] Unstable 0.17.5.0 --- .../ClientSource/Characters/Character.cs | 10 +- .../Transition/UgcTransition.cs | 225 ++++++++++++ .../ClientSource/DebugConsole.cs | 52 --- .../ClientSource/GUI/CrewManagement.cs | 6 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 26 +- .../ClientSource/GUI/GUITextBox.cs | 5 +- .../ClientSource/GUI/Store.cs | 88 ++--- .../ClientSource/GUI/TabMenu.cs | 347 +++++++++++------- .../ClientSource/GUI/UpgradeStore.cs | 30 +- .../ClientSource/GUI/VotingInterface.cs | 160 +++++--- .../BarotraumaClient/ClientSource/GameMain.cs | 8 +- .../ClientSource/GameSession/CargoManager.cs | 2 + .../GameSession/GameModes/CampaignMode.cs | 24 +- .../GameModes/MultiPlayerCampaign.cs | 6 +- .../GameModes/SinglePlayerCampaign.cs | 4 +- .../ClientSource/GameSession/GameSession.cs | 1 + .../ClientSource/GameSession/RoundSummary.cs | 2 +- .../Items/Components/ItemComponent.cs | 14 +- .../Items/Components/Machines/Fabricator.cs | 47 ++- .../Items/Components/Machines/MiniMap.cs | 6 +- .../ClientSource/Map/Map/Map.cs | 2 +- .../ClientSource/Networking/GameClient.cs | 68 ++-- .../ClientSource/Networking/Voting.cs | 319 ++++++++-------- .../CampaignSetupUI/CampaignSetupUI.cs | 58 +++ .../MultiPlayerCampaignSetupUI.cs | 75 +--- .../SinglePlayerCampaignSetupUI.cs | 74 +--- .../ClientSource/Screens/CampaignUI.cs | 22 +- .../Screens/CharacterEditor/Wizard.cs | 11 +- .../ClientSource/Screens/EditorScreen.cs | 2 + .../ClientSource/Screens/MainMenuScreen.cs | 9 +- .../ClientSource/Screens/NetLobbyScreen.cs | 93 +---- .../ClientSource/Screens/ServerListScreen.cs | 30 +- .../ClientSource/Screens/SubEditorScreen.cs | 30 +- .../ClientSource/Settings/SettingsMenu.cs | 8 +- .../Steam/{ => WorkshopMenu}/BBCode.cs | 8 +- .../Immutable/ImmutableWorkshopMenu.cs | 41 +++ .../{ => WorkshopMenu/Mutable}/ItemList.cs | 2 +- .../Mutable/MutableWorkshopMenu.cs} | 18 +- .../{ => WorkshopMenu/Mutable}/PublishTab.cs | 2 +- .../Steam/{ => WorkshopMenu}/UiUtil.cs | 24 +- .../Steam/WorkshopMenu/WorkshopMenu.cs | 12 + .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../GameModes/CharacterCampaignData.cs | 2 +- .../GameModes/MultiPlayerCampaign.cs | 248 ++++++------- .../ServerSource/GameSession/MedicalClinic.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 160 ++++---- .../ServerSource/Networking/ServerSettings.cs | 4 +- .../ServerSource/Networking/Voting.cs | 268 ++++++++++---- .../ServerSource/Screens/NetLobbyScreen.cs | 2 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/permissionpresets.xml | 2 +- .../SharedSource/Characters/Character.cs | 9 +- .../ContentPackageManager.cs | 20 +- .../ContentManagement/ContentXElement.cs | 18 + .../BarotraumaShared/SharedSource/Enums.cs | 11 +- .../SharedSource/Events/Missions/Mission.cs | 16 +- .../Events/Missions/MissionPrefab.cs | 3 +- .../SharedSource/GameSession/CargoManager.cs | 1 + .../GameSession/Data/Reputation.cs | 2 +- .../SharedSource/GameSession/Data/Wallet.cs | 15 +- .../GameSession/GameModes/CampaignMode.cs | 37 +- .../GameModes/MultiPlayerCampaign.cs | 4 +- .../SharedSource/GameSession/GameSession.cs | 62 +++- .../Items/Components/Machines/Controller.cs | 93 +++-- .../Items/Components/Machines/Fabricator.cs | 30 +- .../SharedSource/Items/Item.cs | 11 +- .../SharedSource/Items/ItemPrefab.cs | 15 +- .../SharedSource/Map/Levels/Level.cs | 8 +- .../Map/Levels/LevelGenerationParams.cs | 4 +- .../SharedSource/Map/Map/Location.cs | 12 +- .../SharedSource/Map/Map/LocationType.cs | 37 +- .../SharedSource/Map/Map/Map.cs | 41 ++- .../Map/Outposts/OutpostGenerator.cs | 5 + .../SharedSource/Map/SubmarineInfo.cs | 79 +--- .../Networking/ClientPermissions.cs | 7 +- .../SharedSource/Networking/NetworkMember.cs | 7 +- .../SharedSource/Networking/ServerSettings.cs | 85 +++-- .../SharedSource/Networking/Voting.cs | 26 +- .../SharedSource/Steam/Workshop.cs | 10 +- .../SharedSource/SteamAchievementManager.cs | 10 +- .../SharedSource/{Map => Utils}/Md5Hash.cs | 0 .../SharedSource/Utils/NamedEvent.cs | 6 + .../SharedSource/Utils/SaveUtil.cs | 58 ++- Barotrauma/BarotraumaShared/changelog.txt | 50 +++ 88 files changed, 2033 insertions(+), 1430 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs rename Barotrauma/BarotraumaClient/ClientSource/Steam/{ => WorkshopMenu}/BBCode.cs (96%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs rename Barotrauma/BarotraumaClient/ClientSource/Steam/{ => WorkshopMenu/Mutable}/ItemList.cs (99%) rename Barotrauma/BarotraumaClient/ClientSource/Steam/{WorkshopMenu.cs => WorkshopMenu/Mutable/MutableWorkshopMenu.cs} (97%) rename Barotrauma/BarotraumaClient/ClientSource/Steam/{ => WorkshopMenu/Mutable}/PublishTab.cs (99%) rename Barotrauma/BarotraumaClient/ClientSource/Steam/{ => WorkshopMenu}/UiUtil.cs (79%) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs rename Barotrauma/BarotraumaShared/SharedSource/{Map => Utils}/Md5Hash.cs (100%) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 4bd56bce4..0891e5008 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -1151,15 +1151,7 @@ namespace Barotrauma } } - partial void OnMoneyChanged(int prevAmount, int newAmount) - { - if (newAmount > prevAmount) - { - int increase = newAmount - prevAmount; - AddMessage("+" + TextManager.GetWithVariable("currencyformat", "[credits]", "[value]").Value, - GUIStyle.Yellow, playSound: this == Controlled, "money".ToIdentifier(), increase); - } - } + partial void OnMoneyChanged(int prevAmount, int newAmount) { } partial void OnTalentGiven(TalentPrefab talentPrefab) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs new file mode 100644 index 000000000..026b93043 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs @@ -0,0 +1,225 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +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; + +namespace Barotrauma.Transition +{ + /// + /// Class dedicated to transitioning away from the old, shitty + /// Mods + Submarines folders to the new LocalMods folder + /// + public static class UgcTransition + { + private const string readmeName = "LOCALMODS_README.txt"; + + public static void Prepare() + { + TaskPool.Add("UgcTransition.Prepare", DetermineItemsToTransition(), t => + { + if (!t.TryGetResult(out (OldSubs, OldMods) pair)) { return; } + var (subs, mods) = pair; + if (!subs.FilePaths.Any() && !mods.Mods.Any()) { return; } + + var msgBox = new GUIMessageBox(TextManager.Get("Ugc.TransferTitle"), "", relativeSize: (0.5f, 0.8f), + buttons: new LocalizedString[] { TextManager.Get("Ugc.TransferButton") }); + + var desc = new GUITextBlock(new RectTransform((1.0f, 0.24f), msgBox.Content.RectTransform), + text: TextManager.Get("Ugc.TransferDesc"), wrap: true, textAlignment: Alignment.CenterLeft); + + var modsList = new GUIListBox(new RectTransform((1.0f, 0.6f), msgBox.Content.RectTransform)) + { + HoverCursor = CursorState.Default + }; + Dictionary pathTickboxMap = new Dictionary(); + + void addHeader(LocalizedString str) + { + var itemFrame = new GUITextBlock(new RectTransform((1.0f, 0.08f), modsList.Content.RectTransform), + text: str, font: GUIStyle.SubHeadingFont) + { + CanBeFocused = false + }; + } + void addTickbox(string dir, string name, bool ticked) + { + var itemFrame = new GUIFrame(new RectTransform((1.0f, 0.07f), modsList.Content.RectTransform), + style: null) + { + CanBeFocused = false + }; + var tickbox = new GUITickBox(new RectTransform(Vector2.One, itemFrame.RectTransform), name) + { + Selected = ticked + }; + pathTickboxMap.Add(dir, tickbox); + } + + addHeader(TextManager.Get("WorkshopLabelSubmarines")); + foreach (var sub in subs.FilePaths) + { + var subName = Path.GetFileNameWithoutExtension(sub); + addTickbox(sub, subName, ticked: !ContentPackageManager.LocalPackages.Any(p => p.NameMatches(subName))); + } + + addHeader(""); + addHeader(TextManager.Get("SubscribedMods")); + foreach (var mod in mods.Mods) + { + addTickbox(mod.Dir, mod.Name, ticked: !ContentPackageManager.LocalPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == mod.Item?.Id)); + } + + GUIMessageBox? subMsgBox = null; + + void createSubMsgBox(LocalizedString str, bool closable) + { + subMsgBox?.Close(); + subMsgBox = new GUIMessageBox(headerText: "", text: str, + buttons: closable ? new[] { TextManager.Get("Close") } : Array.Empty()); + if (closable) + { + subMsgBox.Buttons[0].OnClicked = subMsgBox.Close; + } + } + + msgBox.Buttons[0].OnClicked = (b, o) => + { + TaskPool.Add("TransferMods", TransferMods(pathTickboxMap), t2 => + { + if (t2.Exception != null) + { + DebugConsole.ThrowError("There was an error transferring mods", t2.Exception.GetInnermost()); + } + ContentPackageManager.LocalPackages.Refresh(); + createSubMsgBox(TextManager.Get("Ugc.TransferComplete"), closable: true); + }); + msgBox.Close(); + createSubMsgBox(TextManager.Get("Ugc.Transferring"), closable: false); + return false; + }; + }); + } + + private struct OldSubs + { + public readonly IReadOnlyList FilePaths; + + public OldSubs(IReadOnlyList filePaths) + { + FilePaths = filePaths; + } + } + + private struct OldMods + { + public readonly IReadOnlyList<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> Mods; + + public OldMods(IReadOnlyList<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> mods) + { + Mods = mods; + } + } + + private const string oldSubsPath = "Submarines"; + private const string oldModsPath = "Mods"; + + private static async Task<(OldSubs Subs, OldMods Mods)> DetermineItemsToTransition() + { + string[] subs = Array.Empty(); + List<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> mods + = new List<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)>(); + if (FolderShouldBeTransitioned(oldModsPath)) + { + subs = Directory.GetFiles(oldSubsPath, "*.sub", SearchOption.TopDirectoryOnly); + string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", SearchOption.TopDirectoryOnly); + + var publishedItems = await SteamManager.Workshop.GetPublishedItems(); + foreach (var modDir in allOldMods) + { + var fileList = XMLExtensions.TryLoadXml(Path.Combine(modDir, ContentPackage.FileListFileName), out _); + if (fileList?.Root is null) { continue; } + + var oldId = fileList.Root.GetAttributeUInt64("steamworkshopid", 0); + var updateTime = File.GetLastWriteTime(modDir).ToUniversalTime(); + var oldName = fileList.Root.GetAttributeString("name", ""); + + var item = oldId != 0 ? publishedItems.FirstOrNull(it => it.Id == oldId) : null; + if (oldId == 0 || item.HasValue) + { + mods.Add((modDir, oldName, item, updateTime)); + } + } + } + + while (!(Screen.Selected is MainMenuScreen)) { await Task.Delay(500); } + + return (new OldSubs(subs), new OldMods(mods)); + } + + private static bool FolderShouldBeTransitioned(string folderName) + { + return Directory.Exists(folderName) + && !File.Exists(Path.Combine(folderName, readmeName)); + } + + private static async Task TransferMods(Dictionary pathTickboxMap) + { + //WriteReadme(oldSubsPath); //can't do this because the submarine discovery code is borked + WriteReadme(oldModsPath); + foreach (var (path, tickbox) in pathTickboxMap) + { + if (!tickbox.Selected) { continue; } + string dirName = Path.GetFileNameWithoutExtension(path); + string destPath = Path.Combine(ContentPackage.LocalModsDir, dirName); + + //find unique path to save in + for (int i = 0;;i++) + { + if (!Directory.Exists(destPath)) { break; } + destPath = Path.Combine(ContentPackage.LocalModsDir, $"{dirName}.{i}"); + } + + if (path.StartsWith(oldSubsPath, StringComparison.OrdinalIgnoreCase)) + { + //copying a sub: manually create filelist.xml + ModProject modProject = new ModProject + { + Name = dirName, + ModVersion = ContentPackage.DefaultModVersion + }; + modProject.AddFile(ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{dirName}.sub"))); + + Directory.CreateDirectory(destPath); + File.Copy(path, Path.Combine(destPath, $"{dirName}.sub")); + modProject.Save(Path.Combine(destPath, ContentPackage.FileListFileName)); + + await Task.Yield(); + } + else + { + //copying a mod: we have a neat method for that! + await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath); + } + } + } + + private static void WriteReadme(string folderName) + { + if (!Directory.Exists(folderName)) { return; } + File.WriteAllText(path: Path.Combine(folderName, readmeName), + contents: "This folder is no longer used by Barotrauma;\n" + + "your mods and submarines should have been transferred\n" + + "to LocalMods. If they are not being found, delete this\n" + + "readme and relaunch the game.", encoding: Encoding.UTF8); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index f653a1a83..57d4bc38f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1101,58 +1101,6 @@ namespace Barotrauma } }, isCheat: true)); - commands.Add(new Command("save|savesub", "save [submarine name]: Save the currently loaded submarine using the specified name.", (string[] args) => - { - if (args.Length < 1) { return; } - - GameMain.SubEditorScreen.SetMode(SubEditorScreen.Mode.Default); - - string fileName = string.Join(" ", args); - if (fileName.Contains("../")) - { - ThrowError("Illegal symbols in filename (../)"); - return; - } - - if (Submarine.MainSub.TrySaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) - { - NewMessage("Sub saved", Color.Green); - } - })); - - commands.Add(new Command("load|loadsub", "load [submarine name]: Load a submarine.", (string[] args) => - { - if (args.Length == 0) { return; } - - if (GameMain.GameSession != null) - { - ThrowError("The loadsub command cannot be used when a round is running. You should probably be using spawnsub instead."); - return; - } - - string name = string.Join(" ", args); - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => name.Equals(s.Name, StringComparison.OrdinalIgnoreCase)); - if (subInfo == null) - { - string path = Path.Combine(SubmarineInfo.SavePath, name); - if (!File.Exists(path)) - { - ThrowError($"Could not find a submarine with the name \"{name}\" or in the path {path}."); - return; - } - subInfo = new SubmarineInfo(path); - } - - Submarine.Load(subInfo, true); - }, - () => - { - return new string[][] - { - SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() - }; - })); - commands.Add(new Command("cleansub", "", (string[] args) => { for (int i = MapEntity.mapEntityList.Count - 1; i >= 0; i--) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index 17f493e12..37935913a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -22,7 +22,7 @@ namespace Barotrauma private GUIButton clearAllButton; private List PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; - private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); + private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(ClientPermissions.ManageHires); private Point resolutionWhenCreated; @@ -433,7 +433,7 @@ namespace Barotrauma else if (!btn.Enabled) { btn.ToolTip = string.Empty; - btn.Enabled = true; + btn.Enabled = HasPermission; } }; @@ -632,7 +632,7 @@ namespace Barotrauma totalBlock.Text = TextManager.FormatCurrency(total); bool enoughMoney = campaign == null || campaign.Wallet.CanAfford(total); totalBlock.TextColor = enoughMoney ? Color.White : Color.Red; - validateHiresButton.Enabled = enoughMoney && pendingList.Content.RectTransform.Children.Any(); + validateHiresButton.Enabled = enoughMoney && HasPermission && pendingList.Content.RectTransform.Children.Any(); } public bool ValidateHires(List hires, bool createNetworkEvent = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index cba0182cf..88b835ede 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -316,26 +316,27 @@ namespace Barotrauma if (GameMain.ShowPerf) { + int x = 400; int y = 10; - DrawString(spriteBatch, new Vector2(300, y), + DrawString(spriteBatch, new Vector2(x, y), "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; - GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: GUIStyle.Green); + GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle(x, y, 170, 50), color: GUIStyle.Green); y += 50; - DrawString(spriteBatch, new Vector2(300, y), + 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; - GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(300, y, 170, 50), color: Color.LightBlue); + GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle(x, y, 170, 50), color: Color.LightBlue); y += 50; foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers) { float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key); - DrawString(spriteBatch, new Vector2(300, y), + 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; @@ -351,18 +352,19 @@ namespace Barotrauma if (Powered.Grids != null) { - DrawString(spriteBatch, new Vector2(300, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); + DrawString(spriteBatch, new Vector2(x, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont); y += 15; } if (Settings.EnableDiagnostics) { - DrawString(spriteBatch, new Vector2(320, 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(320, y + 15), "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(320, y + 30), "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(320, y + 45), "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(320, y + 60), "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(320, y + 75), "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); + x += 20; + 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), "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), "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), "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), "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), "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); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 7a096a1a6..4635c2150 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -534,7 +534,10 @@ namespace Barotrauma } } Vector2 finalBottomRight = characterPositions[endIndex]; - finalBottomRight += Font.MeasureChar(Text[endIndex]) * TextBlock.TextScale; + if (Text.Length > endIndex) + { + finalBottomRight += Font.MeasureChar(Text[endIndex]) * TextBlock.TextScale; + } drawRect(topLeft, finalBottomRight); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 7d408efd1..88bf466e4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -113,71 +113,40 @@ namespace Barotrauma #region Permissions - private bool hadPermissions, hadBuyPermissions, hadSellInventoryPermissions, hadSellSubPermissions; + private bool hadBuyPermissions, hadSellInventoryPermissions, hadSellSubPermissions; - private bool HasPermissions - { - get => GetPermissions(); - set => hadPermissions = value; - } private bool HasBuyPermissions { - get => HasPermissions || GetPermissions(StoreTab.Buy); + get => HasPermissionToUseTab(StoreTab.Buy); set => hadBuyPermissions = value; } private bool HasSellInventoryPermissions { - get => HasPermissions || GetPermissions(StoreTab.Sell); + get => HasPermissionToUseTab(StoreTab.Sell); set => hadSellInventoryPermissions = value; } private bool HasSellSubPermissions { - get => HasPermissions || GetPermissions(StoreTab.SellSub); + get => HasPermissionToUseTab(StoreTab.SellSub); set => hadSellSubPermissions = value; } - private bool GetPermissions(StoreTab? tab = null) + private bool HasPermissionToUseTab(StoreTab tab) { - if (!tab.HasValue) + return tab switch { - return campaignUI.Campaign.AllowedToManageCampaign() || campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.CampaignStore); - } - else - { - return tab.Value switch - { - StoreTab.Buy => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.BuyItems), - StoreTab.Sell => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems), - StoreTab.SellSub => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems), - _ => false, - }; - } + StoreTab.Buy => true, + StoreTab.Sell => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems), + StoreTab.SellSub => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems), + _ => false, + }; } - private void UpdatePermissions(StoreTab? tab = null) + private void UpdatePermissions() { - HasPermissions = GetPermissions(); - if (!tab.HasValue) - { - HasBuyPermissions = GetPermissions(StoreTab.Buy); - HasSellInventoryPermissions = GetPermissions(StoreTab.Sell); - HasSellSubPermissions = GetPermissions(StoreTab.SellSub); - } - else - { - switch (tab.Value) - { - case StoreTab.Buy: - HasBuyPermissions = GetPermissions(tab.Value); - break; - case StoreTab.Sell: - HasSellInventoryPermissions = GetPermissions(tab.Value); - break; - case StoreTab.SellSub: - HasSellSubPermissions = GetPermissions(tab.Value); - break; - } - } + HasBuyPermissions = HasPermissionToUseTab(StoreTab.Buy); + HasSellInventoryPermissions = HasPermissionToUseTab(StoreTab.Sell); + HasSellSubPermissions = HasPermissionToUseTab(StoreTab.SellSub); } private bool HasTabPermissions(StoreTab tab) @@ -196,23 +165,16 @@ namespace Barotrauma return HasTabPermissions(activeTab); } - private bool HavePermissionsChanged(StoreTab? tab = null) + private bool HavePermissionsChanged(StoreTab tab) { - if (!tab.HasValue) + bool hadTabPermissions = tab switch { - return hadPermissions != HasPermissions; - } - else - { - bool hadTabPermissions = tab.Value switch - { - StoreTab.Buy => hadBuyPermissions, - StoreTab.Sell => hadSellInventoryPermissions, - StoreTab.SellSub => hadSellSubPermissions, - _ => false - }; - return hadTabPermissions != HasTabPermissions(tab.Value); - } + StoreTab.Buy => hadBuyPermissions, + StoreTab.Sell => hadSellInventoryPermissions, + StoreTab.SellSub => hadSellSubPermissions, + _ => false + }; + return hadTabPermissions != HasTabPermissions(tab); } #endregion @@ -2202,10 +2164,6 @@ namespace Barotrauma { RefreshItemsToSellFromSub(); } - if (needsRefresh || HavePermissionsChanged()) - { - Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); - } if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) { RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index abd866462..da6b8d6d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -43,6 +43,7 @@ namespace Barotrauma private GUIButton transferMenuButton; private float transferMenuOpenState; private bool transferMenuStateCompleted; + private readonly HashSet registeredEvents = new HashSet(); private class LinkedGUI { @@ -218,7 +219,7 @@ namespace Barotrauma public void AddToGUIUpdateList() { - infoFrame?.AddToGUIUpdateList(order: 1); + infoFrame?.AddToGUIUpdateList(); NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); } @@ -298,11 +299,13 @@ namespace Barotrauma } SetBalanceText(balanceText, campaignMode.Bank.Balance); - campaignMode.OnMoneyChanged.RegisterOverwriteExisting(nameof(CreateInfoFrame).ToIdentifier(), e => + Identifier eventIdentifier = nameof(CreateInfoFrame).ToIdentifier(); + campaignMode.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => { - if (e.Wallet != campaignMode.Bank) { return; } + if (!e.Owner.IsNone()) { return; } SetBalanceText(balanceText, e.Wallet.Balance); }); + registeredEvents.Add(eventIdentifier); static void SetBalanceText(GUITextBlock text, int balance) { @@ -371,15 +374,16 @@ namespace Barotrauma } } - private const float jobColumnWidthPercentage = 0.138f; - private const float characterColumnWidthPercentage = 0.656f; - private const float pingColumnWidthPercentage = 0.206f; + private const float jobColumnWidthPercentage = 0.138f, + characterColumnWidthPercentage = 0.45f, + pingColumnWidthPercentage = 0.206f, + walletColumnWidthPercentage = 0.206f; - private int jobColumnWidth, characterColumnWidth, pingColumnWidth; + private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth; private void CreateCrewListFrame(GUIFrame crewFrame) { - crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? Array.Empty(); + crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new List() { TestScreen.dummyCharacter}; teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); // Show own team first when there's more than one team @@ -553,20 +557,23 @@ namespace Barotrauma GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale"); + GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale"); sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); + walletButton.RectTransform.RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont; - jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = walletButton.TextBlock.Font = GUIStyle.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = walletButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = walletButton.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; pingColumnWidth = pingButton.Rect.Width; + walletColumnWidth = walletButton.Rect.Width; } private void CreateMultiPlayerList(bool refresh) @@ -651,6 +658,8 @@ namespace Barotrauma }; } } + + CreateWalletCrewFrame(character, paddedFrame); } private void CreateMultiPlayerClientElement(Client client) @@ -678,6 +687,10 @@ namespace Barotrauma }; CreateNameWithPermissionIcon(client, paddedFrame); + if (client.Character is { } character) + { + CreateWalletCrewFrame(character, paddedFrame); + } linkedGUIList.Add(new LinkedGUI(client, frame, false, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); } @@ -714,6 +727,47 @@ namespace Barotrauma return 0; } + private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame) + { + GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center) + { + CanBeFocused = false + }; + + if (character.IsBot) { return; } + + GUILayoutGroup paddedLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1f), walletLayout.RectTransform, Anchor.Center), isHorizontal: true) + { + Stretch = true + }; + + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "StoreTradingIcon", scaleToFit: true); + GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); + SetWalletText(walletBlock, character.Wallet); + + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + { + Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}"); + campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => + { + if (!(e.Owner is Some { Value: var owner }) || owner != character) { return; } + SetWalletText(walletBlock, e.Wallet); + }); + registeredEvents.Add(eventIdentifier); + } + + static void SetWalletText(GUITextBlock block, Wallet wallet) + { + block.Text = TextManager.FormatCurrency(wallet.Balance); + block.ToolTip = string.Empty; + if (block.TextSize.X + block.Padding.X + block.Padding.Z > block.Rect.Width) + { + block.ToolTip = block.Text; + block.Text = TextManager.Get("crewwallet.balance.toomuchtoshow"); + } + } + } + private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame) { GUITextBlock characterNameBlock; @@ -795,7 +849,7 @@ namespace Barotrauma GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter"); if (existingPreview != null) { infoFrameHolder.RemoveChild(existingPreview); } - GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) }) + GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.69f), infoFrameHolder.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) }) { UserData = "SelectedCharacter" }; @@ -810,28 +864,29 @@ namespace Barotrauma { GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); GameMain.Client.SelectCrewCharacter(character, preview); - CreateWalletFrame(background, character); + if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); } } } else if (client != null) { GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); GameMain.Client?.SelectCrewClient(client, preview); - if (client.Character != null) + if (client.Character != null && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { - CreateWalletFrame(background, client.Character); + CreateWalletFrame(background, client.Character, mpCampaign); } } return true; } - private void CreateWalletFrame(GUIComponent parent, Character character) + private void CreateWalletFrame(GUIComponent parent, Character character, MultiPlayerCampaign campaign) { + if (campaign is null) { throw new ArgumentNullException(nameof(campaign), "Tried to create a wallet frame when campaign was null"); } if (character is null) { throw new ArgumentNullException(nameof(character), "Tried to create a wallet frame for a null character");} isTransferMenuOpen = false; transferMenuOpenState = 1f; - ImmutableArray salaryCrew = Mission.GetSalaryEligibleCrew().Where(c => c != character).ToImmutableArray(); + ImmutableHashSet salaryCrew = GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != character).ToImmutableHashSet(); Wallet targetWallet = character.Wallet; @@ -905,8 +960,8 @@ namespace Barotrauma GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center); GUILayoutGroup centerButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), buttonLayout.RectTransform), isHorizontal: true); - GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false }; + GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false }; // @formatter:on ImmutableArray layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout); MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups); @@ -927,137 +982,146 @@ namespace Barotrauma ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen); ToggleCenterButton(centerButton, isSending); - if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) + + if (!(Character.Controlled is { } myCharacter)) { - if (!(Character.Controlled is { } myCharacter)) - { - salarySlider.Enabled = false; - transferAmountInput.Enabled = false; - centerButton.Enabled = false; - confirmButton.Enabled = false; - return; - } + salarySlider.Enabled = false; + transferAmountInput.Enabled = false; + centerButton.Enabled = false; + confirmButton.Enabled = false; + return; + } - bool hasPermissions = campaign.AllowedToManageCampaign(); - salarySlider.Enabled = hasPermissions; - Wallet otherWallet; + bool hasMoneyPermissions = campaign.AllowedToManageCampaign(ClientPermissions.ManageMoney); + salarySlider.Enabled = hasMoneyPermissions; + Wallet otherWallet; - switch (hasPermissions) - { - 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; - } + 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); - - if (!hasPermissions) + MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups); + updateButtonText(); + if (!hasMoneyPermissions) + { + if (character != Character.Controlled) { centerButton.Enabled = centerButton.CanBeFocused = false; - salarySlider.Enabled = salarySlider.CanBeFocused = false; } + salarySlider.Enabled = salarySlider.CanBeFocused = false; + } - leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); + leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance); + UpdateAllInputs(); + + centerButton.OnClicked = (btn, o) => + { + isSending = !isSending; + updateButtonText(); + ToggleCenterButton(btn, isSending); UpdateAllInputs(); + return true; + }; - centerButton.OnClicked = (btn, o) => + void updateButtonText() + { + confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney"); + } + + transferAmountInput.OnValueChanged = input => + { + UpdateInputs(); + }; + + transferAmountInput.OnValueEntered = input => + { + UpdateAllInputs(); + }; + + Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier(); + campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => + { + if (e.Wallet == targetWallet) { - isSending = !isSending; - ToggleCenterButton(btn, isSending); - UpdateAllInputs(); - return true; - }; - - transferAmountInput.OnValueChanged = input => - { - UpdateInputs(); - }; - - transferAmountInput.OnValueEntered = input => - { - UpdateAllInputs(); - }; - - campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(CreateWalletFrame).ToIdentifier(), e => - { - if (e.Wallet == targetWallet) - { - moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); - salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; - } - 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; - }; - - void UpdateAllInputs() - { - UpdateInputs(); - UpdateMaxInput(); + moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance); + salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f; } + UpdateAllInputs(); + }); + registeredEvents.Add(eventIdentifier); - void UpdateInputs() - { - 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; - } - } + resetButton.OnClicked = (button, o) => + { + transferAmountInput.IntValue = 0; + UpdateAllInputs(); + return true; + }; - void UpdateMaxInput() + 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) { - transferAmountInput.MaxValueInt = isSending ? targetWallet.Balance : otherWallet.Balance; + 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; + } + } + + void UpdateMaxInput() + { + transferAmountInput.MaxValueInt = isSending ? targetWallet.Balance : otherWallet.Balance; } static void ToggleTransferMenuIcon(GUIButton btn, bool open) @@ -1173,7 +1237,7 @@ namespace Barotrauma private void CreateMultiPlayerLogContent(GUIFrame crewFrame) { - var logContainer = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), crewFrame.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) }); + var logContainer = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) }); var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.900f, 0.900f), logContainer.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, 0.0475f) }, style: null); var content = new GUILayoutGroup(new RectTransform(Vector2.One, innerFrame.RectTransform)) { @@ -1188,22 +1252,22 @@ namespace Barotrauma Spacing = (int)(5 * GUI.Scale) }; - foreach (Pair pair in storedMessages) + foreach ((string message, PlayerConnectionChangeType type) in storedMessages) { - AddLineToLog(pair.First, pair.Second); + AddLineToLog(message, type); } logList.BarScroll = 1f; } - private static readonly List> storedMessages = new List>(); + private static readonly List<(string message, PlayerConnectionChangeType type)> storedMessages = new List<(string message, PlayerConnectionChangeType type)>(); public static void StorePlayerConnectionChangeMessage(ChatMessage message) { if (!GameMain.GameSession?.IsRunning ?? true) { return; } string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; - storedMessages.Add(new Pair(msg, message.ChangeType)); + storedMessages.Add((msg, message.ChangeType)); if (GameSession.IsTabMenuOpen && SelectedTab == InfoFrameTab.Crew) { @@ -2026,5 +2090,14 @@ namespace Barotrauma if (character != Character.Controlled) { return; } UpdateTalentInfo(); } + + public void OnClose() + { + if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; } + foreach (Identifier identifier in registeredEvents) + { + campaign.OnMoneyChanged.TryDeregister(identifier); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index a6a0f3a54..cbdf71c9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -73,6 +73,8 @@ namespace Barotrauma private Point screenResolution; + private bool needsRefresh = true; + /// /// While set to true any call to will cause the buy button to be disabled and to not update the prices. /// This is to prevent us from buying another upgrade before the server has given us the new prices and causing potential syncing issues. @@ -102,13 +104,18 @@ namespace Barotrauma CreateUI(upgradeFrame); if (Campaign == null) { return; } - Campaign.UpgradeManager.OnUpgradesChanged += RefreshAll; - Campaign.CargoManager.OnPurchasedItemsChanged += RefreshAll; - Campaign.CargoManager.OnSoldItemsChanged += RefreshAll; - Campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(UpgradeStore).ToIdentifier(), e => { RefreshAll(); } ); + Campaign.UpgradeManager.OnUpgradesChanged += RequestRefresh; + Campaign.CargoManager.OnPurchasedItemsChanged += RequestRefresh; + Campaign.CargoManager.OnSoldItemsChanged += RequestRefresh; + Campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(UpgradeStore).ToIdentifier(), e => { RequestRefresh(); } ); } - public void RefreshAll() + public void RequestRefresh() + { + needsRefresh = true; + } + + private void RefreshAll() { switch (selectedUpgradeTab) { @@ -131,6 +138,7 @@ namespace Barotrauma } break; } + needsRefresh = false; } private void RefreshUpgradeList() @@ -1295,7 +1303,9 @@ namespace Barotrauma { if (Campaign == null) { return; } - if (!parent.Children.Any() || Submarine.MainSub != null && Submarine.MainSub != drawnSubmarine || GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) + if (!parent.Children.Any() || + Submarine.MainSub != null && Submarine.MainSub != drawnSubmarine || + GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind(); drawnSubmarine = Submarine.MainSub; @@ -1313,6 +1323,10 @@ namespace Barotrauma // we also need this when we first load in so we know which category entries to disable since the CampaignUI is created before the submarine is loaded in. RefreshAll(); } + if (needsRefresh) + { + RefreshAll(); + } // accept an active confirmation popup if any if (PlayerInput.KeyHit(Keys.Enter) && GUIMessageBox.MessageBoxes.Any()) @@ -1588,7 +1602,7 @@ namespace Barotrauma if (button != null) { button.Enabled = currentLevel < prefab.MaxLevel; - if (WaitForServerUpdate || !campaign.AllowedToManageCampaign() || !campaign.Wallet.CanAfford(price)) + if (WaitForServerUpdate || !campaign.Wallet.CanAfford(price)) { button.Enabled = false; } @@ -1693,7 +1707,7 @@ namespace Barotrauma return frames.ToArray(); } - private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); + private bool HasPermission => true; // just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me) private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index 4f5541602..a927e30a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -22,27 +23,50 @@ namespace Barotrauma private float votingTime = 100f; private float timer; private VoteType currentVoteType; - private Color submarineColor => GUIStyle.Orange; + private Color SubmarineColor => GUIStyle.Orange; private Point createdForResolution; - public VotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) + public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, float votingTime) { - if (starter == null || info == null) return; - SetSubmarineVotingText(starter, info, type); - this.votingTime = votingTime; - getYesVotes = SubmarineYesVotes; - getNoVotes = SubmarineNoVotes; - getMaxVotes = SubmarineMaxVotes; - onVoteEnd = () => SendSubmarineVoteEndMessage(info, type); + if (starter == null || info == null) { return null; } - Initialize(starter, type); + var subVoting = new VotingInterface() + { + votingTime = votingTime, + getYesVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountYes(type) ?? 0, + getNoVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountNo(type) ?? 0, + getMaxVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountMax(type) ?? 0, + }; + subVoting.onVoteEnd = () => subVoting.SendSubmarineVoteEndMessage(info, type); + subVoting.SetSubmarineVotingText(starter, info, type); + subVoting.Initialize(starter, type); + return subVoting; } + public static VotingInterface CreateMoneyTransferVotingInterface(Client starter, Client from, Client to, int amount, float votingTime) + { + if (starter == null) { return null; } + if (from == null && to == null) { return null; } + + var transferVoting = new VotingInterface() + { + votingTime = votingTime, + getYesVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountYes(VoteType.TransferMoney) ?? 0, + getNoVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountNo(VoteType.TransferMoney) ?? 0, + getMaxVotes = () => GameMain.NetworkMember?.Voting?.GetVoteCountMax(VoteType.TransferMoney) ?? 0, + }; + transferVoting.onVoteEnd = () => transferVoting.SendMoneyTransferVoteEndMessage(from, to, amount); + transferVoting.SetMoneyTransferVotingText(starter, from, to, amount); + transferVoting.Initialize(starter, VoteType.TransferMoney); + return transferVoting; + } + + private void Initialize(Client starter, VoteType type) { currentVoteType = type; CreateVotingGUI(); - if (starter.ID == GameMain.Client.ID) SetGUIToVotedState(2); + if (starter.ID == GameMain.Client.ID) { SetGUIToVotedState(2); } VoteRunning = true; } @@ -50,7 +74,7 @@ namespace Barotrauma { createdForResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - if (frame != null) frame.Parent.RemoveChild(frame); + frame?.Parent.RemoveChild(frame); frame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.VotingArea, GameMain.Client.InGameHUD.RectTransform), style: ""); int padding = HUDLayoutSettings.Padding * 2; @@ -116,8 +140,8 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!VoteRunning) return; - if (GameMain.GraphicsWidth != createdForResolution.X || GameMain.GraphicsHeight != createdForResolution.Y) CreateVotingGUI(); + if (!VoteRunning) { return; } + if (GameMain.GraphicsWidth != createdForResolution.X || GameMain.GraphicsHeight != createdForResolution.Y) { CreateVotingGUI(); } yesVotes = getYesVotes(); noVotes = getNoVotes(); maxVotes = getMaxVotes(); @@ -126,7 +150,6 @@ namespace Barotrauma votingTimer.BarSize = timer / votingTime; } - public void EndVote(bool passed, int yesVoteFinal, int noVoteFinal) { VoteRunning = false; @@ -143,19 +166,20 @@ namespace Barotrauma 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 submarineRichString = $"‖color:{SubmarineColor.R},{SubmarineColor.G},{SubmarineColor.B}‖{info.DisplayName}‖color:end‖"; + LocalizedString text = string.Empty; switch (type) { case VoteType.PurchaseAndSwitchSub: - votingOnText = TextManager.GetWithVariables("submarinepurchaseandswitchvote", + text = TextManager.GetWithVariables("submarinepurchaseandswitchvote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[amount]", info.Price.ToString()), ("[currencyname]", TextManager.Get("credit").ToLower())); break; case VoteType.PurchaseSub: - votingOnText = TextManager.GetWithVariables("submarinepurchasevote", + text = TextManager.GetWithVariables("submarinepurchasevote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[amount]", info.Price.ToString()), @@ -163,10 +187,9 @@ namespace Barotrauma break; case VoteType.SwitchSub: int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); - if (deliveryFee > 0) { - votingOnText = TextManager.GetWithVariables("submarineswitchfeevote", + text = TextManager.GetWithVariables("submarineswitchfeevote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString), ("[locationname]", endLocation.Name), @@ -175,37 +198,22 @@ namespace Barotrauma } else { - votingOnText = TextManager.GetWithVariables("submarineswitchnofeevote", + text = TextManager.GetWithVariables("submarineswitchnofeevote", ("[playername]", characterRichString), ("[submarinename]", submarineRichString)); } break; } - votingOnText = RichString.Rich(votingOnText); - } - - private int SubmarineYesVotes() - { - return GameMain.NetworkMember.SubmarineVoteYesCount; - } - - private int SubmarineNoVotes() - { - return GameMain.NetworkMember.SubmarineVoteNoCount; - } - - private int SubmarineMaxVotes() - { - return GameMain.NetworkMember.SubmarineVoteMax; + votingOnText = RichString.Rich(text); } private void SendSubmarineVoteEndMessage(SubmarineInfo info, VoteType type) { - GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes.ToString(), noVotes.ToString(), votePassed).Value, ChatMessageType.Server); + GameMain.NetworkMember.AddChatMessage(GetSubmarineVoteResultMessage(info, type, yesVotes, noVotes, votePassed).Value, ChatMessageType.Server); } - public static LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, string yesVoteString, string noVoteString, bool votePassed) + private LocalizedString GetSubmarineVoteResultMessage(SubmarineInfo info, VoteType type, int yesVoteCount, int noVoteCount, bool votePassed) { LocalizedString result = string.Empty; @@ -214,18 +222,18 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchaseandswitchvotepassed" : "submarinepurchaseandswitchvotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", info.Price.ToString()), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteString), - ("[novotecount]" , noVoteString)); + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]" , noVoteCount.ToString())); break; case VoteType.PurchaseSub: result = TextManager.GetWithVariables(votePassed ? "submarinepurchasevotepassed" : "submarinepurchasevotefailed", ("[submarinename]", info.DisplayName), - ("[amount]", info.Price.ToString()), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", info.Price)), ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteString), - ("[novotecount]", noVoteString)); + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); break; case VoteType.SwitchSub: int deliveryFee = SubmarineSelection.DeliveryFeePerDistanceTravelled * GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation); @@ -235,17 +243,17 @@ namespace Barotrauma result = TextManager.GetWithVariables(votePassed ? "submarineswitchfeevotepassed" : "submarineswitchfeevotefailed", ("[submarinename]", info.DisplayName), ("[locationname]", endLocation.Name), - ("[amount]", deliveryFee.ToString()), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", deliveryFee)), ("[currencyname]", TextManager.Get("credit").ToLower()), - ("[yesvotecount]", yesVoteString), - ("[novotecount]", noVoteString)); + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); } else { result = TextManager.GetWithVariables(votePassed ? "submarineswitchnofeevotepassed" : "submarineswitchnofeevotefailed", ("[submarinename]", info.DisplayName), - ("[yesvotecount]", yesVoteString), - ("[novotecount]", noVoteString)); + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); } break; default: @@ -255,6 +263,58 @@ namespace Barotrauma } #endregion + + private void SetMoneyTransferVotingText(Client starter, Client from, Client to, int amount) + { + 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‖"; + + LocalizedString text = string.Empty; + if (from == null && to != null) + { + text = TextManager.GetWithVariables("crewwallet.requestbanktoselfvote", + ("[requester]", characterRichString), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount))); + } + else if (from != null && to == null) + { + text = TextManager.GetWithVariables("crewwallet.requestselftobankvote", + ("[requester]", characterRichString), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount))); + } + else + { + //not supported atm: clients can only requests transfers between their own wallet and the bank + LocalizedString bankName = TextManager.Get("crewwallet.bank"); + text = TextManager.GetWithVariables("crewwallet.requesttransfervote", + ("[requester]", characterRichString), + ("[player1]", from?.Character == null ? bankName : from.Character.Name), + ("[player2]", to?.Character == null ? bankName : to.Character.Name), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount))); + } + + votingOnText = RichString.Rich(text); + } + private void SendMoneyTransferVoteEndMessage(Client from, Client to, int amount) + { + GameMain.NetworkMember.AddChatMessage(GetMoneyTransferVoteResultMessage(from, to, amount, yesVotes, noVotes, votePassed).Value, ChatMessageType.Server); + } + + public static LocalizedString GetMoneyTransferVoteResultMessage(Client from, Client to, int transferAmount, int yesVoteCount, int noVoteCount, bool votePassed) + { + LocalizedString result = string.Empty; + if (from != null) + { + result = TextManager.GetWithVariables(votePassed ? "crewwallet.banktoplayer.votepassed" : "crewwallet.banktoplayer.votefailed", + ("[playername]", from.Name), + ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", transferAmount)), + ("[yesvotecount]", yesVoteCount.ToString()), + ("[novotecount]", noVoteCount.ToString())); + } + return result; + } public void Remove() { if (frame != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index e4c8ab417..a046c051f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -17,6 +17,7 @@ using Barotrauma.Tutorials; using Barotrauma.Media; using Barotrauma.Extensions; using System.Threading.Tasks; +using Barotrauma.Transition; namespace Barotrauma { @@ -463,6 +464,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; + UgcTransition.Prepare(); var contentPackageLoadRoutine = ContentPackageManager.Init(); foreach (var progress in contentPackageLoadRoutine) { @@ -691,14 +693,12 @@ namespace Barotrauma } else if (GameSettings.CurrentConfig.AutomaticCampaignLoadEnabled) { - IEnumerable saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); - + var saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); if (saveFiles.Count() > 0) { - saveFiles = saveFiles.OrderBy(file => File.GetLastWriteTime(file)); try { - SaveUtil.LoadGame(saveFiles.Last()); + SaveUtil.LoadGame(saveFiles.OrderBy(file => file.SaveTime).Last().FilePath); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index 29cea3e9b..aa1b4528a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -67,6 +67,8 @@ namespace Barotrauma public void SetSoldItems(Dictionary> items) { + if (SoldItems.Count == 0 && items.Count == 0) { return; } + SoldItems.Clear(); foreach (var entry in items) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 6d334c7a2..6b0927e40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -86,30 +86,14 @@ namespace Barotrauma /// /// There is a server-side implementation of the method in /// - public bool AllowedToEndRound() - { - //allow ending the round if the client has permissions, is the owner, the only client in the server - //or if no-one has management permissions - if (GameMain.Client == null) { return true; } - return - GameMain.Client.HasPermission(ClientPermissions.ManageRound) || - GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Client.ConnectedClients.Count == 1 || - GameMain.Client.IsServerOwner || - GameMain.Client.ConnectedClients.None(c => - c.InGame && (c.IsOwner || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign))); - } - - /// - /// There is a server-side implementation of the method in - /// - public bool AllowedToManageCampaign(ClientPermissions permissions = ClientPermissions.ManageCampaign) + public bool AllowedToManageCampaign(ClientPermissions permissions) { //allow managing the round if the client has permissions, is the owner, the only client in the server, //or if no-one has management permissions if (GameMain.Client == null) { return true; } return GameMain.Client.HasPermission(permissions) || + GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) || GameMain.Client.ConnectedClients.Count == 1 || GameMain.Client.IsServerOwner || GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions))); @@ -210,7 +194,7 @@ namespace Barotrauma if (endRoundButton.Visible) { - if (!AllowedToEndRound()) + if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) { buttonText = TextManager.Get("map"); } @@ -306,7 +290,7 @@ namespace Barotrauma default: ShowCampaignUI = true; CampaignUI.SelectTab(npc.CampaignInteractionType, storeIdentifier: npc.MerchantIdentifier); - CampaignUI.UpgradeStore?.RefreshAll(); + CampaignUI.UpgradeStore?.RequestRefresh(); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 2847da846..3c2dd322e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -42,7 +42,7 @@ namespace Barotrauma return PersonalWallet; } - public static void StartCampaignSetup(IEnumerable saveFiles) + public static void StartCampaignSetup(List saveFiles) { var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; parent.ClearChildren(); @@ -746,7 +746,7 @@ namespace Barotrauma if (reputation.HasValue) { campaign.Map.CurrentLocation.Reputation.SetReputation(reputation.Value); - campaign?.CampaignUI?.UpgradeStore?.RefreshAll(); + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } foreach (var availableMission in availableMissions) @@ -786,7 +786,7 @@ namespace Barotrauma if (shouldRefresh) { - campaign?.CampaignUI?.UpgradeStore?.RefreshAll(); + campaign?.CampaignUI?.UpgradeStore?.RequestRefresh(); } if (myCharacterInfo != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index ffa578204..53ba428fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -110,7 +110,7 @@ namespace Barotrauma petsElement = subElement; break; case Wallet.LowerCaseSaveElementName: - Bank = new Wallet(subElement); + Bank = new Wallet(Option.None(), subElement); break; case "stats": LoadStats(subElement); @@ -129,7 +129,7 @@ namespace Barotrauma int oldMoney = element.GetAttributeInt("money", 0); if (oldMoney > 0) { - Bank = new Wallet + Bank = new Wallet(Option.None()) { Balance = oldMoney }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 0f794fa40..627f79236 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -32,6 +32,7 @@ namespace Barotrauma } else { + tabMenu?.OnClose(); tabMenu = null; NetLobbyScreen.JobInfoFrame = null; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index abe401df3..06e09c661 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -318,7 +318,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); if (GameMain.IsMultiplayer && Character.Controlled is { } controlled) { - var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, Mission.GetSalaryEligibleCrew().Where(c => c != controlled), Option.Some(reward)); + var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option.Some(reward)); if (share > 0) { string shareFormatted = string.Format(CultureInfo.InvariantCulture, "{0:N0}", share); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 02ad190c4..a189f5470 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -38,11 +38,14 @@ namespace Barotrauma.Items.Components public readonly bool Loop; - public ItemSound(RoundSound sound, ActionType type, bool loop = false) + public readonly bool OnlyPlayInSameSub; + + public ItemSound(RoundSound sound, ActionType type, bool loop = false, bool onlyPlayInSameSub = false) { this.RoundSound = sound; this.Type = type; this.Loop = loop; + this.OnlyPlayInSameSub = onlyPlayInSameSub; } } @@ -339,6 +342,11 @@ namespace Barotrauma.Items.Components return; } + if (itemSound.OnlyPlayInSameSub && item.Submarine != null && Character.Controlled != null) + { + if (Character.Controlled.Submarine == null || !Character.Controlled.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return; } + } + if (itemSound.Loop) { if (loopingSoundChannel != null && loopingSoundChannel.Sound != itemSound.RoundSound.Sound) @@ -500,7 +508,9 @@ namespace Barotrauma.Items.Components RoundSound sound = RoundSound.Load(subElement); if (sound == null) { break; } - ItemSound itemSound = new ItemSound(sound, type, subElement.GetAttributeBool("loop", false)) + ItemSound itemSound = new ItemSound(sound, type, + subElement.GetAttributeBool("loop", false), + subElement.GetAttributeBool("onlyinsamesub", false)) { VolumeProperty = subElement.GetAttributeIdentifier("volumeproperty", "") }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 433243ee3..b5e745407 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -50,6 +50,9 @@ namespace Barotrauma.Items.Components [Serialize("FabricatorCreate", IsPropertySaveable.Yes)] public string CreateButtonText { get; set; } + [Serialize("vendingmachine.outofstock", IsPropertySaveable.Yes)] + public string FabricationLimitReachedText { get; set; } + partial void InitProjSpecific() { //CreateGUI(); @@ -195,7 +198,7 @@ namespace Barotrauma.Items.Components foreach (FabricationRecipe fi in fabricationRecipes.Values) { - var frame = new GUIFrame(new RectTransform(new Point(itemList.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) + var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null) { UserData = fi, HoverColor = Color.Gold * 0.2f, @@ -223,6 +226,13 @@ namespace Barotrauma.Items.Components AutoScaleVertical = true, ToolTip = fi.TargetItem.Description }; + + new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), + TextManager.Get(FabricationLimitReachedText), font: GUIStyle.SmallFont, textAlignment: Alignment.BottomRight) + { + UserData = nameof(FabricationLimitReachedText), + Visible = false + }; } } @@ -297,7 +307,8 @@ namespace Barotrauma.Items.Components } else { - sufficientSkillsText.Visible = false; + sufficientSkillsText.Visible = insufficientSkillsText.Visible = false; + sufficientSkillsText.Enabled = insufficientSkillsText.Enabled = false; } var requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform), @@ -493,14 +504,15 @@ namespace Barotrauma.Items.Components if (string.IsNullOrWhiteSpace(filter)) { itemList.Content.Children.ForEach(c => c.Visible = true); - return true; } - - foreach (GUIComponent child in itemList.Content.Children) + else { - FabricationRecipe recipe = child.UserData as FabricationRecipe; - if (recipe?.DisplayName == null) { continue; } - child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase); + foreach (GUIComponent child in itemList.Content.Children) + { + FabricationRecipe recipe = child.UserData as FabricationRecipe; + if (recipe?.DisplayName == null) { continue; } + child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase); + } } HideEmptyItemListCategories(); @@ -516,7 +528,10 @@ namespace Barotrauma.Items.Components { if (!(child.UserData is FabricationRecipe recipe)) { - child.Visible = recipeVisible; + if (child.Enabled) + { + child.Visible = recipeVisible; + } recipeVisible = false; } else @@ -719,24 +734,26 @@ namespace Barotrauma.Items.Components { foreach (GUIComponent child in itemList.Content.Children) { - if (!(child.UserData is FabricationRecipe itemPrefab)) { continue; } + if (!(child.UserData is FabricationRecipe recipe)) { continue; } - if (itemPrefab != selectedItem && + if (recipe != selectedItem && (child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y)) { continue; } - bool canBeFabricated = CanBeFabricated(itemPrefab, availableIngredients, character); - if (itemPrefab == selectedItem) + bool canBeFabricated = CanBeFabricated(recipe, availableIngredients, character); + if (recipe == selectedItem) { activateButton.Enabled = canBeFabricated; } var childContainer = child.GetChild(); - childContainer.GetChild().TextColor = Color.White * (canBeFabricated ? 1.0f : 0.5f); - childContainer.GetChild().Color = itemPrefab.TargetItem.InventoryIconColor * (canBeFabricated ? 1.0f : 0.5f); + childContainer.GetChild().Color = recipe.TargetItem.InventoryIconColor * (canBeFabricated ? 1.0f : 0.5f); + + var limitReachedText = child.FindChild(nameof(FabricationLimitReachedText)); + limitReachedText.Visible = !canBeFabricated && fabricationLimits.TryGetValue(recipe.RecipeHash, out int amount) && amount <= 0; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index a1ea10952..87289c772 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -396,7 +396,7 @@ namespace Barotrauma.Items.Components private bool VisibleOnItemFinder(Item it) { - if (it.Submarine != item.Submarine) { return false; } + if (!item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } if (it.NonInteractable || it.HiddenInGame) { return false; } if (it.GetComponent() == null) { return false; } @@ -432,10 +432,10 @@ namespace Barotrauma.Items.Components scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent() != null || it.GetComponent() != null)).ToImmutableHashSet(); + ImmutableHashSet hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent() != null || it.GetComponent() != null)).ToImmutableHashSet(); miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents); - IEnumerable electrialPointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); + IEnumerable electrialPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electrialPointsOfInterest, out electricalMapComponents); Dictionary electricChildren = new Dictionary(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 0b68a66ed..4f0dfbe2c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -412,7 +412,7 @@ namespace Barotrauma new GUIMessageBox(string.Empty, TextManager.Get("LockedPathTooltip")); } //clients aren't allowed to select the location without a permission - else if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign() ?? false) + else if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap) ?? false) { connectionHighlightState = 0.0f; SelectedConnection = connection; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 559fe77b3..1d3c335a7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -271,6 +271,7 @@ namespace Barotrauma.Networking otherClients = new List(); serverSettings = new ServerSettings(this, "Server", 0, 0, 0, false, false); + Voting = new Voting(); if (steamId == 0) { @@ -637,7 +638,7 @@ namespace Barotrauma.Networking if (gameStarted && Screen.Selected == GameMain.GameScreen) { - EndVoteTickBox.Visible = serverSettings.Voting.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); + EndVoteTickBox.Visible = ServerSettings.AllowEndVoting && HasSpawned && !(GameMain.GameSession?.GameMode is CampaignMode); respawnManager?.Update(deltaTime); @@ -898,13 +899,13 @@ namespace Barotrauma.Networking GUI.SetSavingIndicatorState(save); break; case ServerPacketHeader.CAMPAIGN_SETUP_INFO: - UInt16 saveCount = inc.ReadUInt16(); - List saveFiles = new List(); + byte saveCount = inc.ReadByte(); + List saveInfos = new List(); for (int i = 0; i < saveCount; i++) { - saveFiles.Add(inc.ReadString()); + saveInfos.Add(INetSerializableStruct.Read(inc)); } - MultiPlayerCampaign.StartCampaignSetup(saveFiles); + MultiPlayerCampaign.StartCampaignSetup(saveInfos); break; case ServerPacketHeader.PERMISSIONS: ReadPermissions(inc); @@ -1458,7 +1459,7 @@ namespace Barotrauma.Networking { if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - campaign.CampaignUI?.UpgradeStore?.RefreshAll(); + campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); } } @@ -1666,10 +1667,7 @@ namespace Barotrauma.Networking isOutpost = levelData.Type == LevelData.LevelType.Outpost; } - if (GameMain.Client?.ServerSettings?.Voting != null) - { - GameMain.Client.ServerSettings.Voting.ResetVotes(GameMain.Client.ConnectedClients); - } + Voting?.ResetVotes(GameMain.Client.ConnectedClients); if (loadTask != null) { @@ -1882,7 +1880,7 @@ namespace Barotrauma.Networking var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash); if (matchingSub == null) { - matchingSub = new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false) + matchingSub = new SubmarineInfo(Path.Combine(SaveUtil.SubmarineDownloadFolder, subName) + ".sub", subHash, tryLoad: false) { SubmarineClass = (SubmarineClass)subClass }; @@ -2086,7 +2084,7 @@ namespace Barotrauma.Networking { if (GameMain.GameSession?.GameMode is CampaignMode campaign) { - campaign.CampaignUI?.UpgradeStore?.RefreshAll(); + campaign.CampaignUI?.UpgradeStore?.RequestRefresh(); campaign.CampaignUI?.CrewManagement?.RefreshPermissions(); } } @@ -2208,8 +2206,8 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SetAutoRestart(autoRestartEnabled, autoRestartTimer); serverSettings.VoiceChatEnabled = voiceChatEnabled; - serverSettings.Voting.AllowSubVoting = allowSubVoting; - serverSettings.Voting.AllowModeVoting = allowModeVoting; + serverSettings.AllowSubVoting = allowSubVoting; + serverSettings.AllowModeVoting = allowModeVoting; if (clientPeer is SteamP2POwnerPeer) { @@ -2240,7 +2238,7 @@ namespace Barotrauma.Networking ChatMessage.ClientRead(inc); break; case ServerNetObject.VOTE: - serverSettings.Voting.ClientRead(inc); + Voting.ClientRead(inc); break; } } @@ -2826,12 +2824,12 @@ namespace Barotrauma.Networking public void Vote(VoteType voteType, object data) { - if (clientPeer == null) return; + if (clientPeer == null) { return; } IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.UPDATE_LOBBY); msg.Write((byte)ClientNetObject.VOTE); - serverSettings.Voting.ClientWrite(msg, voteType, data); + Voting.ClientWrite(msg, voteType, data); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); clientPeer.Send(msg, DeliveryMethod.Reliable); @@ -2847,19 +2845,27 @@ namespace Barotrauma.Networking #region Submarine Change Voting public void InitiateSubmarineChange(SubmarineInfo sub, VoteType voteType) { - if (sub == null) return; - if (serverSettings.Voting.VoteRunning) - { - new GUIMessageBox(TextManager.Get("unabletoinitiateavoteheader"), TextManager.Get("votealreadyactivetext")); - return; - } + if (sub == null) { return; } Vote(voteType, sub); } public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, float timeOut) { - if (info == null || votingInterface != null) return; - votingInterface = new VotingInterface(starter, info, type, timeOut); + if (info == null || votingInterface != null) { return; } + votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, timeOut); + } + #endregion + + #region Money Transfer Voting + public void ShowMoneyTransferVoteInterface(Client starter, Client from, int amount, Client to, float timeOut) + { + if (votingInterface != null) { return; } + if (from == null && to == null) + { + DebugConsole.ThrowError("Tried to initiate a vote for transferring from null to null!"); + return; + } + votingInterface = VotingInterface.CreateMoneyTransferVotingInterface(starter, from, to, amount, timeOut); } #endregion @@ -3103,7 +3109,7 @@ namespace Barotrauma.Networking { if (!gameStarted) return false; - if (!serverSettings.Voting.AllowEndVoting || !HasSpawned) + if (!serverSettings.AllowEndVoting || !HasSpawned) { tickBox.Visible = false; return false; @@ -3322,15 +3328,17 @@ namespace Barotrauma.Networking inGameHUD.DrawManually(spriteBatch); - if (EndVoteCount > 0) + int endVoteCount = Voting.GetVoteCountYes(VoteType.EndRound); + int endVoteMax = Voting.GetVoteCountMax(VoteType.EndRound); + if (endVoteCount > 0) { if (EndVoteTickBox.Visible) { - EndVoteTickBox.Text = $"{endRoundVoteText} {EndVoteCount}/{EndVoteMax}"; + EndVoteTickBox.Text = $"{endRoundVoteText} {endVoteCount}/{endVoteMax}"; } else { - LocalizedString endVoteText = TextManager.GetWithVariables("EndRoundVotes", ("[votes]", EndVoteCount.ToString()), ("[max]", EndVoteMax.ToString())); + LocalizedString endVoteText = TextManager.GetWithVariables("EndRoundVotes", ("[votes]", endVoteCount.ToString()), ("[max]", endVoteMax.ToString())); GUI.DrawString(spriteBatch, EndVoteTickBox.Rect.Center.ToVector2() - GUIStyle.SmallFont.MeasureString(endVoteText) / 2, endVoteText.Value, Color.White, @@ -3498,7 +3506,7 @@ namespace Barotrauma.Networking OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } }; } - else if (serverSettings.Voting.AllowVoteKick && client.AllowKicking) + else if (serverSettings.AllowVoteKick && client.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), TextManager.Get("VoteToKick"), style: "GUIButtonSmall") diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 23458baca..e088e3b15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -2,56 +2,42 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { partial class Voting { - public bool AllowSubVoting - { - get { return allowSubVoting; } - set - { - if (value == allowSubVoting) return; - allowSubVoting = value; - GameMain.NetLobbyScreen.SubList.Enabled = value || - (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - var subVotesLabel = GameMain.NetLobbyScreen.Frame.FindChild("subvotes", true) as GUITextBlock; - subVotesLabel.Visible = value; - var subVisButton = GameMain.NetLobbyScreen.SubVisibilityButton; - subVisButton.RectTransform.AbsoluteOffset - = new Point(value ? (int)(subVotesLabel.TextSize.X + subVisButton.Rect.Width) : 0, 0); + private readonly Dictionary + voteCountYes = new Dictionary(), + voteCountNo = new Dictionary(), + voteCountMax = new Dictionary(); - UpdateVoteTexts(null, VoteType.Sub); - GameMain.NetLobbyScreen.SubList.Deselect(); - } + public int GetVoteCountYes(VoteType voteType) + { + voteCountYes.TryGetValue(voteType, out int value); + return value; } - public bool AllowModeVoting + public int GetVoteCountNo(VoteType voteType) { - get { return allowModeVoting; } - set - { - if (value == allowModeVoting) return; - allowModeVoting = value; - GameMain.NetLobbyScreen.ModeList.Enabled = - value || - (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectMode)); - - GameMain.NetLobbyScreen.Frame.FindChild("modevotes", true).Visible = value; - - // Disable modes that cannot be voted on - foreach (var guiComponent in GameMain.NetLobbyScreen.ModeList.Content.Children) - { - if (guiComponent is GUIFrame frame) - { - frame.CanBeFocused = !allowModeVoting || ((GameModePreset) frame.UserData).Votable; - } - } - - UpdateVoteTexts(null, VoteType.Mode); - GameMain.NetLobbyScreen.ModeList.Deselect(); - } + voteCountNo.TryGetValue(voteType, out int value); + return value; + } + public int GetVoteCountMax(VoteType voteType) + { + voteCountMax.TryGetValue(voteType, out int value); + return value; + } + public void SetVoteCountYes(VoteType voteType, int value) + { + voteCountYes[voteType] = value; + } + public void SetVoteCountNo(VoteType voteType, int value) + { + voteCountNo[voteType] = value; + } + public void SetVoteCountMax(VoteType voteType, int value) + { + voteCountMax[voteType] = value; } public void UpdateVoteTexts(List clients, VoteType voteType) @@ -139,17 +125,15 @@ namespace Barotrauma msg.Write(votedClient.ID); break; case VoteType.StartRound: - if (!(data is bool)) return; + if (!(data is bool)) { return; } msg.Write((bool)data); break; - case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: - if (!VoteRunning) - { - SubmarineInfo voteSub = data as SubmarineInfo; - if (voteSub == null) return; + if (data is SubmarineInfo voteSub) + { + //initiate sub vote msg.Write(true); msg.Write(voteSub.Name); } @@ -159,7 +143,11 @@ namespace Barotrauma msg.Write(false); msg.Write((int)data); } - + break; + case VoteType.TransferMoney: + if (!(data is int)) { return; } + msg.Write(false); //not initiating a vote + msg.Write((int)data); break; } @@ -168,8 +156,8 @@ namespace Barotrauma public void ClientRead(IReadMessage inc) { - AllowSubVoting = inc.ReadBoolean(); - if (allowSubVoting) + GameMain.Client.ServerSettings.AllowSubVoting = inc.ReadBoolean(); + if (GameMain.Client.ServerSettings.AllowSubVoting) { UpdateVoteTexts(null, VoteType.Sub); int votableCount = inc.ReadByte(); @@ -186,8 +174,8 @@ namespace Barotrauma SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes); } } - AllowModeVoting = inc.ReadBoolean(); - if (allowModeVoting) + GameMain.Client.ServerSettings.AllowModeVoting = inc.ReadBoolean(); + if (GameMain.Client.ServerSettings.AllowModeVoting) { UpdateVoteTexts(null, VoteType.Mode); int votableCount = inc.ReadByte(); @@ -199,135 +187,136 @@ namespace Barotrauma SetVoteText(GameMain.NetLobbyScreen.ModeList, mode, votes); } } - AllowEndVoting = inc.ReadBoolean(); - if (AllowEndVoting) + GameMain.Client.ServerSettings.AllowEndVoting = inc.ReadBoolean(); + if (GameMain.Client.ServerSettings.AllowEndVoting) { - GameMain.NetworkMember.EndVoteCount = inc.ReadByte(); - GameMain.NetworkMember.EndVoteMax = inc.ReadByte(); + SetVoteCountYes(VoteType.EndRound, inc.ReadByte()); + SetVoteCountMax(VoteType.EndRound, inc.ReadByte()); } - AllowVoteKick = inc.ReadBoolean(); + GameMain.Client.ServerSettings.AllowVoteKick = inc.ReadBoolean(); - byte subVoteStateByte = inc.ReadByte(); - VoteState subVoteState = VoteState.None; - try - { - subVoteState = (VoteState)subVoteStateByte; - } + byte activeVoteStateByte = inc.ReadByte(); + + VoteState activeVoteState = VoteState.None; + try { activeVoteState = (VoteState)activeVoteStateByte; } catch (System.Exception e) { - DebugConsole.ThrowError("Failed to cast vote type \"" + subVoteStateByte + "\"", e); + DebugConsole.ThrowError("Failed to cast vote type \"" + activeVoteStateByte + "\"", e); } - if (subVoteState != VoteState.None) + if (activeVoteState != VoteState.None) { byte voteTypeByte = inc.ReadByte(); VoteType voteType = VoteType.Unknown; - - try - { - voteType = (VoteType)voteTypeByte; - } + try { voteType = (VoteType)voteTypeByte; } catch (System.Exception e) { DebugConsole.ThrowError("Failed to cast vote type \"" + voteTypeByte + "\"", e); } - if (voteType != VoteType.Unknown) + byte yesClientCount = inc.ReadByte(); + for (int i = 0; i < yesClientCount; i++) { - byte yesClientCount = inc.ReadByte(); - for (int i = 0; i < yesClientCount; i++) - { - byte clientID = inc.ReadByte(); - var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); - matchingClient?.SetVote(voteType, 2); - } - - byte noClientCount = inc.ReadByte(); - for (int i = 0; i < noClientCount; i++) - { - byte clientID = inc.ReadByte(); - var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); - matchingClient?.SetVote(voteType, 1); - } - - GameMain.NetworkMember.SubmarineVoteYesCount = yesClientCount; - GameMain.NetworkMember.SubmarineVoteNoCount = noClientCount; - GameMain.NetworkMember.SubmarineVoteMax = inc.ReadByte(); - - switch (subVoteState) - { - case VoteState.Started: - Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == GameMain.Client.ID); - if (!myClient.InGame) - { - VoteRunning = true; - return; - } - - string subName1 = inc.ReadString(); - SubmarineInfo info = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName1); - - if (info == null) - { - DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); - return; - } - - VoteRunning = true; - byte starterID = inc.ReadByte(); - Client starterClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == starterID); - float timeOut = inc.ReadByte(); - GameMain.Client.ShowSubmarineChangeVoteInterface(starterClient, info, voteType, timeOut); - break; - case VoteState.Running: - // Nothing specific - break; - case VoteState.Passed: - case VoteState.Failed: - VoteRunning = false; - - bool passed = inc.ReadBoolean(); - string subName2 = inc.ReadString(); - SubmarineInfo subInfo = GameMain.Client.ServerSubmarines.FirstOrDefault(s => s.Name == subName2); - - if (subInfo == null) - { - DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); - return; - } - - if (GameMain.Client.VotingInterface != null) - { - GameMain.Client.VotingInterface.EndVote(passed, yesClientCount, noClientCount); - } - else if (GameMain.Client.ConnectedClients.Count > 1) - { - GameMain.NetworkMember.AddChatMessage(VotingInterface.GetSubmarineVoteResultMessage(subInfo, voteType, yesClientCount.ToString(), noClientCount.ToString(), passed).Value, ChatMessageType.Server); - } - - if (passed) - { - int deliveryFee = inc.ReadInt16(); - switch (voteType) - { - case VoteType.PurchaseAndSwitchSub: - GameMain.GameSession.PurchaseSubmarine(subInfo); - GameMain.GameSession.SwitchSubmarine(subInfo, 0); - break; - case VoteType.PurchaseSub: - GameMain.GameSession.PurchaseSubmarine(subInfo); - break; - case VoteType.SwitchSub: - GameMain.GameSession.SwitchSubmarine(subInfo, deliveryFee); - break; - } - - SubmarineSelection.ContentRefreshRequired = true; - } - break; - } + byte clientID = inc.ReadByte(); + var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); + matchingClient?.SetVote(voteType, 2); } - } + + byte noClientCount = inc.ReadByte(); + for (int i = 0; i < noClientCount; i++) + { + byte clientID = inc.ReadByte(); + var matchingClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == clientID); + matchingClient?.SetVote(voteType, 1); + } + byte maxClientCount = inc.ReadByte(); + + SetVoteCountYes(voteType, yesClientCount); + SetVoteCountNo(voteType, noClientCount); + SetVoteCountMax(voteType, maxClientCount); + + switch (activeVoteState) + { + case VoteState.Started: + byte starterID = inc.ReadByte(); + Client starterClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == starterID); + float timeOut = inc.ReadByte(); + + Client myClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == GameMain.Client.ID); + if (!myClient.InGame) { return; } + + switch (voteType) + { + case VoteType.PurchaseSub: + case VoteType.PurchaseAndSwitchSub: + case VoteType.SwitchSub: + string subName1 = inc.ReadString(); + 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); + break; + case VoteType.TransferMoney: + byte fromClientId = inc.ReadByte(); + byte toClientId = inc.ReadByte(); + int transferAmount = inc.ReadInt32(); + + Client fromClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == fromClientId); + Client toClient = GameMain.NetworkMember.ConnectedClients.Find(c => c.ID == toClientId); + GameMain.Client.ShowMoneyTransferVoteInterface(starterClient, fromClient, transferAmount, toClient, timeOut); + break; + } + break; + case VoteState.Running: + // Nothing specific + break; + case VoteState.Passed: + case VoteState.Failed: + bool passed = inc.ReadBoolean(); + + SubmarineInfo subInfo = null; + 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) + { + DebugConsole.ThrowError("Failed to find a matching submarine, vote aborted"); + return; + } + break; + } + + GameMain.Client.VotingInterface?.EndVote(passed, yesClientCount, noClientCount); + + if (passed && subInfo != null) + { + int deliveryFee = inc.ReadInt16(); + switch (voteType) + { + case VoteType.PurchaseAndSwitchSub: + GameMain.GameSession.PurchaseSubmarine(subInfo); + GameMain.GameSession.SwitchSubmarine(subInfo, 0); + break; + case VoteType.PurchaseSub: + GameMain.GameSession.PurchaseSubmarine(subInfo); + break; + case VoteType.SwitchSub: + GameMain.GameSession.SwitchSubmarine(subInfo, deliveryFee); + break; + } + + SubmarineSelection.ContentRefreshRequired = true; + } + break; + } + } GameMain.NetworkMember.ConnectedClients.ForEach(c => c.SetVote(VoteType.StartRound, false)); byte readyClientCount = inc.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs index ff0f4ffc2..c7cd94394 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -1,5 +1,8 @@ +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Barotrauma { @@ -44,5 +47,60 @@ namespace Barotrauma this.newGameContainer = newGameContainer; this.loadGameContainer = loadGameContainer; } + + protected List prevSaveFiles; + protected GUIComponent CreateSaveElement(CampaignMode.SaveInfo saveInfo) + { + if (string.IsNullOrEmpty(saveInfo.FilePath)) + { + DebugConsole.AddWarning("Error when updating campaign load menu: path to a save file was empty.\n" + Environment.StackTrace); + return null; + } + + var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") + { + UserData = saveInfo.FilePath + }; + + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), Path.GetFileNameWithoutExtension(saveInfo.FilePath)) + { + CanBeFocused = false + }; + + if (saveInfo.EnabledContentPackageNames != null && saveInfo.EnabledContentPackageNames.Any()) + { + if (!GameSession.IsCompatibleWithEnabledContentPackages(saveInfo.EnabledContentPackageNames, out LocalizedString errorMsg)) + { + nameText.TextColor = GUIStyle.Red; + saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); + } + } + + prevSaveFiles ??= new List(); + prevSaveFiles.Add(saveInfo); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), + text: saveInfo.SubmarineName, font: GUIStyle.SmallFont) + { + CanBeFocused = false, + UserData = saveInfo.FilePath + }; + + + string saveTimeStr = string.Empty; + if (saveInfo.SaveTime > 0) + { + DateTime time = ToolBox.Epoch.ToDateTime(saveInfo.SaveTime); + saveTimeStr = time.ToString(); + } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), + text: saveTimeStr, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) + { + CanBeFocused = false, + UserData = saveInfo.FilePath + }; + + return saveFrame; + } } } \ 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 e93563192..97e08e561 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -12,7 +12,7 @@ namespace Barotrauma { private GUIButton deleteMpSaveButton; - public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable saveFiles = null) + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, List saveFiles = null) : base(newGameContainer, loadGameContainer) { var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: false) @@ -219,8 +219,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - private List prevSaveFiles; - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -242,73 +241,9 @@ namespace Barotrauma OnSelected = SelectSaveFile }; - foreach (string saveFile in saveFiles) + foreach (CampaignMode.SaveInfo saveInfo in saveFiles) { - if (string.IsNullOrEmpty(saveFile)) - { - DebugConsole.AddWarning("Error when updating campaign load menu: path to a save file was empty.\n" + Environment.StackTrace); - continue; - } - - string fileName = saveFile; - string subName = ""; - string saveTime = ""; - string contentPackageStr = ""; - var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") - { - UserData = saveFile - }; - - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), "") - { - CanBeFocused = false - }; - - bool isCompatible = true; - prevSaveFiles ??= new List(); - - prevSaveFiles?.Add(saveFile); - string[] splitSaveFile = saveFile.Split(';'); - saveFrame.UserData = splitSaveFile[0]; - fileName = Path.GetFileNameWithoutExtension(splitSaveFile[0]); - nameText.Text = fileName; - if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } - if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } - if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } - - if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) - { - DateTime time = ToolBox.Epoch.ToDateTime(unixTime); - saveTime = time.ToString(); - } - if (!string.IsNullOrEmpty(contentPackageStr)) - { - List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out LocalizedString errorMsg)) - { - nameText.TextColor = GUIStyle.Red; - saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); - } - } - if (!isCompatible) - { - nameText.TextColor = GUIStyle.Red; - saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); - } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: subName, font: GUIStyle.SmallFont) - { - CanBeFocused = false, - UserData = fileName - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTime, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) - { - CanBeFocused = false, - UserData = fileName - }; + CreateSaveElement(saveInfo); } saveList.Content.RectTransform.SortChildren((c1, c2) => @@ -380,7 +315,7 @@ namespace Barotrauma EventEditorScreen.AskForConfirmation(header, body, () => { SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.RemoveAll(s => s.StartsWith(saveFile)); + prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile); UpdateLoadMenu(prevSaveFiles.ToList()); return true; }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 9c62c6816..870827d91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -21,7 +21,7 @@ namespace Barotrauma private GUIButton nextButton; private GUILayoutGroup characterInfoColumns; - public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) : base(newGameContainer, loadGameContainer) { UpdateNewGameMenu(submarines); @@ -606,8 +606,7 @@ namespace Barotrauma } } - private List prevSaveFiles; - public void UpdateLoadMenu(IEnumerable saveFiles = null) + public void UpdateLoadMenu(IEnumerable saveFiles = null) { prevSaveFiles?.Clear(); prevSaveFiles = null; @@ -647,32 +646,16 @@ namespace Barotrauma } }; - foreach (string saveFile in saveFiles) + foreach (var saveInfo in saveFiles) { - string fileName = saveFile; - string subName = ""; - string saveTime = ""; - string contentPackageStr = ""; - var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") - { - UserData = saveFile - }; - - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), "") - { - CanBeFocused = false - }; - - bool isCompatible = true; - prevSaveFiles ??= new List(); - - nameText.Text = Path.GetFileNameWithoutExtension(saveFile); - XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); - + var saveFrame = CreateSaveElement(saveInfo); + if (saveFrame == null) { continue; } + + XDocument doc = SaveUtil.LoadGameSessionDoc(saveInfo.FilePath); if (doc?.Root == null) { - DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); - nameText.TextColor = GUIStyle.Red; + DebugConsole.ThrowError("Error loading save file \"" + saveInfo.FilePath + "\". The file may be corrupted."); + saveFrame.GetChild().TextColor = GUIStyle.Red; continue; } if (doc.Root.GetChildElement("multiplayercampaign") != null) @@ -681,44 +664,11 @@ namespace Barotrauma saveList.Content.RemoveChild(saveFrame); continue; } - subName = doc.Root.GetAttributeString("submarine", ""); - saveTime = doc.Root.GetAttributeString("savetime", ""); - isCompatible = SaveUtil.IsSaveFileCompatible(doc); - contentPackageStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", ""); - prevSaveFiles?.Add(saveFile); - if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) + if (!SaveUtil.IsSaveFileCompatible(doc)) { - DateTime time = ToolBox.Epoch.ToDateTime(unixTime); - saveTime = time.ToString(); - } - if (!string.IsNullOrEmpty(contentPackageStr)) - { - List contentPackagePaths = contentPackageStr.Split('|').ToList(); - if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out LocalizedString errorMsg)) - { - nameText.TextColor = GUIStyle.Red; - saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); - } - } - if (!isCompatible) - { - nameText.TextColor = GUIStyle.Red; + saveFrame.GetChild().TextColor = GUIStyle.Red; saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); } - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), - text: subName, font: GUIStyle.SmallFont) - { - CanBeFocused = false, - UserData = fileName - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), - text: saveTime, textAlignment: Alignment.Right, font: GUIStyle.SmallFont) - { - CanBeFocused = false, - UserData = fileName - }; } saveList.Content.RectTransform.SortChildren((c1, c2) => @@ -830,7 +780,7 @@ namespace Barotrauma EventEditorScreen.AskForConfirmation(header, body, () => { SaveUtil.DeleteSave(saveFile); - prevSaveFiles?.RemoveAll(s => s.StartsWith(saveFile)); + prevSaveFiles?.RemoveAll(s => s.FilePath == saveFile); UpdateLoadMenu(prevSaveFiles.ToList()); return true; }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index efbb7b0cc..c1864e3cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -332,7 +333,7 @@ namespace Barotrauma foreach (GUITickBox tickBox in missionTickBoxes) { bool disable = hasMaxMissions && !tickBox.Selected; - tickBox.Enabled = Campaign.AllowedToManageCampaign() && !disable; + tickBox.Enabled = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) && !disable; tickBox.Box.DisabledColor = disable ? tickBox.Box.Color * 0.5f : tickBox.Box.Color * 0.8f; foreach (GUIComponent child in tickBox.Parent.Parent.Children) { @@ -480,7 +481,7 @@ namespace Barotrauma if (GUI.MouseOn == tickBox) { return false; } if (tickBox != null) { - if (Campaign.AllowedToManageCampaign() && tickBox.Enabled) + if (Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) && tickBox.Enabled) { tickBox.Selected = !tickBox.Selected; } @@ -521,10 +522,10 @@ namespace Barotrauma }; tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); tickBox.RectTransform.IsFixedSize = true; - tickBox.Enabled = Campaign.AllowedToManageCampaign(); + tickBox.Enabled = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap); tickBox.OnSelected += (GUITickBox tb) => { - if (!Campaign.AllowedToManageCampaign()) { return false; } + if (!Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; } if (tb.Selected) { @@ -544,7 +545,7 @@ namespace Barotrauma UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation)); if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - Campaign.AllowedToManageCampaign()) + Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { GameMain.Client?.SendCampaignState(); } @@ -665,7 +666,7 @@ namespace Barotrauma return true; }, Enabled = true, - Visible = Campaign.AllowedToEndRound() + Visible = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) }; buttonArea.RectTransform.MinSize = new Point(0, StartButton.RectTransform.MinSize.Y); @@ -702,12 +703,10 @@ namespace Barotrauma { case CampaignMode.InteractionType.Repair: repairHullsButton.Enabled = - (Campaign.PurchasedHullRepairs || Campaign.Wallet.CanAfford(CampaignMode.HullRepairCost)) && - Campaign.AllowedToManageCampaign(); + (Campaign.PurchasedHullRepairs || Campaign.Wallet.CanAfford(CampaignMode.HullRepairCost)); repairHullsButton.GetChild().Selected = Campaign.PurchasedHullRepairs; repairItemsButton.Enabled = - (Campaign.PurchasedItemRepairs || Campaign.Wallet.CanAfford(CampaignMode.ItemRepairCost)) && - Campaign.AllowedToManageCampaign(); + (Campaign.PurchasedItemRepairs || Campaign.Wallet.CanAfford(CampaignMode.ItemRepairCost)); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; if (GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind) @@ -718,8 +717,7 @@ namespace Barotrauma else { replaceShuttlesButton.Enabled = - (Campaign.PurchasedLostShuttles || Campaign.Wallet.CanAfford(CampaignMode.ShuttleReplaceCost)) && - Campaign.AllowedToManageCampaign(); + (Campaign.PurchasedLostShuttles || Campaign.Wallet.CanAfford(CampaignMode.ShuttleReplaceCost)); replaceShuttlesButton.GetChild().Selected = Campaign.PurchasedLostShuttles; } break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 8fb996949..d0c67ae6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -387,11 +387,14 @@ namespace Barotrauma.CharacterEditor contentPackageDropDown.Flash(); return false; } - if (!File.Exists(TexturePath)) + if (SourceCharacter?.SpeciesName != CharacterPrefab.HumanSpeciesName) { - GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red); - texturePathElement.Flash(GUIStyle.Red); - return false; + if (!File.Exists(TexturePath)) + { + GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red); + texturePathElement.Flash(GUIStyle.Red); + return false; + } } var path = Path.GetFileName(TexturePath); if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index be295b3ed..7108479ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -10,11 +10,13 @@ namespace Barotrauma public override sealed void Deselect() { DeselectEditorSpecific(); +#if !DEBUG //reset cheats the player might have used in the editor GameMain.LightManager.LightingEnabled = true; GameMain.LightManager.LosEnabled = true; Hull.EditFire = false; Hull.EditWater = false; +#endif } protected virtual void DeselectEditorSpecific() { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 7fb77b452..f8468ea3c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -1006,13 +1006,12 @@ namespace Barotrauma spriteBatch.End(); } - private void StartGame(SubmarineInfo selectedSub, string saveName, string mapSeed, CampaignSettings settings) + private void StartGame(SubmarineInfo selectedSub, string savePath, string mapSeed, CampaignSettings settings) { - if (string.IsNullOrEmpty(saveName)) return; + if (string.IsNullOrEmpty(savePath)) { return; } var existingSaveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); - - if (existingSaveFiles.Any(s => s == saveName)) + if (existingSaveFiles.Any(s => s.FilePath == savePath)) { new GUIMessageBox(TextManager.Get("SaveNameInUseHeader"), TextManager.Get("SaveNameInUseText")); return; @@ -1045,7 +1044,7 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); - GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.SinglePlayerCampaign, settings, mapSeed); + GameMain.GameSession = new GameSession(selectedSub, savePath, GameModePreset.SinglePlayerCampaign, settings, mapSeed); GameMain.GameSession.CrewManager.CharacterInfos.Clear(); foreach (var characterInfo in campaignSetupUI.CharacterMenus.Select(m => m.CharacterInfo)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index af54e3654..a4cf42674 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1299,7 +1299,7 @@ namespace Barotrauma if (GameMain.Client != null) { - GameMain.Client.ServerSettings.Voting.ResetVotes(GameMain.Client.ConnectedClients); + GameMain.Client.Voting.ResetVotes(GameMain.Client.ConnectedClients); spectateButton.OnClicked = GameMain.Client.SpectateClicked; ReadyToStartBox.OnSelected = GameMain.Client.SetReadyToStart; } @@ -1307,9 +1307,6 @@ namespace Barotrauma roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Recalculate(); - GameMain.NetworkMember.EndVoteCount = 0; - GameMain.NetworkMember.EndVoteMax = 1; - base.Select(); } @@ -1348,9 +1345,9 @@ namespace Barotrauma ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings) && !GameMain.Client.GameStarted; - SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); + SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); ShuttleList.Enabled = ShuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub) && !GameMain.Client.GameStarted; - ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); + ModeList.Enabled = GameMain.Client.ServerSettings.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); @@ -1889,7 +1886,7 @@ namespace Barotrauma } else { - var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { UserData = "classtext", @@ -1907,7 +1904,7 @@ namespace Barotrauma VoteType voteType; if (component.Parent == GameMain.NetLobbyScreen.SubList.Content) { - if (!GameMain.Client.ServerSettings.Voting.AllowSubVoting) + if (!GameMain.Client.ServerSettings.AllowSubVoting) { var selectedSub = component.UserData as SubmarineInfo; if (!selectedSub.RequiredContentPackagesInstalled) @@ -1942,7 +1939,7 @@ namespace Barotrauma } else if (component.Parent == GameMain.NetLobbyScreen.ModeList.Content) { - if (!GameMain.Client.ServerSettings.Voting.AllowModeVoting) + if (!GameMain.Client.ServerSettings.AllowModeVoting) { if (GameMain.Client.HasPermission(ClientPermissions.SelectMode)) { @@ -2384,6 +2381,7 @@ namespace Barotrauma return true; } }; + permissionTick.ToolTip = permissionTick.TextBlock.ToolTip = TextManager.Get("ClientPermission." + permission + ".description"); } var listBoxContainerRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionContainer.RectTransform)) @@ -2478,7 +2476,7 @@ namespace Barotrauma if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient)) { - if (GameMain.Client.ServerSettings.Voting.AllowVoteKick && + if (GameMain.Client.ServerSettings.AllowVoteKick && selectedClient != null && selectedClient.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), @@ -3564,22 +3562,7 @@ namespace Barotrauma if (GameMain.Client.ServerSettings.AllowFileTransfers) { - errorMsg += TextManager.Get("DownloadSubQuestion"); - - var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) - { - UserData = "request" + subName - }; - requestFileBox.Buttons[0].UserData = new string[] { subName, md5Hash }; - requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; - requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => - { - string[] fileInfo = (string[])userdata; - GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo[0], fileInfo[1]); - return true; - }; - requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; + GameMain.Client?.RequestFile(FileTransferType.Submarine, subName, md5Hash); } else { @@ -3596,7 +3579,7 @@ namespace Barotrauma public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData) { - if (GameMain.Client == null) return false; + if (GameMain.Client == null) { return false; } //already downloading the selected sub file if (GameMain.Client.FileReceiver.ActiveTransfers.Any(t => t.FileName == serverSubmarine.Name + ".sub")) @@ -3610,59 +3593,19 @@ namespace Barotrauma return true; } - purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name); + FailedSubInfo fileInfo = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.StringRepresentation); - LocalizedString errorMsg = ""; - if (purchasableSub == null) + switch (deliveryData) { - errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", serverSubmarine.Name) + " "; - } - else if (purchasableSub.MD5Hash?.StringRepresentation == null) - { - errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", serverSubmarine.Name) + " "; - /*GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); - if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; }*/ - } - else - { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", - ("[subname]", purchasableSub.Name), - ("[myhash]", purchasableSub.MD5Hash.ShortRepresentation), - ("[serverhash]", Md5Hash.GetShortHash(serverSubmarine.MD5Hash.StringRepresentation))) + " "; - } - - errorMsg += TextManager.Get("DownloadSubQuestion"); - - //already showing a message about the same sub - if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "request" + serverSubmarine.Name)) - { - return false; - } - - var requestFileBox = new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg, - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }) - { - UserData = "request" + serverSubmarine.Name - }; - requestFileBox.Buttons[0].UserData = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.StringRepresentation); - requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; - requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => - { - FailedSubInfo fileInfo = (FailedSubInfo)userdata; - - if (deliveryData == SubmarineDeliveryData.Owned) - { + case SubmarineDeliveryData.Owned: FailedOwnedSubs.Add(fileInfo); - } - else if (deliveryData == SubmarineDeliveryData.Campaign) - { + break; + case SubmarineDeliveryData.Campaign: FailedCampaignSubs.Add(fileInfo); - } + break; + } - GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo.Name, fileInfo.Hash); - return true; - }; - requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; + GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo.Name, fileInfo.Hash); return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 5ec47b586..bd2b9b4c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -1905,27 +1905,27 @@ namespace Barotrauma toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion); } + int maxIncompatibleToList = 10; + List incompatibleModNames = new List(); for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { - bool listAsIncompatible = false; - if (serverInfo.ContentPackageWorkshopIds[i] == 0) - { - listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i]); - } - else - { - listAsIncompatible = ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation != serverInfo.ContentPackageHashes[i] && - contentPackage.SteamWorkshopId == serverInfo.ContentPackageWorkshopIds[i]); - } + bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(contentPackage => contentPackage.Hash.StringRepresentation == serverInfo.ContentPackageHashes[i]); if (listAsIncompatible) { - if (toolTip != "") toolTip += "\n"; - toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", - ("[contentpackage]", serverInfo.ContentPackageNames[i]), - ("[hash]", Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]))); + incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", + ("[name]", serverInfo.ContentPackageNames[i]), + ("[hash]", Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i])))); + + } + } + if (incompatibleModNames.Any()) + { + toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList)); + if (incompatibleModNames.Count > maxIncompatibleToList) + { + toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); } } - serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.5f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 7e6abbd01..05723864f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -237,7 +237,7 @@ namespace Barotrauma public override Camera Cam => cam; public static XDocument AutoSaveInfo; - private static readonly string autoSavePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves"); + private static readonly string autoSavePath = Path.Combine("Submarines", ".AutoSaves"); private static readonly string autoSaveInfoPath = Path.Combine(autoSavePath, "autosaves.xml"); private static string GetSubDescription() @@ -678,7 +678,7 @@ namespace Barotrauma //----------------------------------------------- - showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.5f), GUI.Canvas) + showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.15f, 0.5f), GUI.Canvas) { MinSize = new Point(190, 0) }) @@ -771,22 +771,26 @@ namespace Barotrauma } foreach (string subcategory in availableSubcategories) { - var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), + var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.15f), subcategoryList.Content.RectTransform), TextManager.Get("subcategory." + subcategory).Fallback(subcategory), font: GUIStyle.SmallFont) { UserData = subcategory, Selected = !IsSubcategoryHidden(subcategory), OnSelected = (GUITickBox obj) => { hiddenSubCategories[(string)obj.UserData] = !obj.Selected; return true; }, }; - if (tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f) + tb.TextBlock.Wrap = true; + } + + GUITextBlock.AutoScaleAndNormalize(subcategoryList.Content.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock)); + foreach (GUIComponent child in subcategoryList.Content.Children) + { + if (child is GUITickBox tb && tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f) { tb.ToolTip = tb.Text; tb.Text = ToolBox.LimitString(tb.Text.Value, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); } } - GUITextBlock.AutoScaleAndNormalize(subcategoryList.Content.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock)); - showEntitiesPanel.RectTransform.NonScaledSize = new Point( (int)(paddedShowEntitiesPanel.RectTransform.Children.Max(c => (int)((c.GUIComponent as GUITickBox)?.TextBlock.TextSize.X ?? 0)) / paddedShowEntitiesPanel.RectTransform.RelativeSize.X), @@ -2933,7 +2937,7 @@ namespace Barotrauma if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn) { deleteBtn.Enabled = userData is SubmarineInfo subInfo - && (GetContentPackageIntrinsicallyTiedToSub(subInfo) != null || Path.GetDirectoryName(subInfo.FilePath) == SubmarineInfo.SavePath); + && GetContentPackageIntrinsicallyTiedToSub(subInfo) != null; } return true; } @@ -3102,8 +3106,9 @@ namespace Barotrauma var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); // set the submarine file path to the "default" value - loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); - loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName").Value; + var unspecifiedFileName = TextManager.Get("UnspecifiedSubFileName"); + loadedSub.Info.FilePath = Path.Combine(ContentPackage.LocalModsDir, unspecifiedFileName.Value, $"{unspecifiedFileName}.sub"); + loadedSub.Info.Name = unspecifiedFileName.Value; try { loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); @@ -3199,8 +3204,7 @@ namespace Barotrauma //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); - bool isInOldSavePath = Path.GetDirectoryName(sub.FilePath) == SubmarineInfo.SavePath; - if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage) && !isInOldSavePath) { return; } + if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; } var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), @@ -3216,10 +3220,6 @@ namespace Barotrauma ContentPackageManager.LocalPackages.Refresh(); ContentPackageManager.EnabledPackages.DisableRemovedMods(); } - else if (isInOldSavePath && File.Exists(sub.FilePath)) - { - File.Delete(sub.FilePath); - } sub.Dispose(); SubmarineInfo.RefreshSavedSubs(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index c809ad75f..823439ace 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -13,7 +13,7 @@ using OpenAL; namespace Barotrauma { - public class SettingsMenu + class SettingsMenu { public static SettingsMenu? Instance { get; private set; } @@ -709,7 +709,9 @@ namespace Barotrauma GUIFrame content = CreateNewContentFrame(Tab.Mods); content.RectTransform.RelativeSize = Vector2.One; - workshopMenu = new WorkshopMenu(content); + workshopMenu = Screen.Selected is MainMenuScreen + ? (WorkshopMenu)new MutableWorkshopMenu(content) + : (WorkshopMenu)new ImmutableWorkshopMenu(content); } private void CreateBottomButtons() @@ -729,7 +731,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { GameSettings.SetCurrentConfig(unsavedConfig); - WorkshopMenu.Apply(); + if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) { mutableWorkshopMenu.Apply(); } GameSettings.SaveCurrentConfig(); mainFrame.Flash(color: GUIStyle.Green); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs similarity index 96% rename from Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs rename to Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs index 93b6b3235..72a4861b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/BBCode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/BBCode.cs @@ -8,9 +8,9 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Steam { - public partial class WorkshopMenu + abstract partial class WorkshopMenu { - private readonly struct BBWord + protected readonly struct BBWord { [Flags] public enum TagType @@ -42,10 +42,10 @@ namespace Barotrauma.Steam } } - private static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]", + protected static readonly Regex bbTagRegex = new Regex(@"\[(.+?)\]", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container) + protected static GUICustomComponent CreateBBCodeElement(string bbCode, GUIListBox container) { Point cachedContainerSize = Point.Zero; List bbWords = new List(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs new file mode 100644 index 000000000..f94a64186 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -0,0 +1,41 @@ +#nullable enable +using Microsoft.Xna.Framework; + +namespace Barotrauma.Steam +{ + sealed class ImmutableWorkshopMenu : WorkshopMenu + { + public ImmutableWorkshopMenu(GUIFrame parent) : base(parent) + { + var mainLayout + = new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.Center), isHorizontal: false); + + Label(mainLayout, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont); + var coreBox = new GUIButton( + NewItemRectT(mainLayout), style: "GUITextBoxNoIcon", text: ContentPackageManager.EnabledPackages.Core!.Name, textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false, + CanBeSelected = false + }; + coreBox.TextBlock.Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f); + + Label(mainLayout, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); + var regularList = new GUIListBox( + NewItemRectT(mainLayout, heightScale: 12f)) + { + OnSelected = (component, o) => false, + HoverCursor = CursorState.Default + }; + foreach (var p in ContentPackageManager.EnabledPackages.Regular) + { + var regularBox = new GUITextBlock( + new RectTransform((1.0f, 0.07f), regularList.Content.RectTransform), text: p.Name) + { + CanBeFocused = false + }; + } + + Label(mainLayout, TextManager.Get("CannotChangeMods"), GUIStyle.Font); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs similarity index 99% rename from Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs rename to Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 5ab0a9d8a..3ae7116ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -12,7 +12,7 @@ using ItemOrPackage = Barotrauma.Either itemOrPackage.TryGet(out ContentPackage package) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs similarity index 97% rename from Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs rename to Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 59b42ef78..a66e87f55 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -10,7 +10,7 @@ using ItemOrPackage = Barotrauma.Either tabContents; + protected readonly GUILayoutGroup tabber; + protected readonly Dictionary tabContents; - private readonly GUIFrame contentFrame; + protected readonly GUIFrame contentFrame; private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); @@ -39,15 +39,17 @@ namespace Barotrauma.Steam private readonly GUIListBox popularModsList; private readonly GUIListBox selfModsList; - public WorkshopMenu(GUIFrame parent) + public MutableWorkshopMenu(GUIFrame parent) : base(parent) { - var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); + var mainLayout + = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false); - tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; + tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true) + { Stretch = true }; tabContents = new Dictionary(); contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); - + CreateInstalledModsTab( out enabledCoreDropdown, out enabledRegularModsList, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs similarity index 99% rename from Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs rename to Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index 720121b09..ec55b9e72 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -15,7 +15,7 @@ using Path = Barotrauma.IO.Path; namespace Barotrauma.Steam { - public partial class WorkshopMenu + sealed partial class MutableWorkshopMenu : WorkshopMenu { private class LocalThumbnail : IDisposable { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs similarity index 79% rename from Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs rename to Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index 9293df567..910fe6d1f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -7,22 +7,22 @@ using Microsoft.Xna.Framework; namespace Barotrauma.Steam { - public partial class WorkshopMenu + abstract partial class WorkshopMenu { - private static RectTransform NewItemRectT(GUILayoutGroup parent, float heightScale = 1.0f) + protected static RectTransform NewItemRectT(GUILayoutGroup parent, float heightScale = 1.0f) => new RectTransform((1.0f, 0.06f * heightScale), parent.RectTransform, Anchor.CenterLeft); - private static void Spacer(GUILayoutGroup parent, float height = 0.03f) + protected static void Spacer(GUILayoutGroup parent, float height = 0.03f) { new GUIFrame(new RectTransform((1.0f, height), parent.RectTransform, Anchor.CenterLeft), style: null); } - private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font, float heightScale = 1.0f) + protected static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font, float heightScale = 1.0f) { return new GUITextBlock(NewItemRectT(parent, heightScale), str, font: font); } - private static GUITextBox ScrollableTextBox(GUILayoutGroup parent, float heightScale, string text) + protected static GUITextBox ScrollableTextBox(GUILayoutGroup parent, float heightScale, string text) { var containingListBox = new GUIListBox(NewItemRectT(parent, heightScale)); var textBox = new GUITextBox( @@ -53,12 +53,12 @@ namespace Barotrauma.Steam return textBox; } - private static GUIDropDown DropdownEnum( + protected static GUIDropDown DropdownEnum( GUILayoutGroup parent, Func textFunc, T currentValue, Action setter) where T : Enum => Dropdown(parent, textFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); - private static GUIDropDown Dropdown( + protected static GUIDropDown Dropdown( GUILayoutGroup parent, Func textFunc, IReadOnlyList values, T currentValue, Action setter, float heightScale = 1.0f) { @@ -67,7 +67,7 @@ namespace Barotrauma.Steam return dropdown; } - private static void SwapDropdownValues( + protected static void SwapDropdownValues( GUIDropDown dropdown, Func textFunc, IReadOnlyList values, T currentValue, Action setter) { @@ -88,10 +88,10 @@ namespace Barotrauma.Steam }; } - private static int Round(float v) => (int)MathF.Round(v); - private static string Percentage(float v) => $"{Round(v * 100)}"; + protected static int Round(float v) => (int)MathF.Round(v); + protected static string Percentage(float v) => $"{Round(v * 100)}"; - private struct ActionCarrier + protected struct ActionCarrier { public readonly Identifier Id; public readonly Action Action; @@ -102,7 +102,7 @@ namespace Barotrauma.Steam } } - private GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action) + protected GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action) => new GUIFrame(new RectTransform(Vector2.Zero, parent.RectTransform), style: null) { UserData = new ActionCarrier(id, action) }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs new file mode 100644 index 000000000..1a3e8f53c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace Barotrauma.Steam +{ + abstract partial class WorkshopMenu + { + public WorkshopMenu(GUIFrame parent) + { + + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 65d8b3408..92e9d579c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.4.0 + 0.17.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index d6beedd53..9f9f372a2 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.4.0 + 0.17.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 539cc7653..8ffe5ccb4 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.4.0 + 0.17.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index dbda5fa53..ab23d88c7 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.4.0 + 0.17.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 883d5184f..3e74a9d79 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.4.0 + 0.17.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 98015ddf0..671dedf34 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -129,7 +129,7 @@ namespace Barotrauma public void ApplyWalletData(Character character) { - character.Wallet = new Wallet(WalletData); + character.Wallet = new Wallet(Option.Some(character), WalletData); } public XElement Save() diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index d8cc3b09b..d509c9ae9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -135,7 +135,7 @@ namespace Barotrauma DebugConsole.NewMessage("Saved campaigns:", Color.White); for (int i = 0; i < saveFiles.Length; i++) { - DebugConsole.NewMessage(" " + i + ". " + saveFiles[i], Color.White); + DebugConsole.NewMessage(" " + i + ". " + saveFiles[i].FilePath, Color.White); } DebugConsole.ShowQuestionPrompt("Select a save file to load (0 - " + (saveFiles.Length - 1) + "):", (string selectedSave) => { @@ -148,7 +148,7 @@ namespace Barotrauma } else { - LoadCampaign(saveFiles[saveIndex]); + LoadCampaign(saveFiles[saveIndex].FilePath); } }); } @@ -166,28 +166,13 @@ namespace Barotrauma /// /// There is a client-side implementation of the method in /// - public bool AllowedToEndRound(Client client) - { - //allow ending the round if the client has permissions, is the owner, the only client in the server, - //or if no-one has permissions - return - client.HasPermission(ClientPermissions.ManageRound) || - client.HasPermission(ClientPermissions.ManageCampaign) || - GameMain.Server.ConnectedClients.Count == 1 || - IsOwner(client) || - GameMain.Server.ConnectedClients.None(c => - c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageRound) || c.HasPermission(ClientPermissions.ManageCampaign))); - } - - /// - /// There is a client-side implementation of the method in - /// - public bool AllowedToManageCampaign(Client client, ClientPermissions permissions = ClientPermissions.ManageCampaign) + 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))); @@ -519,7 +504,7 @@ namespace Barotrauma walletsToCheck.Clear(); walletsToCheck.Add(0, Bank); - foreach (Character character in Mission.GetSalaryEligibleCrew()) + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Player)) { walletsToCheck.Add(character.ID, character.Wallet); } @@ -715,107 +700,102 @@ namespace Barotrauma purchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall)); } - bool allowedToManageCampaign = AllowedToManageCampaign(sender); - if (AllowedToManageCampaign(sender)) + Location location = Map.CurrentLocation; + int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; + int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; + int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; + Wallet personalWallet = GetWallet(sender); + + if (purchasedHullRepairs != PurchasedHullRepairs) { - Location location = Map.CurrentLocation; - int hullRepairCost = location?.GetAdjustedMechanicalCost(HullRepairCost) ?? HullRepairCost; - int itemRepairCost = location?.GetAdjustedMechanicalCost(ItemRepairCost) ?? ItemRepairCost; - int shuttleRetrieveCost = location?.GetAdjustedMechanicalCost(ShuttleReplaceCost) ?? ShuttleReplaceCost; - Wallet personalWallet = GetWallet(sender); - - if (purchasedHullRepairs != PurchasedHullRepairs) + switch (purchasedHullRepairs) { - switch (purchasedHullRepairs) - { - case true when personalWallet.CanAfford(hullRepairCost): - personalWallet.Deduct(hullRepairCost); - PurchasedHullRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); - break; - case false: - PurchasedHullRepairs = false; - personalWallet.Refund(hullRepairCost); - break; - } + case true when personalWallet.CanAfford(hullRepairCost): + personalWallet.Deduct(hullRepairCost); + PurchasedHullRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs"); + break; + case false: + PurchasedHullRepairs = false; + personalWallet.Refund(hullRepairCost); + break; } + } - if (purchasedItemRepairs != PurchasedItemRepairs) + if (purchasedItemRepairs != PurchasedItemRepairs) + { + switch (purchasedItemRepairs) { - switch (purchasedItemRepairs) - { - case true when personalWallet.CanAfford(itemRepairCost): - personalWallet.Deduct(itemRepairCost); - PurchasedItemRepairs = true; - GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); - break; - case false: - PurchasedItemRepairs = false; - personalWallet.Refund(itemRepairCost); - break; - } + case true when personalWallet.CanAfford(itemRepairCost): + personalWallet.Deduct(itemRepairCost); + PurchasedItemRepairs = true; + GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs"); + break; + case false: + PurchasedItemRepairs = false; + personalWallet.Refund(itemRepairCost); + break; } + } - if (purchasedLostShuttles != PurchasedLostShuttles) + if (purchasedLostShuttles != PurchasedLostShuttles) + { + if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { - if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) - { - GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); - } - else if (purchasedLostShuttles && personalWallet.TryDeduct(shuttleRetrieveCost)) - { - PurchasedLostShuttles = true; - GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); - } - else if (!purchasedItemRepairs) - { - PurchasedLostShuttles = false; - personalWallet.Refund(shuttleRetrieveCost); - } + GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } - - if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) + else if (purchasedLostShuttles && personalWallet.TryDeduct(shuttleRetrieveCost)) { - Map.SetLocation(currentLocIndex); + PurchasedLostShuttles = true; + GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle"); } + else if (!purchasedItemRepairs) + { + PurchasedLostShuttles = false; + personalWallet.Refund(shuttleRetrieveCost); + } + } + if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport) + { + Map.SetLocation(currentLocIndex); + } + + if (AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) + { Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex); if (Map.SelectedLocation == null) { Map.SelectRandomLocation(preferUndiscovered: true); } if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } CheckTooManyMissions(Map.CurrentLocation, sender); } - - bool allowedToUseStore = AllowedToManageCampaign(sender, ClientPermissions.CampaignStore); - if (allowedToManageCampaign || allowedToUseStore || AllowedToManageCampaign(sender, ClientPermissions.BuyItems)) + + var prevBuyCrateItems = new Dictionary>(CargoManager.ItemsInBuyCrate); + foreach (var store in prevBuyCrateItems) { - var prevBuyCrateItems = new Dictionary>(CargoManager.ItemsInBuyCrate); - foreach (var store in prevBuyCrateItems) + foreach (var item in store.Value) { - foreach (var item in store.Value) - { - CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); - } - } - foreach (var store in buyCrateItems) - { - foreach (var item in store.Value) - { - CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); - } - } - var prevPurchasedItems = new Dictionary>(CargoManager.PurchasedItems); - foreach (var store in prevPurchasedItems) - { - CargoManager.SellBackPurchasedItems(store.Key, store.Value); - } - foreach (var store in purchasedItems) - { - CargoManager.PurchaseItems(store.Key, store.Value, false, sender); + CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, -item.Quantity, sender); } } + foreach (var store in buyCrateItems) + { + foreach (var item in store.Value) + { + CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); + } + } + var prevPurchasedItems = new Dictionary>(CargoManager.PurchasedItems); + foreach (var store in prevPurchasedItems) + { + CargoManager.SellBackPurchasedItems(store.Key, store.Value); + } + foreach (var store in purchasedItems) + { + CargoManager.PurchaseItems(store.Key, store.Value, false, sender); + } bool allowedToSellSubItems = AllowedToManageCampaign(sender, ClientPermissions.SellSubItems); - if (allowedToManageCampaign || allowedToUseStore || allowedToSellSubItems) + if (allowedToSellSubItems) { var prevSubSellCrateItems = new Dictionary>(CargoManager.ItemsInSellFromSubCrate); foreach (var store in prevSubSellCrateItems) @@ -834,7 +814,7 @@ namespace Barotrauma } } bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); - if (allowedToManageCampaign || allowedToUseStore || (allowedToSellInventoryItems && allowedToSellSubItems)) + if (allowedToSellInventoryItems && allowedToSellSubItems) { // for some reason CargoManager.SoldItem is never cleared by the server, I've added a check to SellItems that ignores all // sold items that are removed so they should be discarded on the next message @@ -867,37 +847,34 @@ namespace Barotrauma bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } - if (allowedToManageCampaign) + foreach (var (prefab, category, _) in purchasedUpgrades) { - foreach (var (prefab, category, _) in purchasedUpgrades) - { - UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); + UpgradeManager.PurchaseUpgrade(prefab, category, client: sender); - // unstable logging - int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); - int level = UpgradeManager.GetUpgradeLevel(prefab, category); - GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); - } - foreach (var purchasedItemSwap in purchasedItemSwaps) + // unstable logging + int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation); + int level = UpgradeManager.GetUpgradeLevel(prefab, category); + GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage); + } + foreach (var purchasedItemSwap in purchasedItemSwaps) + { + if (purchasedItemSwap.ItemToInstall == null) { - if (purchasedItemSwap.ItemToInstall == null) - { - UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); - } - else - { - UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender); - } + UpgradeManager.CancelItemSwap(purchasedItemSwap.ItemToRemove); } - foreach (Item item in Item.ItemList) + else { - if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) - { - UpgradeManager.CancelItemSwap(item); - item.PendingItemSwap = null; - } + UpgradeManager.PurchaseItemSwap(purchasedItemSwap.ItemToRemove, purchasedItemSwap.ItemToInstall, client: sender); } } + foreach (Item item in Item.ItemList) + { + if (item.PendingItemSwap != null && !purchasedItemSwaps.Any(it => it.ItemToRemove == item)) + { + UpgradeManager.CancelItemSwap(item); + item.PendingItemSwap = null; + } + } } public void ServerReadMoney(IReadMessage msg, Client sender) @@ -907,19 +884,26 @@ namespace Barotrauma switch (transfer.Sender) { case Some { Value: var id }: - - if (id != sender.CharacterID && !AllowedToManageCampaign(sender)) { return; } + if (id != sender.CharacterID && !AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) { return; } Wallet wallet = GetWalletByID(id); if (wallet is InvalidWallet) { return; } TransferMoney(wallet); break; - case None _: - if (!AllowedToManageCampaign(sender)) { return; } - - TransferMoney(Bank); + if (!AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) + { + if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) + { + GameMain.Server?.Voting.StartTransferVote(sender, null, transfer.Amount, sender); + } + return; + } + else + { + TransferMoney(Bank); + } break; } @@ -952,10 +936,10 @@ namespace Barotrauma { NetWalletSetSalaryUpdate update = INetSerializableStruct.Read(msg); - if (!AllowedToManageCampaign(sender)) { return; } + if (!AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) { return; } Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); - targetCharacter?.Wallet.SetRewardDistrubiton(update.NewRewardDistribution); + targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution); } public void ServerReadCrew(IReadMessage msg, Client sender) @@ -994,7 +978,7 @@ namespace Barotrauma List hiredCharacters = new List(); CharacterInfo firedCharacter = null; - if (location != null && AllowedToManageCampaign(sender)) + if (location != null && AllowedToManageCampaign(sender, ClientPermissions.ManageHires)) { if (fireCharacter) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs index 6f19e2077..e9c23978c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -18,7 +18,7 @@ namespace Barotrauma private struct RateLimitInfo { public int Requests; - public const int MaxRequests = 5; + public const int MaxRequests = 10; public DateTimeOffset Expiry; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 4373de52d..0e2b53f6e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -130,6 +130,8 @@ namespace Barotrauma.Networking KarmaManager.SelectPreset(serverSettings.KarmaPreset); serverSettings.SetPassword(password); + Voting = new Voting(); + ownerKey = ownKey; ownerSteamId = steamId; @@ -383,23 +385,7 @@ namespace Barotrauma.Networking TraitorManager?.Update(deltaTime); - if (serverSettings.Voting.VoteRunning) - { - Voting.SubVote.Timer += deltaTime; - - if (Voting.SubVote.Timer >= serverSettings.SubmarineVoteTimeout) - { - // Do not take unanswered into account for total - if (SubmarineVoteYesCount / (float)(SubmarineVoteYesCount + SubmarineVoteNoCount) >= serverSettings.SubmarineVoteRequiredRatio) - { - SwitchSubmarine(); - } - else - { - serverSettings.Voting.StopSubmarineVote(false); - } - } - } + Voting.Update(deltaTime); bool isCrewDead = connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); @@ -1073,7 +1059,7 @@ namespace Barotrauma.Networking ChatMessage.ServerRead(inc, c); break; case ClientNetObject.VOTE: - serverSettings.Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c); break; default: return; @@ -1220,7 +1206,7 @@ namespace Barotrauma.Networking entityEventManager.Read(inc, c); break; case ClientNetObject.VOTE: - serverSettings.Voting.ServerRead(inc, c); + Voting.ServerRead(inc, c); break; case ClientNetObject.SPECTATING_POS: c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); @@ -1309,17 +1295,11 @@ namespace Barotrauma.Networking var mpCampaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (command == ClientPermissions.ManageRound && mpCampaign != null) { - if (!mpCampaign.AllowedToEndRound(sender)) - { - return; - } + //do nothing, ending campaign rounds is checked in more detail below } else if (command == ClientPermissions.ManageCampaign && mpCampaign != null) { - if (!mpCampaign.AllowedToManageCampaign(sender)) - { - return; - } + //do nothing, campaign permissions are checked in more detail in MultiplayerCampaign.ServerRead } else if (!sender.HasPermission(command)) { @@ -1381,21 +1361,26 @@ namespace Barotrauma.Networking bool end = inc.ReadBoolean(); if (end) { - bool save = inc.ReadBoolean(); - if (gameStarted) + if (mpCampaign == null || + mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) || + mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign)) { - Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); - if (mpCampaign != null && Level.IsLoadedOutpost && save) + bool save = inc.ReadBoolean(); + if (gameStarted) { - mpCampaign.SavePlayers(); - GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); - SaveUtil.SaveGame(GameMain.GameSession.SavePath); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); + if (mpCampaign != null && Level.IsLoadedOutpost && save) + { + mpCampaign.SavePlayers(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + else + { + save = false; + } + EndGame(wasSaved: save); } - else - { - save = false; - } - EndGame(wasSaved: save); } } else @@ -1403,14 +1388,17 @@ namespace Barotrauma.Networking bool continueCampaign = inc.ReadBoolean(); if (mpCampaign != null && mpCampaign.GameOver || continueCampaign) { - MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + if (mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) + { + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + } } else if (!gameStarted && !initiatedStartGame) { Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); StartGame(); } - else if (mpCampaign != null) + else if (mpCampaign != null && (mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) { var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); //don't force location if we've teleported @@ -1473,34 +1461,20 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier == "multiplayercampaign") { - string[] saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false).ToArray(); - for (int i = 0; i < saveFiles.Length; i++) - { - XDocument doc = SaveUtil.LoadGameSessionDoc(saveFiles[i]); - if (doc?.Root != null) - { - saveFiles[i] = - string.Join(";", - saveFiles[i].Replace(';', ' '), - doc.Root.GetAttributeStringUnrestricted("submarine", ""), - doc.Root.GetAttributeStringUnrestricted("savetime", ""), - doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", "")); - } - } - + const int MaxSaves = 255; + var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false); IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO); - msg.Write((UInt16)saveFiles.Count()); - foreach (string saveFile in saveFiles) + msg.Write((byte)Math.Min(saveInfos.Count, MaxSaves)); + for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++) { - msg.Write(saveFile); + msg.Write(saveInfos[i]); } - serverPeer.Send(msg, sender.Connection, DeliveryMethod.Reliable); } break; case ClientPermissions.ManageCampaign: - (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.ServerRead(inc, sender); + mpCampaign?.ServerRead(inc, sender); break; case ClientPermissions.ConsoleCommands: { @@ -1900,8 +1874,8 @@ namespace Barotrauma.Networking outmsg.Write(selectedShuttle.Name); outmsg.Write(selectedShuttle.MD5Hash.ToString()); - outmsg.Write(serverSettings.Voting.AllowSubVoting); - outmsg.Write(serverSettings.Voting.AllowModeVoting); + outmsg.Write(serverSettings.AllowSubVoting); + outmsg.Write(serverSettings.AllowModeVoting); outmsg.Write(serverSettings.VoiceChatEnabled); @@ -2036,9 +2010,9 @@ namespace Barotrauma.Networking SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; SubmarineInfo selectedSub; - if (serverSettings.Voting.AllowSubVoting) + if (serverSettings.AllowSubVoting) { - selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); + selectedSub = Voting.HighestVoted(VoteType.Sub, connectedClients); if (selectedSub == null) { selectedSub = GameMain.NetLobbyScreen.SelectedSub; } } else @@ -2051,7 +2025,7 @@ namespace Barotrauma.Networking return false; } - GameModePreset selectedMode = serverSettings.Voting.HighestVoted(VoteType.Mode, connectedClients); + GameModePreset selectedMode = Voting.HighestVoted(VoteType.Mode, connectedClients); if (selectedMode == null) { selectedMode = GameMain.NetLobbyScreen.SelectedMode; } if (selectedMode == null) @@ -2455,11 +2429,8 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Running; - if (GameMain.Server?.ServerSettings?.Voting != null) - { - GameMain.Server.ServerSettings.Voting.ResetVotes(GameMain.Server.ConnectedClients); - } - + Voting?.ResetVotes(GameMain.Server.ConnectedClients); + GameMain.GameScreen.Select(); Log("Round started.", ServerLog.MessageType.ServerMessage); @@ -3233,23 +3204,24 @@ namespace Barotrauma.Networking serverPeer.Send(msg, transfer.Connection, DeliveryMethod.ReliableOrdered); } - public void UpdateVoteStatus() + public void UpdateVoteStatus(bool checkActiveVote = true) { - if (connectedClients.Count == 0) return; + if (connectedClients.Count == 0) { return; } - if (serverSettings.Voting.VoteRunning) + 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 (SubmarineVoteNoCount / (float)SubmarineVoteMax > 1f - serverSettings.SubmarineVoteRequiredRatio) + if (no / (float)max > 1f - serverSettings.VoteRequiredRatio) { - serverSettings.Voting.StopSubmarineVote(false); - return; // Update will be re-sent via StopSubmarineVote + Voting.ActiveVote.Finish(Voting, passed: false); } - else if (SubmarineVoteYesCount / (float)SubmarineVoteMax >= serverSettings.SubmarineVoteRequiredRatio) + else if (yes / max >= serverSettings.VoteRequiredRatio) { - SwitchSubmarine(); - return; // Update will be re-sent via StopSubmarineVote - } + Voting.ActiveVote.Finish(Voting, passed: true); + } } Client.UpdateKickVotes(connectedClients); @@ -3279,10 +3251,12 @@ namespace Barotrauma.Networking SendVoteStatus(connectedClients); - if (serverSettings.Voting.AllowEndVoting && EndVoteMax > 0 && - ((float)EndVoteCount / (float)EndVoteMax) >= serverSettings.EndVoteRequiredRatio) + int endVoteCount = ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound)); + int endVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned); + if (serverSettings.AllowEndVoting && endVoteMax > 0 && + ((float)endVoteCount / (float)endVoteMax) >= serverSettings.EndVoteRequiredRatio) { - Log("Ending round by votes (" + EndVoteCount + "/" + (EndVoteMax - EndVoteCount) + ")", ServerLog.MessageType.ServerMessage); + Log("Ending round by votes (" + endVoteCount + "/" + (endVoteMax - endVoteCount) + ")", ServerLog.MessageType.ServerMessage); EndGame(wasSaved: false); } } @@ -3294,7 +3268,7 @@ namespace Barotrauma.Networking IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.UPDATE_LOBBY); msg.Write((byte)ServerNetObject.VOTE); - serverSettings.Voting.ServerWrite(msg); + Voting.ServerWrite(msg); msg.Write((byte)ServerNetObject.END_OF_MESSAGE); foreach (var c in recipients) @@ -3303,11 +3277,13 @@ namespace Barotrauma.Networking } } - private void SwitchSubmarine() + public void SwitchSubmarine() { - SubmarineInfo targetSubmarine = Voting.SubVote.Sub; - VoteType voteType = Voting.SubVote.VoteType; - Client starter = Voting.SubVote.VoteStarter; + if (!(Voting.ActiveVote is Voting.SubmarineVote subVote)) { return; } + + SubmarineInfo targetSubmarine = subVote.Sub; + VoteType voteType = Voting.ActiveVote.VoteType; + Client starter = Voting.ActiveVote.VoteStarter; int deliveryFee = 0; switch (voteType) @@ -3318,7 +3294,7 @@ namespace Barotrauma.Networking GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); break; case VoteType.SwitchSub: - deliveryFee = Voting.SubVote.DeliveryFee; + deliveryFee = subVote.DeliveryFee; break; default: return; @@ -3326,10 +3302,10 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); + GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee, starter); } - serverSettings.Voting.StopSubmarineVote(true); + Voting.StopSubmarineVote(true); } public void UpdateClientPermissions(Client client) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 5580a12fe..6bf8c9216 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -312,8 +312,8 @@ namespace Barotrauma.Networking AutoRestart = doc.Root.GetAttributeBool("autorestart", false); - Voting.AllowSubVoting = SubSelectionMode == SelectionMode.Vote; - Voting.AllowModeVoting = ModeSelectionMode == SelectionMode.Vote; + AllowSubVoting = SubSelectionMode == SelectionMode.Vote; + AllowModeVoting = ModeSelectionMode == SelectionMode.Vote; selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index f6d9cf671..985bcdf3c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -7,59 +7,161 @@ namespace Barotrauma { partial class Voting { - public bool AllowSubVoting + public interface IVote { - get { return allowSubVoting; } - set { allowSubVoting = value; } - } - public bool AllowModeVoting - { - get { return allowModeVoting; } - set { allowModeVoting = value; } - } + public Client VoteStarter { get; } + public VoteType VoteType { get; } + public float Timer { get; set; } - public struct SubmarineVote + public VoteState State { get; set; } + + public void Finish(Voting voting, bool passed); + } + + public class SubmarineVote : IVote { - public Client VoteStarter; + public Client VoteStarter { get; } + public VoteType VoteType { get; } + public float Timer { get; set; } + + public VoteState State { get; set; } + public SubmarineInfo Sub; - public VoteType VoteType; - public float Timer; public int DeliveryFee; - public VoteState State; + + public SubmarineVote(Client starter, SubmarineInfo subInfo, int deliveryFee, VoteType voteType) + { + Sub = subInfo; + DeliveryFee = deliveryFee; + VoteType = voteType; + State = VoteState.Started; + VoteStarter = starter; + } + + public void Finish(Voting voting, bool passed) + { + if (passed) + { + GameMain.Server?.SwitchSubmarine(); + } + voting.StopSubmarineVote(passed); + } } - public static SubmarineVote SubVote; + public static IVote ActiveVote; + + public class TransferVote : IVote + { + public Client VoteStarter { get; } + public VoteType VoteType { get; } + public float Timer { get; set; } + + public VoteState State { get; set; } + + //null = bank + public readonly Client From, To; + public readonly int TransferAmount; + + public TransferVote(Client starter, Client from, int transferAmount, Client to) + { + VoteStarter = starter; + From = from; + To = to; + TransferAmount = transferAmount; + State = VoteState.Started; + VoteType = VoteType.TransferMoney; + } + + public void Finish(Voting voting, bool passed) + { + if (passed) + { + Wallet fromWallet = From == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : From.Character?.Wallet; + if (fromWallet.TryDeduct(TransferAmount)) + { + Wallet toWallet = To == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : To.Character?.Wallet; + toWallet.Give(TransferAmount); + } + } + voting.StopMoneyTransferVote(passed); + } + } + + private static readonly Queue pendingVotes = new Queue(); private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) { - SubVote.Sub = subInfo; - SubVote.DeliveryFee = voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0; - SubVote.VoteType = voteType; - SubVote.State = VoteState.Started; - SubVote.VoteStarter = sender; - VoteRunning = true; + var subVote = new SubmarineVote( + sender, + subInfo, + voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, + voteType); + StartOrEnqueueVote(subVote); sender.SetVote(voteType, 2); + GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } public void StopSubmarineVote(bool passed) { - VoteRunning = false; - SubVote.State = passed ? VoteState.Passed : VoteState.Failed; + if (!(ActiveVote is SubmarineVote)) { return; } + StopActiveVote(passed); + } - GameMain.Server.UpdateVoteStatus(); + public void StopMoneyTransferVote(bool passed) + { + if (!(ActiveVote is TransferVote)) { return; } + StopActiveVote(passed); + } + + public void StopActiveVote(bool passed) + { + ActiveVote.State = passed ? VoteState.Passed : VoteState.Failed; + GameMain.Server.UpdateVoteStatus(checkActiveVote: false); - GameMain.NetworkMember.SubmarineVoteYesCount = GameMain.NetworkMember.SubmarineVoteNoCount = GameMain.NetworkMember.SubmarineVoteMax = 0; for (int i = 0; i < GameMain.NetworkMember.ConnectedClients.Count; i++) { - GameMain.NetworkMember.ConnectedClients[i].SetVote(SubVote.VoteType, 0); + GameMain.NetworkMember.ConnectedClients[i].SetVote(ActiveVote.VoteType, 0); } - SubVote.Sub = null; - SubVote.DeliveryFee = 0; - SubVote.VoteType = VoteType.Unknown; - SubVote.Timer = 0.0f; - SubVote.State = VoteState.None; - SubVote.VoteStarter = null; + ActiveVote = null; + if (pendingVotes.Any()) + { + ActiveVote = pendingVotes.Dequeue(); + } + } + + public void StartTransferVote(Client starter, Client from, int transferAmount, Client to) + { + StartOrEnqueueVote(new TransferVote(starter, from, transferAmount, to)); + starter.SetVote(VoteType.TransferMoney, 2); + GameMain.Server.UpdateVoteStatus(checkActiveVote: false); + } + + private void StartOrEnqueueVote(IVote vote) + { + if (ActiveVote == null) + { + ActiveVote = vote; + } + else + { + pendingVotes.Enqueue(vote); + } + } + + public void Update(float deltaTime) + { + if (ActiveVote == null) { return; } + + ActiveVote.Timer += deltaTime; + + if (ActiveVote.Timer >= GameMain.NetworkMember.ServerSettings.VoteTimeout) + { + // 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); + } } public void ServerRead(IReadMessage inc, Client sender) @@ -97,10 +199,6 @@ namespace Barotrauma case VoteType.EndRound: if (!sender.HasSpawned) { return; } sender.SetVote(voteType, inc.ReadBoolean()); - - GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound)); - GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned); - break; case VoteType.Kick: byte kickedClientID = inc.ReadByte(); @@ -126,24 +224,34 @@ namespace Barotrauma case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseSub: case VoteType.SwitchSub: + case VoteType.TransferMoney: bool startVote = inc.ReadBoolean(); if (startVote) { - 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 (voteType == VoteType.TransferMoney) { - StartSubmarineVote(subInfo, voteType, sender); + 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))); + } + 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))) + { + StartSubmarineVote(subInfo, voteType, sender); + } } } else { sender.SetVote(voteType, (int)inc.ReadByte()); } - - GameMain.Server.SubmarineVoteYesCount = GameMain.Server.ConnectedClients.Count(c => c.GetVote(SubVote.VoteType) == 2); - GameMain.Server.SubmarineVoteNoCount = GameMain.Server.ConnectedClients.Count(c => c.GetVote(SubVote.VoteType) == 1); - GameMain.Server.SubmarineVoteMax = GameMain.Server.ConnectedClients.Count(c => c.InGame); break; } @@ -154,10 +262,10 @@ namespace Barotrauma public void ServerWrite(IWriteMessage msg) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } - msg.Write(allowSubVoting); - if (allowSubVoting) + msg.Write(GameMain.Server.ServerSettings.AllowSubVoting); + if (GameMain.Server.ServerSettings.AllowSubVoting) { IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Sub, GameMain.Server.ConnectedClients); msg.Write((byte)voteList.Count); @@ -167,8 +275,8 @@ namespace Barotrauma msg.Write(vote.Key.Name); } } - msg.Write(AllowModeVoting); - if (allowModeVoting) + msg.Write(GameMain.Server.ServerSettings.AllowModeVoting); + if (GameMain.Server.ServerSettings.AllowModeVoting) { IReadOnlyDictionary voteList = GetVoteCounts(VoteType.Mode, GameMain.Server.ConnectedClients); msg.Write((byte)voteList.Count); @@ -178,60 +286,78 @@ namespace Barotrauma msg.Write(vote.Key.Identifier); } } - msg.Write(AllowEndVoting); - if (AllowEndVoting) + msg.Write(GameMain.Server.ServerSettings.AllowEndVoting); + if (GameMain.Server.ServerSettings.AllowEndVoting) { msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote(VoteType.EndRound))); msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.HasSpawned)); } - msg.Write(AllowVoteKick); + msg.Write(GameMain.Server.ServerSettings.AllowVoteKick); - msg.Write((byte)SubVote.State); - if (SubVote.State != VoteState.None) + msg.Write((byte)(ActiveVote?.State ?? VoteState.None)); + if (ActiveVote != null) { - msg.Write((byte)SubVote.VoteType); - - if (SubVote.VoteType != VoteType.Unknown) - { - var yesClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote(SubVote.VoteType) == 2); + 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); foreach (Client c in yesClients) { msg.Write(c.ID); } - var noClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote(SubVote.VoteType) == 1); + var noClients = GameMain.Server.ConnectedClients.FindAll(c => c.InGame && c.GetVote(ActiveVote.VoteType) == 1); msg.Write((byte)noClients.Count); foreach (Client c in noClients) { msg.Write(c.ID); } - msg.Write((byte)GameMain.Server.SubmarineVoteMax); + msg.Write((byte)GameMain.Server.ConnectedClients.Count(c => c.InGame)); - switch (SubVote.State) + switch (ActiveVote.State) { case VoteState.Started: - msg.Write(SubVote.Sub.Name); - msg.Write(SubVote.VoteStarter.ID); - msg.Write((byte)GameMain.Server.ServerSettings.SubmarineVoteTimeout); + msg.Write(ActiveVote.VoteStarter.ID); + msg.Write((byte)GameMain.Server.ServerSettings.VoteTimeout); + + switch (ActiveVote.VoteType) + { + case VoteType.PurchaseSub: + case VoteType.PurchaseAndSwitchSub: + case VoteType.SwitchSub: + msg.Write((ActiveVote as SubmarineVote).Sub.Name); + break; + case VoteType.TransferMoney: + var transferVote = (ActiveVote as TransferVote); + msg.Write(transferVote.From?.ID ?? 0); + msg.Write(transferVote.To?.ID ?? 0); + msg.Write(transferVote.TransferAmount); + break; + } + break; case VoteState.Running: // Nothing specific break; case VoteState.Passed: case VoteState.Failed: - msg.Write(SubVote.State == VoteState.Passed); - msg.Write(SubVote.Sub.Name); - if (SubVote.State == VoteState.Passed) + msg.Write(ActiveVote.State == VoteState.Passed); + switch (ActiveVote.VoteType) { - msg.Write((short)SubVote.DeliveryFee); + case VoteType.PurchaseSub: + case VoteType.PurchaseAndSwitchSub: + case VoteType.SwitchSub: + msg.Write((ActiveVote as SubmarineVote).Sub.Name); + msg.Write((short)(ActiveVote as SubmarineVote).DeliveryFee); + break; } break; - } + } } - } + } var readyClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote(VoteType.StartRound)); msg.Write((byte)readyClients.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index aaccf295c..e746fac64 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -192,7 +192,7 @@ namespace Barotrauma public override void Select() { base.Select(); - GameMain.Server.ServerSettings.Voting.ResetVotes(GameMain.Server.ConnectedClients); + GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients); if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 96429c964..86864cc3c 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.4.0 + 0.17.5.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index 0bad02e51..efff061b6 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -8,7 +8,7 @@ + permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney"> diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index c63af2b86..4d307d4c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -130,7 +130,7 @@ namespace Barotrauma } } - private Wallet wallet = new Wallet(); + private Wallet wallet; public Wallet Wallet { @@ -1055,8 +1055,13 @@ namespace Barotrauma return newCharacter; } + private Character(Submarine submarine, ushort id): base(submarine, id) + { + wallet = new Wallet(Option.Some(this)); + } + protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) - : base(null, id) + : this(null, id) { this.Seed = seed; this.Prefab = prefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 0758b7042..70f3ebdaf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -172,11 +172,21 @@ namespace Barotrauma SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ?? ContentPackageManager.CorePackages.First()); } - SetRegular(Regular - .Select(p => ContentPackageManager.RegularPackages.Contains(p) - ? p - : ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 => p2.SteamWorkshopId == p.SteamWorkshopId)) - .ToArray()); + + List newRegular = new List(); + foreach (var p in Regular) + { + if (ContentPackageManager.RegularPackages.Contains(p)) + { + newRegular.Add(p); + } + else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 + => p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP) + { + newRegular.Add(newP); + } + } + SetRegular(newRegular); } public static void BackUp() diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index 5e378bcb0..5711e7769 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -115,6 +115,24 @@ namespace Barotrauma } public void Remove() => Element.Remove(); + + public override bool Equals(object? obj) + { + return obj is ContentXElement element && this == element; + } + + public override int GetHashCode() + { + return HashCode.Combine(ContentPackage, Element); + } + + public static bool operator ==(in ContentXElement? a, in ContentXElement? b) + { + return a?.ContentPackage == b?.ContentPackage && a?.Element == b?.Element; + } + + public static bool operator !=(in ContentXElement? a, in ContentXElement? b) => + !(a == b); } public static class ContentXElementExtensions diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 6bba12392..94123f36e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using System; + +namespace Barotrauma { public enum TransitionMode { @@ -146,4 +148,11 @@ AlwaysStayConscious, } + [Flags] + public enum CharacterType + { + Bot = 0b01, + Player = 0b10, + Both = Bot | Player + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 413e835e1..dc062b81e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -350,7 +350,7 @@ namespace Barotrauma float difficultyMultiplier = 1 + level.Difficulty / 100f; baseExperienceGain *= difficultyMultiplier; - IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); + IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); @@ -380,7 +380,7 @@ namespace Barotrauma GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value); #if SERVER - totalReward = DistributeRewardsToCrew(GetSalaryEligibleCrew(), totalReward); + totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward); #endif if (totalReward > 0) { @@ -436,18 +436,6 @@ namespace Barotrauma } #endif - public static IEnumerable GetSalaryEligibleCrew() - { - if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return Array.Empty(); } - - IEnumerable characters = crewManager.GetCharacters(); -#if SERVER - return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead).Concat(characters); -#elif CLIENT - return characters; -#endif - } - public static int GetRewardDistibutionSum(IEnumerable crew, int rewardDistribution = 0) => crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 192830b91..0ac70c5c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -95,7 +95,7 @@ namespace Barotrauma public readonly bool IsSideObjective; - public readonly bool RequireWreck; + public readonly bool RequireWreck, RequireRuin; /// /// The mission can only be received when travelling from a location of the first type to a location of the second type @@ -152,6 +152,7 @@ namespace Barotrauma AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); RequireWreck = element.GetAttributeBool("requirewreck", false); + RequireRuin = element.GetAttributeBool("requireruin", false); Commonness = element.GetAttributeInt("commonness", 1); if (element.GetAttribute("difficulty") != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 23a121111..3d60228e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -226,6 +226,7 @@ namespace Barotrauma public void SetPurchasedItems(Dictionary> purchasedItems) { + if (purchasedItems.Count == 0 && PurchasedItems.Count == 0) { return; } PurchasedItems.Clear(); foreach (var entry in purchasedItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 3e33d0669..8a70b39e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -64,7 +64,7 @@ namespace Barotrauma if (reputationChange > 0f) { float reputationGainMultiplier = 1f; - foreach (Character character in GameSession.GetSessionCrewCharacters()) + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { reputationGainMultiplier += character.GetStatValue(StatTypes.ReputationGainMultiplier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index abd84ef03..e10894b20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -6,6 +6,7 @@ namespace Barotrauma { internal readonly struct WalletChangedEvent { + public readonly Option Owner; public readonly Wallet Wallet; public readonly WalletInfo Info; public readonly WalletChangedData ChangedData; @@ -15,6 +16,7 @@ namespace Barotrauma Wallet = wallet; Info = info; ChangedData = changedData; + Owner = wallet.Owner; } } @@ -109,6 +111,8 @@ namespace Barotrauma // ReSharper disable ValueParameterNotUsed internal sealed class InvalidWallet : Wallet { + public InvalidWallet(): base(Option.None()) { } + public override int Balance { get => 0; @@ -132,6 +136,8 @@ namespace Barotrauma AttrubuteNameRewardDistribution = "rewarddistribution", SaveElementName = "Wallet"; + public readonly Option Owner; + private int balance; public virtual int Balance @@ -148,9 +154,12 @@ namespace Barotrauma set => rewardDistribution = ClampRewardDistribution(value); } - public Wallet() { } + public Wallet(Option owner) + { + Owner = owner; + } - public Wallet(XElement element) + public Wallet(Option owner, XElement element): this(owner) { balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0)); rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0)); @@ -185,7 +194,7 @@ namespace Barotrauma SettingsChanged(balanceChanged: Option.Some(-price), rewardChanged: Option.None()); } - public void SetRewardDistrubiton(int value) + public void SetRewardDistribution(int value) { int oldValue = RewardDistribution; RewardDistribution = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 64bd03cb5..5dbf55fa0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -53,7 +53,7 @@ namespace Barotrauma public int GetAddedMissionCount() { int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters()) + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) { count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); } @@ -68,6 +68,15 @@ namespace Barotrauma abstract partial class CampaignMode : GameMode { + [NetworkSerialize] + public struct SaveInfo : INetSerializableStruct + { + public string FilePath; + public int SaveTime; + public string SubmarineName; + public string[] EnabledContentPackageNames; + } + public const int MaxMoney = int.MaxValue / 2; //about 1 billion public const int InitialMoney = 8500; @@ -180,13 +189,37 @@ namespace Barotrauma protected CampaignMode(GameModePreset preset) : base(preset) { - Bank = new Wallet + Bank = new Wallet(Option.None()) { Balance = InitialMoney }; CargoManager = new CargoManager(this); MedicalClinic = new MedicalClinic(this); + Identifier messageIdentifier = new Identifier("money"); + +#if CLIENT + OnMoneyChanged.RegisterOverwriteExisting(new Identifier("CampaignMoneyChangeNotification"), e => + { + if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } + + bool isGain = changed > 0; + + Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; + + switch (e.Owner) + { + case Some { Value: var owner}: + owner.AddMessage(FormatMessage(), clr, playSound: Character.Controlled == owner, messageIdentifier, changed); + break; + case None _ when IsSinglePlayer: + Character.Controlled?.AddMessage(FormatMessage(), clr, playSound: true, messageIdentifier, changed); + break; + } + + string FormatMessage() => TextManager.GetWithVariable(isGain ? "moneygainformat" : "moneyloseformat", "[money]", TextManager.FormatCurrency(Math.Abs(changed))).ToString(); + }); +#endif } public virtual Wallet GetWallet(Client client = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 970f2eaff..781b6599e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -167,7 +167,7 @@ namespace Barotrauma LoadStats(subElement); break; case Wallet.LowerCaseSaveElementName: - Bank = new Wallet(subElement); + Bank = new Wallet(Option.None(), subElement); break; #if SERVER case "savedexperiencepoints": @@ -183,7 +183,7 @@ namespace Barotrauma int oldMoney = element.GetAttributeInt("money", 0); if (oldMoney > 0) { - Bank = new Wallet + Bank = new Wallet(Option.None()) { Balance = oldMoney }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index e6fc1f3e8..ac1b2e40b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -735,14 +735,40 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime); - public static IEnumerable GetSessionCrewCharacters() + /// + /// Returns a list of crew characters currently in the game with a given filter. + /// + /// Character type filter + /// + /// + /// In singleplayer mode the CharacterType.Player returns the currently controlled player. + /// + public static ImmutableHashSet GetSessionCrewCharacters(CharacterType type) { + if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet.Empty; } + + IEnumerable players; + IEnumerable bots; + HashSet characters = new HashSet(); + #if SERVER - return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); -#else - if (GameMain.GameSession?.CrewManager is null) { return Enumerable.Empty(); } - return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null && !c.IsDead); + players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); + bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer); +#elif CLIENT + players = crewManager.GetCharacters().Where(c => c.IsPlayer); + bots = crewManager.GetCharacters().Where(c => c.IsBot); #endif + if (type.HasFlag(CharacterType.Bot)) + { + foreach (Character bot in bots) { characters.Add(bot); } + } + + if (type.HasFlag(CharacterType.Player)) + { + foreach (Character player in players) { characters.Add(player); } + } + + return characters.ToImmutableHashSet(); } public void EndRound(string endMessage, List? traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) @@ -754,7 +780,7 @@ namespace Barotrauma try { - ImmutableArray crewCharacters = GetSessionCrewCharacters().ToImmutableArray(); + ImmutableHashSet crewCharacters = GetSessionCrewCharacters(CharacterType.Both); int prevMoney = GetAmountOfMoney(crewCharacters); @@ -898,7 +924,7 @@ namespace Barotrauma } } - foreach (Character c in GetSessionCrewCharacters()) + foreach (Character c in GetSessionCrewCharacters(CharacterType.Both)) { foreach (var itemSelectedDuration in c.ItemSelectedDurations) { @@ -948,25 +974,25 @@ namespace Barotrauma #endif } - public static bool IsCompatibleWithEnabledContentPackages(IList contentPackagePaths, out LocalizedString errorMsg) + public static bool IsCompatibleWithEnabledContentPackages(IList contentPackageNames, out LocalizedString errorMsg) { errorMsg = ""; //no known content packages, must be an older save file - if (!contentPackagePaths.Any()) { return true; } + if (!contentPackageNames.Any()) { return true; } List missingPackages = new List(); - foreach (string packagePath in contentPackagePaths) + foreach (string packageName in contentPackageNames) { - if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.Path == packagePath)) + if (!ContentPackageManager.EnabledPackages.All.Any(cp => cp.NameMatches(packageName))) { - missingPackages.Add(packagePath); + missingPackages.Add(packageName); } } List excessPackages = new List(); foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All) { if (!cp.HasMultiplayerSyncedContent) { continue; } - if (!contentPackagePaths.Any(p => p == cp.Path)) + if (!contentPackageNames.Any(p => cp.NameMatches(p))) { excessPackages.Add(cp.Name); } @@ -976,9 +1002,9 @@ namespace Barotrauma if (missingPackages.Count == 0 && missingPackages.Count == 0) { var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray(); - for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++) + for (int i = 0; i < contentPackageNames.Count && i < enabledPackages.Length; i++) { - if (contentPackagePaths[i] != enabledPackages[i].Path) + if (!enabledPackages[i].NameMatches(contentPackageNames[i])) { orderMismatch = true; break; @@ -1009,7 +1035,7 @@ namespace Barotrauma if (orderMismatch) { if (!errorMsg.IsNullOrEmpty()) { errorMsg += "\n"; } - errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackagePaths)); + errorMsg += TextManager.GetWithVariable("campaignmode.contentpackageordermismatch", "[loadorder]", string.Join(", ", contentPackageNames)); } return false; @@ -1040,8 +1066,8 @@ namespace Barotrauma } } if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); } - rootElement.Add(new XAttribute("selectedcontentpackages", - string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Path)))); + rootElement.Add(new XAttribute("selectedcontentpackagenames", + string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|"))))); ((CampaignMode)GameMode).Save(doc.Root); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 7d03844eb..296277c9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -13,6 +12,7 @@ namespace Barotrauma.Items.Components { [Editable] public LimbType LimbType { get; set; } + [Editable] public Vector2 Position { get; set; } @@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components partial class Controller : ItemComponent, IServerSerializable { //where the limbs of the user should be positioned when using the controller - private readonly List limbPositions; + private readonly List limbPositions = new List(); private Direction dir; @@ -117,38 +117,9 @@ namespace Barotrauma.Items.Components public Controller(Item item, ContentXElement element) : base(item, element) { - limbPositions = new List(); - userPos = element.GetAttributeVector2("UserPos", Vector2.Zero); - Enum.TryParse(element.GetAttributeString("direction", "None"), out dir); - - foreach (var subElement in element.Elements()) - { - if (subElement.Name != "limbposition") { continue; } - string limbStr = subElement.GetAttributeString("limb", ""); - if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType)) - { - DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type."); - } - else - { - LimbPos limbPos = new LimbPos(limbType, - subElement.GetAttributeVector2("position", Vector2.Zero), - subElement.GetAttributeBool("allowusinglimb", false)); - limbPositions.Add(limbPos); - if (!limbPos.AllowUsingLimb) - { - if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm || - limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm) - { - AllowAiming = false; - } - } - } - - } - + LoadLimbPositions(element); IsActive = true; } @@ -529,5 +500,63 @@ namespace Barotrauma.Items.Components } partial void HideHUDs(bool value); + + public override XElement Save(XElement parentElement) + { + return SaveLimbPositions(base.Save(parentElement)); + } + + public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode) + { + LoadLimbPositions(componentElement); + } + } + + private XElement SaveLimbPositions(XElement element) + { + if (Screen.Selected == GameMain.SubEditorScreen) + { + foreach (var limbPos in limbPositions) + { + element.Add(new XElement("limbposition", + new XAttribute("limb", limbPos.LimbType), + new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)), + new XAttribute("allowusinglimb", limbPos.AllowUsingLimb))); + } + } + return element; + } + + private void LoadLimbPositions(XElement element) + { + limbPositions.Clear(); + foreach (var subElement in element.Elements()) + { + if (subElement.Name != "limbposition") { continue; } + string limbStr = subElement.GetAttributeString("limb", ""); + if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType)) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type."); + } + else + { + LimbPos limbPos = new LimbPos(limbType, + subElement.GetAttributeVector2("position", Vector2.Zero), + subElement.GetAttributeBool("allowusinglimb", false)); + limbPositions.Add(limbPos); + if (!limbPos.AllowUsingLimb) + { + if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm || + limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm) + { + AllowAiming = false; + } + } + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 27b9ae460..ee10aee6e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -80,6 +80,8 @@ namespace Barotrauma.Items.Components private float progressState; + private readonly Dictionary fabricationLimits = new Dictionary(); + public Fabricator(Item item, ContentXElement element) : base(item, element) { @@ -105,6 +107,10 @@ namespace Barotrauma.Items.Components } } fabricationRecipes.Add(recipe.RecipeHash, recipe); + if (recipe.FabricationLimitMax >= 0) + { + fabricationLimits.Add(recipe.RecipeHash, Rand.Range(recipe.FabricationLimitMin, recipe.FabricationLimitMax + 1)); + } } } this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary(); @@ -244,7 +250,7 @@ namespace Barotrauma.Items.Components itemList.Enabled = true; if (activateButton != null) { - activateButton.Text = TextManager.Get("FabricatorCreate"); + activateButton.Text = TextManager.Get(CreateButtonText); } #endif fabricatedItem = null; @@ -400,8 +406,22 @@ namespace Barotrauma.Items.Components quality = GetFabricatedItemQuality(fabricatedItem, user); } + int amount = (int)fabricationitemAmount.Value; + if (fabricationLimits.ContainsKey(fabricatedItem.RecipeHash)) + { + if (amount > fabricationLimits[fabricatedItem.RecipeHash]) + { + amount = fabricationLimits[fabricatedItem.RecipeHash]; + fabricationLimits[fabricatedItem.RecipeHash] = 0; + } + else + { + fabricationLimits[fabricatedItem.RecipeHash] -= amount; + } + } + var tempUser = user; - for (int i = 0; i < (int)fabricationitemAmount.Value; i++) + for (int i = 0; i < amount; i++) { float outCondition = fabricatedItem.OutCondition; GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier); @@ -535,6 +555,11 @@ namespace Barotrauma.Items.Components } } + if (fabricationLimits.TryGetValue(fabricableItem.RecipeHash, out int amount) && amount <= 0) + { + return false; + } + return fabricableItem.RequiredItems.All(requiredItem => { int availablePrefabsAmount = 0; @@ -698,7 +723,6 @@ namespace Barotrauma.Items.Components componentElement.Add(new XAttribute("fabricateditemidentifier", fabricatedItem.TargetItem.Identifier)); componentElement.Add(new XAttribute("savedtimeuntilready", timeUntilReady.ToString("G", CultureInfo.InvariantCulture))); componentElement.Add(new XAttribute("savedrequiredtime", requiredTime.ToString("G", CultureInfo.InvariantCulture))); - } return componentElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 93c68e6ac..411627eef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -114,7 +115,7 @@ namespace Barotrauma private readonly Quality qualityComponent; - private readonly Queue impactQueue = new Queue(); + private readonly ConcurrentQueue impactQueue = new ConcurrentQueue(); //a dictionary containing lists of the status effects in all the components of the item private readonly bool[] hasStatusEffectsOfType; @@ -1695,9 +1696,8 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { - while (impactQueue.Count > 0) + while (impactQueue.TryDequeue(out float impact)) { - float impact = impactQueue.Dequeue(); HandleCollision(impact); } @@ -1933,10 +1933,7 @@ namespace Barotrauma if (contact.FixtureA.Body == f1.Body) { normal = -normal; } float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); - lock (impactQueue) - { - impactQueue.Enqueue(impact); - } + impactQueue.Enqueue(impact); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 33d5b6289..8e25476cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -119,6 +119,11 @@ namespace Barotrauma public readonly uint RecipeHash; public readonly int Amount; + /// + /// How many of this item the fabricator can create (< 0 = unlimited) + /// + public readonly int FabricationLimitMin, FabricationLimitMax; + public FabricationRecipe(XElement element, Identifier itemPrefab) { TargetItemPrefabIdentifier = itemPrefab; @@ -141,6 +146,10 @@ namespace Barotrauma RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); + int limitDefault = element.GetAttributeInt("fabricationlimit", -1); + FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault); + FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault); + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -973,7 +982,11 @@ namespace Barotrauma public int? GetMinPrice() { - int? minPrice = StorePrices.Values.Min(p => p.Price); + int? minPrice = null; + if (StorePrices != null && StorePrices.Any()) + { + minPrice = StorePrices.Values.Min(p => p.Price); + } if (minPrice.HasValue) { if (DefaultPrice != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index f184f08f4..4db3d907b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -831,7 +831,13 @@ namespace Barotrauma } List ruinPositions = new List(); - for (int i = 0; i < GenerationParams.RuinCount; i++) + int ruinCount = GenerationParams.RuinCount; + if (GameMain.GameSession?.GameMode?.Missions.Any(m => m.Prefab.RequireRuin) ?? false) + { + ruinCount = Math.Max(ruinCount, 1); + } + + for (int i = 0; i < ruinCount; i++) { Point ruinSize = new Point(5000); int limitLeft = Math.Max(startPosition.X, ruinSize.X / 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index d1fc85553..cb6640f49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -387,14 +387,14 @@ namespace Barotrauma set; } - [Serialize(3, IsPropertySaveable.Yes, description: "Minimum 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(10, IsPropertySaveable.Yes, description: "Minimum 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 AbyssResourceClustersMin { get; set; } - [Serialize(20, 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(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)] public int AbyssResourceClustersMax { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 4b77d08fa..84d2510c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -257,7 +257,7 @@ namespace Barotrauma } DailySpecials.Clear(); int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount(); - for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) + for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++) { if (availableStock.None()) { break; } var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); @@ -266,7 +266,7 @@ namespace Barotrauma availableStock.Remove(item); } RequestedGoods.Clear(); - for (int i = 0; i < RequestedGoodsCount; i++) + for (int i = 0; i < Location.RequestedGoodsCount; i++) { var item = ItemPrefab.Prefabs.GetRandom(p => p.CanBeSold && !RequestedGoods.Contains(p) && @@ -359,8 +359,8 @@ namespace Barotrauma /// How many map progress steps it takes before the discounts should be updated. /// private const int SpecialsUpdateInterval = 3; - private const int DailySpecialsCount = 3; - private const int RequestedGoodsCount = 3; + private int DailySpecialsCount => Type.DailySpecialsCount; + private int RequestedGoodsCount => Type.RequestedGoodsCount; private int StepsSinceSpecialsUpdated { get; set; } public HashSet StoreIdentifiers { get; } = new HashSet(); @@ -1226,7 +1226,7 @@ namespace Barotrauma public int GetExtraSpecialSalesCount() { - var characters = GameSession.GetSessionCrewCharacters(); + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (!characters.Any()) { return 0; } return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); } @@ -1252,7 +1252,7 @@ namespace Barotrauma Discovered = true; if (checkTalents) { - GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); + GameSession.GetSessionCrewCharacters(CharacterType.Both).ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 2bfa64c65..e81e2795d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -1,13 +1,11 @@ -using Microsoft.Xna.Framework; +using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; -using Barotrauma.IO; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; -using System.Collections.Immutable; namespace Barotrauma { @@ -21,8 +19,9 @@ namespace Barotrauma // private readonly ImmutableArray<(Identifier Name, float Commonness)> hireableJobs; private readonly float totalHireableWeight; - - public Dictionary CommonnessPerZone = new Dictionary(); + + public readonly Dictionary CommonnessPerZone = new Dictionary(); + public readonly Dictionary MinCountPerZone = new Dictionary(); public readonly LocalizedString Name; @@ -65,7 +64,7 @@ namespace Barotrauma get; private set; } - + public string ReplaceInRadiation { get; } public Sprite Sprite { get; private set; } @@ -86,6 +85,8 @@ namespace Barotrauma /// In percentages /// public int StorePriceModifierRange { get; } = 5; + public int DailySpecialsCount { get; } = 1; + public int RequestedGoodsCount { get; } = 1; public List StoreBalanceStatuses { get; } = new List() { @@ -144,7 +145,7 @@ namespace Barotrauma names = new List() { "Name file not found" }; } - string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", new string[] { "" }); + string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty()); foreach (string commonnessPerZoneStr in commonnessPerZoneStrs) { string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':'); @@ -152,12 +153,26 @@ namespace Barotrauma !int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) || !float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness)) { - DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone0index: zone0commonness, zone1index: zone1commonness\""); + DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\""); break; } CommonnessPerZone[zoneIndex] = zoneCommonness; } + string[] minCountPerZoneStrs = element.GetAttributeStringArray("mincountperzone", Array.Empty()); + foreach (string minCountPerZoneStr in minCountPerZoneStrs) + { + string[] splitMinCountPerZone = minCountPerZoneStr.Split(':'); + if (splitMinCountPerZone.Length != 2 || + !int.TryParse(splitMinCountPerZone[0].Trim(), out int zoneIndex) || + !int.TryParse(splitMinCountPerZone[1].Trim(), out int minCount)) + { + DebugConsole.ThrowError("Failed to read minimum count values for location type \"" + Identifier + "\" - minimum counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\""); + break; + } + MinCountPerZone[zoneIndex] = minCount; + } + var hireableJobs = new List<(Identifier, float)>(); foreach (var subElement in element.Elements()) { @@ -205,6 +220,8 @@ namespace Barotrauma 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 0a793e304..478350b10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -218,16 +218,24 @@ namespace Barotrauma foreach (Location location in Locations) { - if (location.Type.Identifier != "city" && - location.Type.Identifier != "outpost") - { - continue; - } + if (location.Type.Identifier != "outpost") { continue; } if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) { CurrentLocation = StartLocation = furthestDiscoveredLocation = location; } } + //if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost + if (CurrentLocation == null) + { + foreach (Location location in Locations) + { + if (!location.Type.HasOutpost) { continue; } + if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X) + { + CurrentLocation = StartLocation = furthestDiscoveredLocation = location; + } + } + } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); CurrentLocation.Discover(true); @@ -273,6 +281,7 @@ namespace Barotrauma } voronoiSites.Clear(); + Dictionary> locationsPerZone = new Dictionary>(); foreach (GraphEdge edge in edges) { if (edge.Point1 == edge.Point2) { continue; } @@ -301,7 +310,24 @@ namespace Barotrauma Vector2 position = points[positionIndex]; if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; } int zone = GetZoneIndex(position.X); - newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient), requireOutpost: false, existingLocations: Locations); + if (!locationsPerZone.ContainsKey(zone)) + { + locationsPerZone[zone] = new List(); + } + + LocationType 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; + } + } + + newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient), + requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations); + locationsPerZone[zone].Add(newLocations[i]); Locations.Add(newLocations[i]); } @@ -448,8 +474,7 @@ namespace Barotrauma Connections[i].Locations[1]; if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned") { - #warning TODO: determinism? - leftMostLocation.ChangeType(LocationType.Prefabs.First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); + leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned")); } leftMostLocation.IsGateBetweenBiomes = true; Connections[i].Locked = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 336462b62..594adb116 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -752,6 +752,7 @@ namespace Barotrauma bool solutionFound = false; foreach (PlacedModule module in movableModules) { + if (module.ThisGap.ConnectedDoor == null && module.PreviousGap.ConnectedDoor == null) { continue; } Vector2 moveDir = GetMoveDir(module.ThisGapPosition); Vector2 moveStep = moveDir * 50.0f; Vector2 currentMove = Vector2.Zero; @@ -1093,6 +1094,10 @@ namespace Barotrauma } thisWayPoint.Remove(); } + else + { + DebugConsole.ThrowError($"Failed to connect waypoints between outpost modules. No waypoint in the {GetOpposingGapPosition(module.ThisGapPosition).ToString().ToLower()} gap of the module \"{module.PreviousModule.Info.Name}\"."); + } gapToRemove.ConnectedDoor?.Item.Remove(); if (hallwayLength <= 1.0f) { gapToRemove?.Remove(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 8dde595c0..c4e2cf5fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -28,8 +28,6 @@ namespace Barotrauma partial class SubmarineInfo : IDisposable { - public const string SavePath = "Submarines"; - private static List savedSubmarines = new List(); public static IEnumerable SavedSubmarines => savedSubmarines; @@ -578,58 +576,14 @@ namespace Barotrauma if (File.Exists(savedSubmarines[i].FilePath)) { bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); - bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath); bool isInContentPackage = contentPackageSubs.Any(f => f.Path == savedSubmarines[i].FilePath); if (isDownloadedSub) { continue; } - if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; } + if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && isInContentPackage) { continue; } } savedSubmarines[i].Dispose(); } - if (!Directory.Exists(SavePath)) - { - try - { - Directory.CreateDirectory(SavePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e); - return; - } - } - - List filePaths; - string[] subDirectories; - - try - { - filePaths = Directory.GetFiles(SavePath).ToList(); - subDirectories = Directory.GetDirectories(SavePath).Where(s => - { - DirectoryInfo dir = new DirectoryInfo(s); - return !dir.Attributes.HasFlag(System.IO.FileAttributes.Hidden) && !dir.Name.StartsWith("."); - }).ToArray(); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e); - return; - } - - foreach (string subDirectory in subDirectories) - { - try - { - filePaths.AddRange(Directory.GetFiles(subDirectory).ToList()); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e); - return; - } - } - + List filePaths = new List(); foreach (BaseSubFile subFile in contentPackageSubs) { if (!filePaths.Any(fp => fp == subFile.Path)) @@ -643,34 +597,7 @@ namespace Barotrauma foreach (string path in filePaths) { var subInfo = new SubmarineInfo(path); - if (subInfo.IsFileCorrupted) - { -#if CLIENT - if (DebugConsole.IsOpen) { DebugConsole.Toggle(); } - var deleteSubPrompt = new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" + - TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name), - new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - string filePath = path; - deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => - { - try - { - File.Delete(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e); - } - deleteSubPrompt.Close(); - return true; - }; - deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close; -#endif - } - else + if (!subInfo.IsFileCorrupted) { savedSubmarines.Add(subInfo); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index 0c3b25d2f..d18e54eef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -22,11 +22,12 @@ namespace Barotrauma.Networking ManageSettings = 0x200, ManagePermissions = 0x400, KarmaImmunity = 0x800, - BuyItems = 0x1000, + ManageMoney = 0x1000, SellInventoryItems = 0x2000, SellSubItems = 0x4000, - CampaignStore = 0x8000, - All = 0xFFFF + ManageMap = 0x8000, + ManageHires = 0x10000, + All = 0x1FFFF } class PermissionPreset diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index e405ecb4f..b8c2d6419 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -116,7 +116,8 @@ namespace Barotrauma.Networking StartRound, PurchaseAndSwitchSub, PurchaseSub, - SwitchSub + SwitchSub, + TransferMoney } public enum ReadyCheckState @@ -179,11 +180,11 @@ namespace Barotrauma.Networking protected ServerSettings serverSettings; + public Voting Voting { get; protected set; } + protected TimeSpan updateInterval; protected DateTime updateTimer; - public int EndVoteCount, EndVoteMax, SubmarineVoteYesCount, SubmarineVoteNoCount, SubmarineVoteMax; - protected bool gameStarted; protected RespawnManager respawnManager; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index f844f406a..c182cfdf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -278,8 +278,6 @@ namespace Barotrauma.Networking { ServerLog = new ServerLog(serverName); - Voting = new Voting(); - Whitelist = new WhiteList(); BanList = new BanList(); @@ -378,8 +376,6 @@ namespace Barotrauma.Networking public ServerLog ServerLog; - public Voting Voting; - public Dictionary MonsterEnabled { get; private set; } public const int MaxExtraCargoItemsOfType = 10; @@ -577,34 +573,20 @@ namespace Barotrauma.Networking [Serialize(true, IsPropertySaveable.Yes)] public bool AllowVoteKick { - get - { - return Voting.AllowVoteKick; - } - set - { - Voting.AllowVoteKick = value; - } + get; set; } [Serialize(true, IsPropertySaveable.Yes)] public bool AllowEndVoting { - get - { - return Voting.AllowEndVoting; - } - set - { - Voting.AllowEndVoting = value; - } + get; set; } private bool allowRespawn; [Serialize(true, IsPropertySaveable.Yes)] public bool AllowRespawn { - get { return allowRespawn; ; } + get { return allowRespawn; } set { if (allowRespawn == value) { return; } @@ -779,7 +761,7 @@ namespace Barotrauma.Networking set { subSelectionMode = value; - Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote; + AllowSubVoting = subSelectionMode == SelectionMode.Vote; ServerDetailsChanged = true; } } @@ -792,7 +774,7 @@ namespace Barotrauma.Networking set { modeSelectionMode = value; - Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote; + AllowModeVoting = modeSelectionMode == SelectionMode.Vote; ServerDetailsChanged = true; } } @@ -807,14 +789,14 @@ namespace Barotrauma.Networking } [Serialize(0.6f, IsPropertySaveable.Yes)] - public float SubmarineVoteRequiredRatio + public float VoteRequiredRatio { get; private set; } [Serialize(30f, IsPropertySaveable.Yes)] - public float SubmarineVoteTimeout + public float VoteTimeout { get; private set; @@ -928,6 +910,59 @@ namespace Barotrauma.Networking set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); } } + private bool allowSubVoting; + //Don't serialize: the value is set based on SubSelectionMode + public bool AllowSubVoting + { + get { return allowSubVoting; } + set + { + if (value == allowSubVoting) { return; } + allowSubVoting = value; +#if CLIENT + GameMain.NetLobbyScreen.SubList.Enabled = value || + (GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.SelectSub)); + var subVotesLabel = GameMain.NetLobbyScreen.Frame.FindChild("subvotes", true) as GUITextBlock; + subVotesLabel.Visible = value; + var subVisButton = GameMain.NetLobbyScreen.SubVisibilityButton; + subVisButton.RectTransform.AbsoluteOffset + = new Point(value ? (int)(subVotesLabel.TextSize.X + subVisButton.Rect.Width) : 0, 0); + + GameMain.Client?.Voting.UpdateVoteTexts(null, VoteType.Sub); + GameMain.NetLobbyScreen.SubList.Deselect(); +#endif + } + } + + private bool allowModeVoting; + //Don't serialize: the value is set based on ModeSelectionMode + public bool AllowModeVoting + { + get { return allowModeVoting; } + set + { + if (value == allowModeVoting) { return; } + allowModeVoting = value; +#if CLIENT + GameMain.NetLobbyScreen.ModeList.Enabled = + value || + (GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.SelectMode)); + GameMain.NetLobbyScreen.Frame.FindChild("modevotes", true).Visible = value; + // Disable modes that cannot be voted on + foreach (var guiComponent in GameMain.NetLobbyScreen.ModeList.Content.Children) + { + if (guiComponent is GUIFrame frame) + { + frame.CanBeFocused = !allowModeVoting || ((GameModePreset)frame.UserData).Votable; + } + } + GameMain.Client?.Voting.UpdateVoteTexts(null, VoteType.Mode); + GameMain.NetLobbyScreen.ModeList.Deselect(); +#endif + } + } + + public void SetPassword(string password) { if (string.IsNullOrEmpty(password)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs index ecab1d04a..5fd149b62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voting.cs @@ -1,19 +1,11 @@ using Barotrauma.Networking; +using System; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { partial class Voting { - private bool allowSubVoting, allowModeVoting; - - public bool AllowVoteKick = true; - - public bool AllowEndVoting = true; - - public bool VoteRunning = false; - public enum VoteState { None = 0, Started = 1, Running = 2, Passed = 3, Failed = 4 }; private IReadOnlyDictionary GetVoteCounts(VoteType voteType, List voters) @@ -39,12 +31,12 @@ namespace Barotrauma public T HighestVoted(VoteType voteType, List voters) { - if (voteType == VoteType.Sub && !AllowSubVoting) return default(T); - if (voteType == VoteType.Mode && !AllowModeVoting) return default(T); + if (voteType == VoteType.Sub && !GameMain.NetworkMember.ServerSettings.AllowSubVoting) { return default; } + if (voteType == VoteType.Mode && !GameMain.NetworkMember.ServerSettings.AllowModeVoting) { return default; } IReadOnlyDictionary voteList = GetVoteCounts(voteType, voters); - T selected = default(T); + T selected = default; int highestVotes = 0; foreach (KeyValuePair votable in voteList) { @@ -71,11 +63,13 @@ namespace Barotrauma { client.ResetVotes(); } - - GameMain.NetworkMember.EndVoteCount = 0; - GameMain.NetworkMember.EndVoteMax = 0; - #if CLIENT + foreach (VoteType voteType in Enum.GetValues(typeof(VoteType))) + { + SetVoteCountYes(voteType, 0); + SetVoteCountNo(voteType, 0); + SetVoteCountMax(voteType, 0); + } UpdateVoteTexts(connectedClients, VoteType.Mode); UpdateVoteTexts(connectedClients, VoteType.Sub); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 9684fbe89..e44c75dd2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -348,6 +348,14 @@ namespace Barotrauma.Steam string val = attribute.Value.CleanUpPathCrossPlatform(correctFilenameCase: 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..]; + } + //Handle really old mods (0.9.0.4-era) that might be structured as //%ModDir%/Mods/[NAME]/[RESOURCE] string fullSrcPath = Path.Combine(fileListDir, val).CleanUpPath(); @@ -418,7 +426,7 @@ namespace Barotrauma.Steam File.Copy(from, to, overwrite: true); } - private static async Task CopyDirectory(string fileListDir, string modName, string from, string to) + public static async Task CopyDirectory(string fileListDir, string modName, string from, string to) { from = Path.GetFullPath(from); to = Path.GetFullPath(to); Directory.CreateDirectory(to); diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 3963df1ca..9f3239e41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -79,10 +79,10 @@ namespace Barotrauma { roundData.EnteredCrushDepth.Add(c); } - else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth * 0.5f) + else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth - 500.0f) { //all characters that have entered crush depth and are still alive get an achievement - if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); + if (roundData.EnteredCrushDepth.Contains(c)) { UnlockAchievement(c, "survivecrushdepth".ToIdentifier()); } } } } @@ -426,7 +426,7 @@ namespace Barotrauma !c.IsDead && c.TeamID != CharacterTeamType.FriendlyNPC && !(c.AIController is EnemyAIController) && - (c.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost))); + (c.Submarine == gameSession.Submarine || gameSession.Submarine.GetConnectedSubs().Contains(c.Submarine) || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost))); if (charactersInSub.Count == 1) { @@ -454,6 +454,10 @@ namespace Barotrauma } foreach (Character character in charactersInSub) { + if (roundData.EnteredCrushDepth.Contains(character)) + { + UnlockAchievement(character, "survivecrushdepth".ToIdentifier()); + } if (character.Info.Job == null) { continue; } UnlockAchievement(character, $"{character.Info.Job.Prefab.Identifier}round".ToIdentifier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs rename to Barotrauma/BarotraumaShared/SharedSource/Utils/Md5Hash.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs index 7b46b9467..9f0a2f711 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/NamedEvent.cs @@ -37,6 +37,12 @@ namespace Barotrauma events.Remove(identifier); } + public void TryDeregister(Identifier identifier) + { + if (!HasEvent(identifier)) { return; } + Deregister(identifier); + } + public bool HasEvent(Identifier identifier) => events.ContainsKey(identifier); public void Invoke(T data) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 4101d9513..e0b919969 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Xml.Linq; using Steamworks.Data; using Color = Microsoft.Xna.Framework.Color; +using System.Text.RegularExpressions; namespace Barotrauma { @@ -227,7 +228,7 @@ namespace Barotrauma return Path.Combine(folder, saveName); } - public static IEnumerable GetSaveFiles(SaveType saveType, bool includeInCompatible = true) + public static IReadOnlyList GetSaveFiles(SaveType saveType, bool includeInCompatible = true) { string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; if (!Directory.Exists(folder)) @@ -250,18 +251,61 @@ namespace Barotrauma files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly)); } - if (!includeInCompatible) + List saveInfos = new List(); + foreach (string file in files) { - for (int i = files.Count - 1; i >= 0; i--) + XDocument doc = LoadGameSessionDoc(file); + if (!includeInCompatible && !IsSaveFileCompatible(doc)) { - XDocument doc = LoadGameSessionDoc(files[i]); - if (!IsSaveFileCompatible(doc)) + continue; + } + if (doc?.Root == null) + { + saveInfos.Add(new CampaignMode.SaveInfo() { - files.RemoveAt(i); + FilePath = file + }); + } + else + { + List enabledContentPackageNames = new List(); + + //backwards compatibility + string enabledContentPackagePathsStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackages", string.Empty); + foreach (string packagePath in enabledContentPackagePathsStr.Split('|')) + { + if (string.IsNullOrEmpty(packagePath)) { continue; } + //change paths to names + string fileName = Path.GetFileNameWithoutExtension(packagePath); + if (fileName == "filelist") + { + enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath))); + } + else + { + enabledContentPackageNames.Add(fileName); + } } + + string enabledContentPackageNamesStr = doc.Root.GetAttributeStringUnrestricted("selectedcontentpackagenames", string.Empty); + //split on pipes, excluding pipes preceded by \ + foreach (string packageName in Regex.Split(enabledContentPackageNamesStr, @"(? Date: Mon, 4 Apr 2022 16:46:08 +0900 Subject: [PATCH 8/9] Unstable 0.17.6.0 --- .../Transition/UgcTransition.cs | 166 ++++++++--- .../ClientSource/DebugConsole.cs | 6 +- .../ClientSource/GUI/ChatBox.cs | 11 +- .../ClientSource/GUI/FileSelection.cs | 4 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 7 +- .../ClientSource/GUI/GUIContextMenu.cs | 13 +- .../ClientSource/GUI/GUITextBox.cs | 9 +- .../ClientSource/GUI/Store.cs | 56 ++-- .../ClientSource/GUI/SubmarineSelection.cs | 17 +- .../ClientSource/GUI/VotingInterface.cs | 4 +- .../BarotraumaClient/ClientSource/GameMain.cs | 4 +- .../ClientSource/GameSession/CrewManager.cs | 8 +- .../GameModes/MultiPlayerCampaign.cs | 18 ++ .../Items/Components/Machines/Fabricator.cs | 27 +- .../Items/Components/StatusHUD.cs | 2 +- .../ClientSource/Items/Item.cs | 45 +-- .../ClientSource/Map/Structure.cs | 2 +- .../ClientSource/Networking/Client.cs | 19 +- .../ClientSource/Networking/GameClient.cs | 73 +---- .../ClientSource/Networking/Voting.cs | 7 +- .../ClientSource/Particles/ParticlePrefab.cs | 5 +- .../ClientSource/Screens/LevelEditorScreen.cs | 6 +- .../ClientSource/Screens/MainMenuScreen.cs | 1 - .../ClientSource/Screens/ModDownloadScreen.cs | 31 +- .../ClientSource/Screens/NetLobbyScreen.cs | 64 ++-- .../ClientSource/Screens/SubEditorScreen.cs | 142 +++------ .../ClientSource/Sounds/SoundPlayer.cs | 31 ++ .../ClientSource/Sounds/SoundPrefab.cs | 1 + .../ClientSource/Steam/BulkDownloader.cs | 125 ++++++++ .../Immutable/ImmutableWorkshopMenu.cs | 22 +- .../Mutable/MutableWorkshopMenu.cs | 278 +++++++++++++----- .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 24 ++ .../Steam/WorkshopMenu/WorkshopMenu.cs | 7 +- .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/DebugConsole.cs | 7 +- .../ServerSource/GameSession/CargoManager.cs | 4 +- .../GameModes/MultiPlayerCampaign.cs | 37 ++- .../Items/Components/Machines/Fabricator.cs | 12 +- .../ServerSource/Networking/BanList.cs | 2 +- .../Networking/FileTransfer/FileSender.cs | 8 +- .../Networking/FileTransfer/ModSender.cs | 2 +- .../ServerSource/Networking/GameServer.cs | 66 ++--- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/EnemyAIController.cs | 2 +- .../Characters/AI/IndoorsSteeringManager.cs | 27 +- .../AI/Objectives/AIObjectiveManager.cs | 2 +- .../Characters/Animation/AnimController.cs | 2 + .../Animation/FishAnimController.cs | 4 +- .../Animation/HumanoidAnimController.cs | 102 +++---- .../Characters/Animation/Ragdoll.cs | 61 +++- .../SharedSource/Characters/Attack.cs | 14 +- .../SharedSource/Characters/Character.cs | 50 +++- .../Characters/Health/CharacterHealth.cs | 12 +- .../Params/Ragdoll/RagdollParams.cs | 11 +- .../ContentFile/ItemAssemblyFile.cs | 1 + .../ContentManagement/ContentFile/TextFile.cs | 1 - .../ContentPackageManager.cs | 13 + .../SharedSource/Events/EventSet.cs | 13 +- .../Events/Missions/BeaconMission.cs | 2 +- .../SharedSource/GameSession/GameSession.cs | 32 +- .../Items/Components/Machines/Fabricator.cs | 16 +- .../SharedSource/Items/Item.cs | 4 +- .../SharedSource/Items/ItemPrefab.cs | 6 + .../SharedSource/Map/ItemAssemblyPrefab.cs | 1 - .../Levels/LevelObjects/LevelObjectManager.cs | 2 +- .../SharedSource/Map/Map/Location.cs | 6 +- .../SharedSource/Map/PriceInfo.cs | 28 +- .../SharedSource/Map/WayPoint.cs | 45 ++- .../SharedSource/Networking/Client.cs | 19 ++ .../SharedSource/Screens/GameScreen.cs | 7 + .../SharedSource/Steam/Workshop.cs | 6 +- .../SharedSource/Text/TextManager.cs | 14 + Barotrauma/BarotraumaShared/changelog.txt | 62 ++++ Libraries/Facepunch.Steamworks/SteamUgc.cs | 18 +- 78 files changed, 1265 insertions(+), 703 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs diff --git a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs index 026b93043..537ca3df8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/ContentManagement/Transition/UgcTransition.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Steam; using Microsoft.Xna.Framework; @@ -26,9 +27,9 @@ namespace Barotrauma.Transition { TaskPool.Add("UgcTransition.Prepare", DetermineItemsToTransition(), t => { - if (!t.TryGetResult(out (OldSubs, OldMods) pair)) { return; } - var (subs, mods) = pair; - if (!subs.FilePaths.Any() && !mods.Mods.Any()) { return; } + if (!t.TryGetResult(out (OldSubs, OldItemAssemblies, OldMods) result)) { return; } + var (subs, itemAssemblies, mods) = result; + if (!subs.FilePaths.Any() && !itemAssemblies.FilePaths.Any() && !mods.Mods.Any()) { return; } var msgBox = new GUIMessageBox(TextManager.Get("Ugc.TransferTitle"), "", relativeSize: (0.5f, 0.8f), buttons: new LocalizedString[] { TextManager.Get("Ugc.TransferButton") }); @@ -63,19 +64,45 @@ namespace Barotrauma.Transition }; pathTickboxMap.Add(dir, tickbox); } - - addHeader(TextManager.Get("WorkshopLabelSubmarines")); - foreach (var sub in subs.FilePaths) + + bool firstHeader = true; + + void addSpacer() { - var subName = Path.GetFileNameWithoutExtension(sub); - addTickbox(sub, subName, ticked: !ContentPackageManager.LocalPackages.Any(p => p.NameMatches(subName))); + if (firstHeader) { firstHeader = false; return; } + addHeader(""); } - addHeader(""); - addHeader(TextManager.Get("SubscribedMods")); - foreach (var mod in mods.Mods) + if (subs.FilePaths.Any()) { - addTickbox(mod.Dir, mod.Name, ticked: !ContentPackageManager.LocalPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == mod.Item?.Id)); + addSpacer(); + addHeader(TextManager.Get("WorkshopLabelSubmarines")); + foreach (var sub in subs.FilePaths) + { + var subName = Path.GetFileNameWithoutExtension(sub); + addTickbox(sub, subName, ticked: !ContentPackageManager.LocalPackages.Any(p => p.NameMatches(subName))); + } + } + + if (itemAssemblies.FilePaths.Any()) + { + addSpacer(); + addHeader(TextManager.Get("ItemAssemblies")); + foreach (var itemAssembly in itemAssemblies.FilePaths) + { + var assemblyName = Path.GetFileNameWithoutExtension(itemAssembly); + addTickbox(itemAssembly, assemblyName, ticked: !ContentPackageManager.LocalPackages.Any(p => p.NameMatches(assemblyName))); + } + } + + if (mods.Mods.Any()) + { + addSpacer(); + addHeader(TextManager.Get("SubscribedMods")); + foreach (var mod in mods.Mods) + { + addTickbox(mod.Dir, mod.Name, ticked: !ContentPackageManager.LocalPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == mod.Item?.Id)); + } } GUIMessageBox? subMsgBox = null; @@ -119,6 +146,16 @@ namespace Barotrauma.Transition } } + private struct OldItemAssemblies + { + public readonly IReadOnlyList FilePaths; + + public OldItemAssemblies(IReadOnlyList filePaths) + { + FilePaths = filePaths; + } + } + private struct OldMods { public readonly IReadOnlyList<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> Mods; @@ -131,15 +168,24 @@ namespace Barotrauma.Transition private const string oldSubsPath = "Submarines"; private const string oldModsPath = "Mods"; + private const string oldItemAssembliesPath = "ItemAssemblies"; - private static async Task<(OldSubs Subs, OldMods Mods)> DetermineItemsToTransition() + private static async Task<(OldSubs Subs, OldItemAssemblies ItemAssemblies, OldMods Mods)> DetermineItemsToTransition() { string[] subs = Array.Empty(); + string[] itemAssemblies = Array.Empty(); List<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)> mods = new List<(string Dir, string Name, Steamworks.Ugc.Item? Item, DateTime InstallTime)>(); if (FolderShouldBeTransitioned(oldModsPath)) { - subs = Directory.GetFiles(oldSubsPath, "*.sub", SearchOption.TopDirectoryOnly); + string[] getFiles(string path, string pattern) + => Directory.Exists(path) + ? Directory.GetFiles(path, pattern, SearchOption.TopDirectoryOnly) + : Array.Empty(); + + subs = getFiles(oldSubsPath, "*.sub"); + itemAssemblies = getFiles(oldItemAssembliesPath, "*.xml"); + string[] allOldMods = Directory.GetDirectories(oldModsPath, "*", SearchOption.TopDirectoryOnly); var publishedItems = await SteamManager.Workshop.GetPublishedItems(); @@ -160,9 +206,9 @@ namespace Barotrauma.Transition } } - while (!(Screen.Selected is MainMenuScreen)) { await Task.Delay(500); } + while (!(Screen.Selected is MainMenuScreen)) { await Task.Delay(50); } - return (new OldSubs(subs), new OldMods(mods)); + return (new OldSubs(subs), new OldItemAssemblies(itemAssemblies), new OldMods(mods)); } private static bool FolderShouldBeTransitioned(string folderName) @@ -173,45 +219,71 @@ namespace Barotrauma.Transition private static async Task TransferMods(Dictionary pathTickboxMap) { - //WriteReadme(oldSubsPath); //can't do this because the submarine discovery code is borked + //WriteReadme(oldSubsPath); //can't do this because the old submarine discovery code is borked WriteReadme(oldModsPath); - foreach (var (path, tickbox) in pathTickboxMap) - { - if (!tickbox.Selected) { continue; } - string dirName = Path.GetFileNameWithoutExtension(path); - string destPath = Path.Combine(ContentPackage.LocalModsDir, dirName); - - //find unique path to save in - for (int i = 0;;i++) - { - if (!Directory.Exists(destPath)) { break; } - destPath = Path.Combine(ContentPackage.LocalModsDir, $"{dirName}.{i}"); - } - - if (path.StartsWith(oldSubsPath, StringComparison.OrdinalIgnoreCase)) - { - //copying a sub: manually create filelist.xml - ModProject modProject = new ModProject - { - Name = dirName, - ModVersion = ContentPackage.DefaultModVersion - }; - modProject.AddFile(ModProject.File.FromPath(Path.Combine(ContentPath.ModDirStr, $"{dirName}.sub"))); + await Task.WhenAll(pathTickboxMap.Select(TransferMod)); + } - Directory.CreateDirectory(destPath); - File.Copy(path, Path.Combine(destPath, $"{dirName}.sub")); - modProject.Save(Path.Combine(destPath, ContentPackage.FileListFileName)); - - await Task.Yield(); + private static Task TransferMod(KeyValuePair kvp) + => TransferMod(kvp.Key, kvp.Value); + + private static async Task TransferMod(string path, GUITickBox tickbox) + { + if (!tickbox.Selected) { return; } + string dirName = Path.GetFileNameWithoutExtension(path); + string destPath = Path.Combine(ContentPackage.LocalModsDir, dirName); + + //find unique path to save in + for (int i = 0;;i++) + { + if (!Directory.Exists(destPath)) { break; } + destPath = Path.Combine(ContentPackage.LocalModsDir, $"{dirName}.{i}"); + } + + bool isSub = path.StartsWith(oldSubsPath, StringComparison.OrdinalIgnoreCase); + bool isItemAssembly = path.StartsWith(oldItemAssembliesPath, StringComparison.OrdinalIgnoreCase); + if (isSub || isItemAssembly) + { + //copying a sub or item assembly: manually create filelist.xml + ModProject modProject = new ModProject + { + Name = dirName, + ModVersion = ContentPackage.DefaultModVersion + }; + + Type fileType; + if (isSub) + { + fileType = typeof(SubmarineFile); + XDocument? doc = SubmarineInfo.OpenFile(path, out _); + if (doc?.Root != null) + { + SubmarineType subType = doc.Root.GetAttributeEnum("type", SubmarineType.Player); + fileType = SubEditorScreen.DetermineSubFileType(subType); + } } else { - //copying a mod: we have a neat method for that! - await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath); + fileType = typeof(ItemAssemblyFile); } + + modProject.AddFile(ModProject.File.FromPath( + Path.Combine(ContentPath.ModDirStr, $"{dirName}.{(isSub ? "sub" : "xml")}"), + fileType)); + + Directory.CreateDirectory(destPath); + File.Copy(path, Path.Combine(destPath, $"{dirName}.{(isSub ? "sub" : "xml")}")); + modProject.Save(Path.Combine(destPath, ContentPackage.FileListFileName)); + + await Task.Yield(); + } + else + { + //copying a mod: we have a neat method for that! + await SteamManager.Workshop.CopyDirectory(path, Path.GetFileName(path), path, destPath); } } - + private static void WriteReadme(string folderName) { if (!Directory.Exists(folderName)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 57d4bc38f..c9c538081 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -3310,11 +3310,7 @@ namespace Barotrauma } depth += " "; - - if (newPrice > 0) - { - newPrices.TryAdd(materialPrefab, newPrice); - } + newPrices.TryAdd(materialPrefab, newPrice); int componentCost = 0; int newComponentCost = 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 9c175f289..c1ee89fb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -25,12 +25,14 @@ namespace Barotrauma get { return _toggleOpen; } set { - _toggleOpen = value; - if (value) hideableElements.Visible = true; + _toggleOpen = PreferChatBoxOpen = value; + if (value) { hideableElements.Visible = true; } } } private float openState; + public static bool PreferChatBoxOpen = true; + public bool CloseAfterMessageSent; private float prevUIScale; @@ -99,6 +101,7 @@ namespace Barotrauma var channelSettingsContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), channelSettingsFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, + CanBeFocused = true, RelativeSpacing = 0.01f }; @@ -119,7 +122,7 @@ namespace Barotrauma Color = new Color(51, 59, 46), SpriteEffects = Microsoft.Xna.Framework.Graphics.SpriteEffects.FlipHorizontally }; - arrowIcon.HoverColor = arrowIcon.PressedColor = arrowIcon.PressedColor = arrowIcon.Color; + arrowIcon.HoverColor = arrowIcon.PressedColor = arrowIcon.SelectedColor = arrowIcon.Color; channelText = new GUITextBox(new RectTransform(new Vector2(0.25f, 0.8f), channelSettingsContent.RectTransform), style: "DigitalFrameLight", textAlignment: Alignment.Center, font: GUIStyle.DigitalFont) { @@ -265,7 +268,7 @@ namespace Barotrauma }; showNewMessagesButton.Visible = false; - ToggleOpen = GameSettings.CurrentConfig.ChatOpen; + ToggleOpen = PreferChatBoxOpen = GameSettings.CurrentConfig.ChatOpen; } public bool TypingChatMessage(GUITextBox textBox, string text) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 2f54a3d4d..71023ab04 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -415,9 +415,9 @@ namespace Barotrauma if (dir.EndsWith("/")) { dir = dir.Substring(0, dir.Length - 1); } int index = dir.LastIndexOf("/"); if (index < 0) { return false; } - CurrentDirectory = CurrentDirectory.Substring(0, index+1); + CurrentDirectory = CurrentDirectory.Substring(0, index + 1); - return false; + return true; } public static void AddToGUIUpdateList() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 88b835ede..3a6ecfd23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -343,7 +343,7 @@ namespace Barotrauma foreach (string childKey in GameMain.PerformanceCounter.GetSavedPartialIdentifiers(key)) { elapsedMillisecs = GameMain.PerformanceCounter.GetPartialAverageElapsedMillisecs(key, childKey); - DrawString(spriteBatch, new Vector2(315, y), + 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; @@ -1422,8 +1422,9 @@ namespace Barotrauma public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit) { - if (font == null) font = GUIStyle.Font; - if (backgroundColor != null) + if (color.A == 0) { return; } + if (font == null) { font = GUIStyle.Font; } + if (backgroundColor != null && backgroundColor.Value.A > 0) { Vector2 textSize = font.MeasureString(text); DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs index 2de088d11..eeebb32c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -95,14 +95,21 @@ namespace Barotrauma // Construct the GUI elements //---------------------------------------------------------------------------------- - GUILayoutGroup background = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform, Anchor.Center)); + GUILayoutGroup background = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform, Anchor.Center)) + { + Stretch = true + }; + Point listSize = estimatedSize; if (hasHeader) { - HeaderLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), background.RectTransform), header, font: headerFont) { Padding = headerPadding }; + Point sz = Point.Zero; + InflateSize(ref sz, header, headerFont); + listSize.Y -= sz.Y; + HeaderLabel = new GUITextBlock(new RectTransform(sz, background.RectTransform), header, font: headerFont) { Padding = headerPadding }; } - GUIListBox optionList = new GUIListBox(new RectTransform(new Vector2(1f, hasHeader ? 0.8f : 1f), background.RectTransform), style: null) + GUIListBox optionList = new GUIListBox(new RectTransform(listSize, background.RectTransform), style: null) { AutoHideScrollBar = false, ScrollBarVisible = false, diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 4635c2150..b4edb64ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -316,6 +316,7 @@ namespace Barotrauma } if (Text == text) { return false; } textBlock.Text = text; + ClearSelection(); if (Text == null) textBlock.Text = ""; if (Text != "" && !Wrap) { @@ -808,10 +809,10 @@ namespace Barotrauma { if (selectedText.Length == 0) { return; } - selectionStartIndex = Math.Max(0, Math.Min(selectionEndIndex, Math.Min(selectionStartIndex, Text.Length - 1))); - int selectionLength = Math.Min(Text.Length - selectionStartIndex, selectedText.Length); - SetText(Text.Remove(selectionStartIndex, selectionLength)); - CaretIndex = Math.Min(Text.Length, selectionStartIndex); + int targetCaretIndex = Math.Max(0, Math.Min(selectionEndIndex, Math.Min(selectionStartIndex, Text.Length - 1))); + int selectionLength = Math.Min(Text.Length - targetCaretIndex, selectedText.Length); + SetText(Text.Remove(targetCaretIndex, selectionLength)); + CaretIndex = targetCaretIndex; ClearSelection(); OnTextChanged?.Invoke(this, Text); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 88bf466e4..8a495614b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -570,7 +570,7 @@ namespace Barotrauma AutoHideScrollBar = false, Visible = false }; - storeDailySpecialsGroup = CreateDealsGroup(storeBuyList); + storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, CurrentLocation?.DailySpecialsCount ?? 1); tabLists.Add(StoreTab.Buy, storeBuyList); storeSellList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) @@ -578,7 +578,7 @@ namespace Barotrauma AutoHideScrollBar = false, Visible = false }; - storeRequestedGoodGroup = CreateDealsGroup(storeSellList); + storeRequestedGoodGroup = CreateDealsGroup(storeSellList, CurrentLocation?.RequestedGoodsCount ?? 1); tabLists.Add(StoreTab.Sell, storeSellList); storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) @@ -586,7 +586,7 @@ namespace Barotrauma AutoHideScrollBar = false, Visible = false }; - storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList); + storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, CurrentLocation?.RequestedGoodsCount ?? 1); tabLists.Add(StoreTab.SellSub, storeSellFromSubList); // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ @@ -713,8 +713,10 @@ namespace Barotrauma private LocalizedString GetPlayerBalanceText() => TextManager.FormatCurrency(PlayerWallet.Balance); - private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) + private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount) { + // Add 1 for the header + elementCount++; var elementHeight = (int)(GUI.yScale * 80); var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null) { @@ -852,24 +854,20 @@ namespace Barotrauma FilterStoreItems(category, searchBox.Text); } - int prevDailySpecialCount; + int prevDailySpecialCount, prevRequestedGoodsCount, prevSubRequestedGoodsCount; private void RefreshStoreBuyList() { float prevBuyListScroll = storeBuyList.BarScroll; float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll; - bool hasPermissions = HasBuyPermissions; - HashSet existingItemFrames = new HashSet(); - int dailySpecialCount = ActiveStore.DailySpecials.Count; - if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) { storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); - storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, 1 + dailySpecialCount); + storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount); storeDailySpecialsGroup.Parent.SetAsFirstChild(); } else @@ -881,6 +879,8 @@ namespace Barotrauma prevDailySpecialCount = dailySpecialCount; } + bool hasPermissions = HasTabPermissions(StoreTab.Sell); + var existingItemFrames = new HashSet(); foreach (PurchasedItem item in ActiveStore.Stock) { CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); @@ -942,29 +942,30 @@ namespace Barotrauma { float prevSellListScroll = storeSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; - bool hasPermissions = HasTabPermissions(StoreTab.Sell); - HashSet existingItemFrames = new HashSet(); - if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any()) + int requestedGoodsCount = ActiveStore.RequestedGoods.Count; + if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount) { - if (storeRequestedGoodGroup == null) + storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent); + if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount) { - storeRequestedGoodGroup = CreateDealsGroup(storeSellList); + storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount); storeRequestedGoodGroup.Parent.SetAsFirstChild(); } else { - storeSellList.RemoveChild(storeRequestedGoodGroup.Parent); storeRequestedGoodGroup = null; } storeSellList.RecalculateChildren(); + prevRequestedGoodsCount = requestedGoodsCount; } + bool hasPermissions = HasTabPermissions(StoreTab.Sell); + var existingItemFrames = new HashSet(); foreach (PurchasedItem item in itemsToSell) { CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (var requestedGood in ActiveStore.RequestedGoods) { if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } @@ -1009,6 +1010,7 @@ namespace Barotrauma removedItemFrames.AddRange(storeRequestedGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); + if (activeTab == StoreTab.Sell) { FilterStoreItems(); } SortItems(StoreTab.Sell); @@ -1020,29 +1022,30 @@ namespace Barotrauma { float prevSellListScroll = storeSellFromSubList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll; - bool hasPermissions = HasSellSubPermissions; - HashSet existingItemFrames = new HashSet(); - if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any()) + int requestedGoodsCount = ActiveStore.RequestedGoods.Count; + if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount) { - if (storeRequestedSubGoodGroup == null) + storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent); + if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount) { - storeRequestedSubGoodGroup = CreateDealsGroup(storeSellList); + storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount); storeRequestedSubGoodGroup.Parent.SetAsFirstChild(); } else { - storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup.Parent); storeRequestedSubGoodGroup = null; } storeSellFromSubList.RecalculateChildren(); + prevSubRequestedGoodsCount = requestedGoodsCount; } + bool hasPermissions = HasSellSubPermissions; + var existingItemFrames = new HashSet(); foreach (PurchasedItem item in itemsToSellFromSub) { CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); } - foreach (var requestedGood in ActiveStore.RequestedGoods) { if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } @@ -1087,6 +1090,7 @@ namespace Barotrauma removedItemFrames.AddRange(storeRequestedSubGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); + if (activeTab == StoreTab.SellSub) { FilterStoreItems(); } SortItems(StoreTab.SellSub); @@ -2164,6 +2168,10 @@ namespace Barotrauma { RefreshItemsToSellFromSub(); } + if (needsRefresh) + { + Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); + } if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy)) { RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 152424061..d4461b472 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -84,19 +84,16 @@ namespace Barotrauma private void Initialize() { initialized = true; + currentSubText = TextManager.Get("currentsub"); + deliveryFeeText = TextManager.Get("deliveryfee"); + deliveryText = TextManager.Get("requestdeliverybutton"); + switchText = TextManager.Get("switchtosubmarinebutton"); + purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); + purchaseOnlyText = TextManager.Get("purchase"); + priceText = TextManager.Get("price"); if (transferService) { deliveryFee = CalculateDeliveryFee(); - currentSubText = TextManager.Get("currentsub"); - deliveryFeeText = TextManager.Get("deliveryfee"); - deliveryText = TextManager.Get("requestdeliverybutton"); - switchText = TextManager.Get("switchtosubmarinebutton"); - } - else - { - purchaseAndSwitchText = TextManager.Get("purchaseandswitch"); - purchaseOnlyText = TextManager.Get("purchase"); - priceText = TextManager.Get("price"); } currencyName = TextManager.Get("credit").Value.ToLowerInvariant(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs index a927e30a4..e54913559 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VotingInterface.cs @@ -305,10 +305,10 @@ namespace Barotrauma public static LocalizedString GetMoneyTransferVoteResultMessage(Client from, Client to, int transferAmount, int yesVoteCount, int noVoteCount, bool votePassed) { LocalizedString result = string.Empty; - if (from != null) + if (from == null && to != null) { result = TextManager.GetWithVariables(votePassed ? "crewwallet.banktoplayer.votepassed" : "crewwallet.banktoplayer.votefailed", - ("[playername]", from.Name), + ("[playername]", to.Name), ("[amount]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", transferAmount)), ("[yesvotecount]", yesVoteCount.ToString()), ("[novotecount]", noVoteCount.ToString())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index a046c051f..de83074a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -472,7 +472,9 @@ namespace Barotrauma TitleScreen.LoadState = MathHelper.Lerp(min, max, progress.Value); yield return CoroutineStatus.Running; } - + + TextManager.VerifyLanguageAvailable(); + DebugConsole.Init(); #if !DEBUG && !OSX diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index d00290a64..439e43f96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -55,15 +55,17 @@ namespace Barotrauma { if (_isCrewMenuOpen == value) { return; } _isCrewMenuOpen = value; - #warning TODO: update GameSettings.CurrentConfig.CrewMenuOpen when round ends + PreferCrewMenuOpen = value; } } + public static bool PreferCrewMenuOpen = true; + public bool AutoShowCrewList() => _isCrewMenuOpen = true; public void AutoHideCrewList() => _isCrewMenuOpen = false; - public void ResetCrewList() => _isCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; + public void ResetCrewList() => _isCrewMenuOpen = PreferCrewMenuOpen; const float CommandNodeAnimDuration = 0.2f; @@ -217,7 +219,7 @@ namespace Barotrauma screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevUIScale = GUI.Scale; - _isCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; + _isCrewMenuOpen = PreferCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen; } public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, IReadOnlyList reports, bool isHorizontal) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 3c2dd322e..2e35a7dac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -592,6 +592,13 @@ namespace Barotrauma 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(); } @@ -697,6 +704,17 @@ namespace Barotrauma 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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index b5e745407..7af73e740 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -4,9 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Reflection.Metadata; namespace Barotrauma.Items.Components { @@ -53,15 +51,12 @@ namespace Barotrauma.Items.Components [Serialize("vendingmachine.outofstock", IsPropertySaveable.Yes)] public string FabricationLimitReachedText { get; set; } - partial void InitProjSpecific() - { - //CreateGUI(); - } - protected override void OnResolutionChanged() { - base.OnResolutionChanged(); - OnItemLoadedProjSpecific(); + if (GuiFrame != null) + { + OnItemLoadedProjSpecific(); + } } protected override void CreateGUI() @@ -162,7 +157,7 @@ namespace Barotrauma.Items.Components // === ACTIVATE BUTTON === // var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), - TextManager.Get(CreateButtonText), style: "DeviceButton") + TextManager.Get(CreateButtonText), style: "DeviceButtonFixedSize") { OnClicked = StartButtonClicked, UserData = selectedItem, @@ -173,7 +168,7 @@ namespace Barotrauma.Items.Components { bottomFrame.RectTransform.RelativeSize = new Vector2(1.0f, 0.1f); activateButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomFrame.RectTransform, Anchor.CenterRight), - TextManager.Get(CreateButtonText), style: "DeviceButton") + TextManager.Get(CreateButtonText), style: "DeviceButtonFixedSize") { OnClicked = StartButtonClicked, UserData = selectedItem, @@ -566,7 +561,7 @@ namespace Barotrauma.Items.Components LocalizedString itemName = GetRecipeNameAndAmount(selectedItem); LocalizedString name = itemName; - float quality = GetFabricatedItemQuality(selectedItem, user); + float quality = selectedItem.Quality ?? GetFabricatedItemQuality(selectedItem, user); if (quality > 0) { name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", itemName + '\n') @@ -636,7 +631,7 @@ namespace Barotrauma.Items.Components float requiredTime = overrideRequiredTime ?? (user == null ? selectedItem.RequiredTime : GetRequiredTime(selectedItem, user)); - if (requiredTime > 0.0f) + if ((int)requiredTime > 0) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont) @@ -778,6 +773,12 @@ namespace Barotrauma.Items.Components UInt16 userID = msg.ReadUInt16(); Character user = Entity.FindEntityByID(userID) as Character; + ushort reachedLimitCount = msg.ReadUInt16(); + for (int i = 0; i < reachedLimitCount; i++) + { + fabricationLimits[msg.ReadUInt32()] = 0; + } + State = newState; if (newState == FabricatorState.Stopped || recipeHash == 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index e956c1721..d6e5500c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -320,7 +320,7 @@ namespace Barotrauma.Items.Components } } - GUI.DrawString(spriteBatch, hudPos, texts[0], textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont); + GUI.DrawString(spriteBatch, hudPos, texts[0].Value, textColors[0] * alpha, Color.Black * 0.7f * alpha, 2, GUIStyle.SubHeadingFont, ForceUpperCase.No); hudPos.X += 5.0f; hudPos.Y += 24.0f * GameSettings.CurrentConfig.Graphics.TextScale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 3cc3610e0..a3a2867d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -77,7 +77,7 @@ namespace Barotrauma get { return base.Rect; } set { - cachedVisibleSize = null; + cachedVisibleExtents = null; base.Rect = value; } } @@ -213,11 +213,11 @@ namespace Barotrauma UpdateSpriteStates(0.0f); } - private Vector2? cachedVisibleSize; + private Rectangle? cachedVisibleExtents; public void ResetCachedVisibleSize() { - cachedVisibleSize = null; + cachedVisibleExtents = null; } public override bool IsVisible(Rectangle worldView) @@ -234,28 +234,39 @@ namespace Barotrauma return false; } - Vector2 size; - if (cachedVisibleSize.HasValue) + Rectangle extents; + if (cachedVisibleExtents.HasValue) { - size = cachedVisibleSize.Value; + extents = cachedVisibleExtents.Value; } else { - float padding = 100.0f; - size = new Vector2(rect.Width + padding, rect.Height + padding); + int padding = 100; + + Vector2 min = new Vector2(-rect.Width / 2 - padding, -rect.Height / 2 - padding); + Vector2 max = -min; + foreach (IDrawableComponent drawable in drawableComponents) { - size.X = Math.Max(drawable.DrawSize.X, size.X); - size.Y = Math.Max(drawable.DrawSize.Y, size.Y); + min.X = Math.Min(min.X, -drawable.DrawSize.X / 2); + min.Y = Math.Min(min.Y, -drawable.DrawSize.Y / 2); + max.X = Math.Max(max.X, drawable.DrawSize.X / 2); + max.Y = Math.Max(max.Y, drawable.DrawSize.Y / 2); } - size *= 0.5f; - cachedVisibleSize = size; + foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) + { + float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; + min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale, min.X); + min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y); + max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale, max.X); + max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y); + } + cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint()); } - //cache world position so we don't need to calculate it 4 times Vector2 worldPosition = WorldPosition; - if (worldPosition.X - size.X > worldView.Right || worldPosition.X + size.X < worldView.X) return false; - if (worldPosition.Y + size.Y < worldView.Y - worldView.Height || worldPosition.Y - size.Y > worldView.Y) return false; + if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; } + if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; } return true; } @@ -934,8 +945,8 @@ namespace Barotrauma { OnSelected = (component, userData) => { - string text = userData as string ?? ""; - AddTag(text); + if (!(userData is Identifier)) { return true; } + AddTag((Identifier)userData); textBox.Text = Tags; msgBox.Close(); return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 24f85d6bd..a36e4216b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -239,7 +239,7 @@ namespace Barotrauma } if (min.X > worldView.Right || max.X < worldView.X) { return false; } - if ( min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } + if (min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 41ab884b8..c59c2d2a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -6,23 +6,6 @@ using System.Linq; namespace Barotrauma.Networking { - struct TempClient - { - public string Name; - public Identifier PreferredJob; - public CharacterTeamType PreferredTeam; - public UInt16 NameID; - public UInt64 SteamID; - public byte ID; - public UInt16 CharacterID; - public float Karma; - public bool Muted; - public bool InGame; - public bool HasPermissions; - public bool IsOwner; - public bool AllowKicking; - } - partial class Client : IDisposable { public VoipSound VoipSound @@ -50,6 +33,8 @@ namespace Barotrauma.Networking public bool AllowKicking; + public bool IsDownloading; + public float Karma; public void UpdateSoundPosition() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 1d3c335a7..57ff660c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1899,44 +1899,6 @@ namespace Barotrauma.Networking if (gameStarted) { - string ownedSubmarineIndexes = inc.ReadString(); - if (ownedSubmarineIndexes != string.Empty) - { - string[] ownedIndexes = ownedSubmarineIndexes.Split(';'); - - if (GameMain.GameSession != null) - { - GameMain.GameSession.OwnedSubmarines = new List(); - for (int i = 0; i < ownedIndexes.Length; i++) - { - if (int.TryParse(ownedIndexes[i], out int index)) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) - { - GameMain.GameSession.OwnedSubmarines.Add(sub); - } - } - } - } - else - { - GameMain.NetLobbyScreen.ServerOwnedSubmarines = new List(); - for (int i = 0; i < ownedIndexes.Length; i++) - { - int index; - if (int.TryParse(ownedIndexes[i], out index)) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) - { - GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(sub); - } - } - } - } - } - if (Screen.Selected != GameMain.GameScreen) { new GUIMessageBox(TextManager.Get("PleaseWait"), TextManager.Get(allowSpectating ? "RoundRunningSpectateEnabled" : "RoundRunningSpectateDisabled")); @@ -1953,37 +1915,8 @@ namespace Barotrauma.Networking int clientCount = inc.ReadByte(); for (int i = 0; i < clientCount; i++) { - byte id = inc.ReadByte(); - UInt64 steamId = inc.ReadUInt64(); - UInt16 nameId = inc.ReadUInt16(); - string name = inc.ReadString(); - Identifier preferredJob = inc.ReadIdentifier(); - byte preferredTeam = inc.ReadByte(); - UInt16 characterID = inc.ReadUInt16(); - float karma = inc.ReadSingle(); - bool muted = inc.ReadBoolean(); - bool inGame = inc.ReadBoolean(); - bool hasPermissions = inc.ReadBoolean(); - bool isOwner = inc.ReadBoolean(); - bool allowKicking = inc.ReadBoolean() || IsServerOwner; + tempClients.Add(INetSerializableStruct.Read(inc)); inc.ReadPadBits(); - - tempClients.Add(new TempClient - { - ID = id, - NameID = nameId, - SteamID = steamId, - Name = name, - PreferredJob = preferredJob, - PreferredTeam = (CharacterTeamType)preferredTeam, - CharacterID = characterID, - Karma = karma, - Muted = muted, - InGame = inGame, - HasPermissions = hasPermissions, - IsOwner = isOwner, - AllowKicking = allowKicking - }); } if (NetIdUtils.IdMoreRecent(listId, LastClientListUpdateID)) @@ -2018,6 +1951,7 @@ namespace Barotrauma.Networking existingClient.InGame = tc.InGame; existingClient.IsOwner = tc.IsOwner; existingClient.AllowKicking = tc.AllowKicking; + existingClient.IsDownloading = tc.IsDownloading; GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); if (Screen.Selected != GameMain.NetLobbyScreen && tc.CharacterID > 0) { @@ -2584,7 +2518,7 @@ namespace Barotrauma.Networking switch (transfer.FileType) { case FileTransferType.Submarine: - new GUIMessageBox(TextManager.Get("ServerDownloadFinished"), TextManager.GetWithVariable("FileDownloadedNotification", "[filename]", transfer.FileName)); + //new GUIMessageBox(TextManager.Get("ServerDownloadFinished"), TextManager.GetWithVariable("FileDownloadedNotification", "[filename]", transfer.FileName)); var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } @@ -2644,7 +2578,6 @@ namespace Barotrauma.Networking NetLobbyScreen.FailedSubInfo failedOwnedSub = GameMain.NetLobbyScreen.FailedOwnedSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.StringRepresentation); if (failedOwnedSub != default) { - GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(newSub); GameMain.NetLobbyScreen.FailedOwnedSubs.Remove(failedOwnedSub); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index e088e3b15..de7227cc8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -166,9 +166,12 @@ namespace Barotrauma int votes = inc.ReadByte(); string subName = inc.ReadString(); List serversubs = new List(); - foreach (GUIComponent item in GameMain.NetLobbyScreen?.SubList?.Content?.Children) + if (GameMain.NetLobbyScreen?.SubList?.Content != null) { - if (item.UserData != null && item.UserData is SubmarineInfo) { serversubs.Add(item.UserData as SubmarineInfo); } + foreach (GUIComponent item in GameMain.NetLobbyScreen.SubList.Content.Children) + { + if (item.UserData != null && item.UserData is SubmarineInfo) { serversubs.Add(item.UserData as SubmarineInfo); } + } } SubmarineInfo sub = serversubs.FirstOrDefault(s => s.Name == subName); SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 519af766f..e758564c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -271,7 +271,10 @@ namespace Barotrauma.Particles StartRotationMax = StartRotationMin; } - if (CollisionRadius <= 0.0f) CollisionRadius = Sprites.Count > 0 ? 1 : Sprites[0].SourceRect.Width / 2.0f; + if (CollisionRadius <= 0.0f && UseCollision) + { + CollisionRadius = Sprites.Count > 0 ? Sprites[0].SourceRect.Width / 2.0f : 1; + } } public Vector2 CalculateEndPosition(Vector2 startPosition, Vector2 velocity) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index a1bbea8ab..b9aed65dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -1048,7 +1048,7 @@ namespace Barotrauma box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = 20; int elementSize = 30; - var listBox = new GUIListBox(new RectTransform(new Vector2(1, 0.9f), box.Content.RectTransform)); + var listBox = new GUIListBox(new RectTransform(new Vector2(1, 0.75f), box.Content.RectTransform)); new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform), TextManager.Get("leveleditor.levelobjname")) { CanBeFocused = false }; @@ -1059,13 +1059,13 @@ namespace Barotrauma var texturePathBox = new GUITextBox(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform)); foreach (LevelObjectPrefab prefab in LevelObjectPrefab.Prefabs) { - if (prefab.Sprites.FirstOrDefault() == null) continue; + if (prefab.Sprites.FirstOrDefault() == null) { continue; } texturePathBox.Text = Path.GetDirectoryName(prefab.Sprites.FirstOrDefault().FilePath.Value); break; } //this is nasty :( - newPrefab = new LevelObjectPrefab(null, null, Identifier.Empty); + newPrefab = new LevelObjectPrefab(null, null, new Identifier("No identifier")); new SerializableEntityEditor(listBox.Content.RectTransform, newPrefab, false, false); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index f8468ea3c..97398b63d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -828,7 +828,6 @@ namespace Barotrauma " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + " -banafterwrongpassword " + wrongPasswordBanBox.Selected.ToString() + " -karmaenabled " + (!karmaBox.Selected).ToString() + - " -karmapreset default" + " -maxplayers " + maxPlayersBox.Text; if (!string.IsNullOrWhiteSpace(passwordBox.Text)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index 681edc43a..c07116c26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -5,6 +5,7 @@ using System.Linq; using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Networking; +using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Steamworks.Data; @@ -94,7 +95,7 @@ namespace Barotrauma } GUIMessageBox msgBox = new GUIMessageBox( - TextManager.Get("WorkshopItemDownloadTitle"), + TextManager.Get("ModDownloadTitle"), "", Array.Empty(), relativeSize: (0.5f, 0.75f)); @@ -122,8 +123,6 @@ namespace Barotrauma return tb; } - var title = textBlock(TextManager.Get("ModDownloadTitle"), GUIStyle.SubHeadingFont, Alignment.Center); - innerLayoutSpacing(0.05f); var header = textBlock(TextManager.Get("ModDownloadHeader"), GUIStyle.Font); innerLayoutSpacing(0.05f); @@ -138,8 +137,8 @@ namespace Barotrauma void buttonContainerSpacing(float width) => new GUIFrame(new RectTransform((width, 1.0f), buttonContainer.RectTransform), style: null); - void button(LocalizedString text, Action action) - => new GUIButton(new RectTransform((0.3f, 1.0f), buttonContainer.RectTransform), text) + void button(LocalizedString text, Action action, float width = 0.3f) + => new GUIButton(new RectTransform((width, 1.0f), buttonContainer.RectTransform), text) { OnClicked = (_, __) => { @@ -159,6 +158,28 @@ namespace Barotrauma }); buttonContainerSpacing(0.1f); + var missingIds = missingPackages.Where( + mp => mp.WorkshopId != 0 + && ContentPackageManager.WorkshopPackages.All(wp + => wp.SteamWorkshopId != mp.WorkshopId)) + .Select(mp => mp.WorkshopId) + .ToArray(); + if (missingIds.Any()) + { + buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true); + buttonContainerSpacing(0.15f); + button(TextManager.Get("SubscribeToAllOnWorkshop"), () => + { + BulkDownloader.SubscribeToServerMods(missingIds, + rejoinEndpoint: GameMain.Client.ClientPeer.ServerConnection.EndPointString, + rejoinLobby: SteamManager.CurrentLobbyID, + rejoinServerName: GameMain.NetLobbyScreen.ServerName.Text); + GameMain.Client.Disconnect(); + GameMain.MainMenuScreen.Select(); + }, width: 0.7f); + buttonContainerSpacing(0.15f); + } + foreach (var p in missingPackages) { pendingDownloads.Enqueue(p); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index a4cf42674..b3949ac46 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -218,9 +218,6 @@ namespace Barotrauma public MultiPlayerCampaignSetupUI CampaignSetupUI; - // Passed onto the gamesession when created - public List ServerOwnedSubmarines = new List(); - public bool UsingShuttle { get { return shuttleTickBox.Selected; } @@ -2007,7 +2004,7 @@ namespace Barotrauma SelectedTextColor = Color.Black, UserData = client }; - var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, + var soundIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, sprite: GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { UserData = new Pair("soundicon", 0.0f), @@ -2017,7 +2014,7 @@ namespace Barotrauma HoverColor = Color.White }; - new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, + var soundIconDisabled = new GUIImage(new RectTransform(Vector2.One * 0.8f, textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, "GUISoundIconDisabled") { UserData = "soundicondisabled", @@ -2026,15 +2023,55 @@ namespace Barotrauma OverrideState = GUIComponent.ComponentState.None, HoverColor = Color.White }; - new GUIFrame(new RectTransform(new Vector2(0.6f, 0.6f), textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(10 + soundIcon.Rect.Width, 0) }, style: "GUIReadyToStart") + + var readyTick = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.6f), textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(10 + soundIcon.Rect.Width, 0) }, style: "GUIReadyToStart") { Visible = false, CanBeFocused = false, ToolTip = TextManager.Get("ReadyToStartTickBox"), UserData = "clientready" }; + + var downloadingThrobber = new GUICustomComponent( + new RectTransform(Vector2.One, textBlock.RectTransform, scaleBasis: ScaleBasis.BothHeight), + onUpdate: null, + onDraw: DrawDownloadThrobber(client, soundIcon, soundIconDisabled, readyTick)); } + private Action DrawDownloadThrobber(Client client, params GUIComponent[] otherComponents) + => (sb, c) => DrawDownloadThrobber(client, otherComponents, sb, c); //poor man's currying + + private void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component) + { + if (!client.IsDownloading) + { + component.ToolTip = ""; + return; + } + + component.HideElementsOutsideFrame = false; + int drawRectX = otherComponents.Where(c => c.Visible) + .Select(c => c.Rect) + .Concat(new Rectangle(component.Parent.Rect.Right, component.Parent.Rect.Y, 0, component.Parent.Rect.Height).ToEnumerable()) + .Min(r => r.X) - component.Parent.Rect.Height - 10; + Rectangle drawRect + = new Rectangle(drawRectX, component.Rect.Y, component.Parent.Rect.Height, component.Parent.Rect.Height); + component.RectTransform.AbsoluteOffset = drawRect.Location - component.Parent.Rect.Location; + component.RectTransform.NonScaledSize = drawRect.Size; + var sheet = GUIStyle.GenericThrobber; + sheet.Draw( + spriteBatch, + pos: drawRect.Location.ToVector2(), + spriteIndex: (int)Math.Floor(Timing.TotalTime * 24.0f) % sheet.FrameCount, + color: Color.White, + origin: Vector2.Zero, rotate: 0.0f, + scale: Vector2.One * component.Parent.Rect.Height / sheet.FrameSize.ToVector2()); + if (component.ToolTip.IsNullOrEmpty()) + { + component.ToolTip = TextManager.Get("PlayerIsDownloadingFiles"); + } + } + public void SetPlayerNameAndJobPreference(Client client) { var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); @@ -2160,17 +2197,16 @@ namespace Barotrauma Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); })); - options.Add(new ContextMenuOption("ModerationMenu.UserDetails", isEnabled: true, onSelected: delegate + options.Add(new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: delegate { GameMain.NetLobbyScreen?.SelectPlayer(client); })); - // Creates sub context menu options for all the ranks - List permissionOptions = new List(); + List rankOptions = new List(); foreach (PermissionPreset rank in PermissionPreset.List) { - permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => + rankOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => { LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); @@ -2190,7 +2226,7 @@ namespace Barotrauma }) { Tooltip = rank.Description }); } - options.Add(new ContextMenuOption("Permissions", isEnabled: canPromo, options: permissionOptions.ToArray())); + options.Add(new ContextMenuOption("Rank", isEnabled: canPromo, options: rankOptions.ToArray())); Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; @@ -3554,12 +3590,6 @@ namespace Barotrauma ("[serverhash]", Md5Hash.GetShortHash(md5Hash))) + " "; } - //already showing a message about the same sub - if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "request" + subName)) - { - return false; - } - if (GameMain.Client.ServerSettings.AllowFileTransfers) { GameMain.Client?.RequestFile(FileTransferType.Submarine, subName, md5Hash); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 05723864f..4d72c0d65 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -793,7 +793,7 @@ namespace Barotrauma showEntitiesPanel.RectTransform.NonScaledSize = new Point( - (int)(paddedShowEntitiesPanel.RectTransform.Children.Max(c => (int)((c.GUIComponent as GUITickBox)?.TextBlock.TextSize.X ?? 0)) / paddedShowEntitiesPanel.RectTransform.RelativeSize.X), + (int)Math.Max(showEntitiesPanel.RectTransform.NonScaledSize.X, paddedShowEntitiesPanel.RectTransform.Children.Max(c => (int)((c.GUIComponent as GUITickBox)?.TextBlock.TextSize.X ?? 0)) / paddedShowEntitiesPanel.RectTransform.RelativeSize.X), (int)(paddedShowEntitiesPanel.RectTransform.Children.Sum(c => c.MinSize.Y) / paddedShowEntitiesPanel.RectTransform.RelativeSize.Y)); GUITextBlock.AutoScaleAndNormalize(paddedShowEntitiesPanel.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock)); @@ -1700,44 +1700,13 @@ namespace Barotrauma return false; } - string specialSavePath = ""; if (MainSub.Info.Type != SubmarineType.Player) { - Identifier typeIdentifier = MainSub.Info.Type.ToString().ToIdentifier(); - Type contentType = ContentFile.Types.FirstOrDefault(t - => !t.Type.IsAbstract - && t.Type.IsSubclassOf(typeof(BaseSubFile)) - && t.Names.Contains(typeIdentifier)) - ?.Type ?? - typeof(SubmarineFile); if (MainSub.Info.Type == SubmarineType.OutpostModule && MainSub.Info.OutpostModuleInfo != null) { - contentType = typeof(OutpostModuleFile); MainSub.Info.PreviewImage = null; } - - if (contentType != typeof(SubmarineFile)) - { -#if DEBUG - var existingFiles = GameMain.VanillaContent.GetFiles(contentType); - if (contentType == typeof(OutpostModuleFile)) - { - existingFiles = existingFiles.Where(f => f.Path.Value.Contains("Ruin") == MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin".ToIdentifier())); - } -#else - var existingFiles = ContentPackageManager.EnabledPackages.All - .Where(c => c != GameMain.VanillaContent) - .SelectMany(c => c.GetFiles(contentType)); -#endif - specialSavePath = existingFiles.FirstOrDefault(f => - ContentPackage.PathAllowedAsLocalModFile(f.Path.Value))?.Path.Value; - - if (!string.IsNullOrEmpty(specialSavePath)) - { - specialSavePath = Path.GetDirectoryName(specialSavePath); - } - } } else if (MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !MainSub.Info.HasTag(SubmarineTag.Shuttle)) { @@ -1758,35 +1727,7 @@ namespace Barotrauma return true; } - if (!string.IsNullOrEmpty(specialSavePath) && - (string.IsNullOrEmpty(MainSub?.Info.FilePath) || Path.GetFileNameWithoutExtension(MainSub.Info.Name) != nameBox.Text || Path.GetDirectoryName(MainSub?.Info.FilePath) != specialSavePath)) - { - string submarineTypeTag = $"SubmarineType.{MainSub.Info.Type}"; - if (MainSub.Info.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(submarineTypeTag)) - { - submarineTypeTag = "MissionType.Pirate"; - } - var msgBox = new GUIMessageBox("", TextManager.GetWithVariables("savesubtospecialfolderprompt", - ("[type]", TextManager.Get(submarineTypeTag)), ("[outpostpath]", specialSavePath)), - new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); - msgBox.Buttons[0].OnClicked = (bt, userdata) => - { - SaveSubToFile(nameBox.Text, specialSavePath); - saveFrame = null; - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked = (bt, userdata) => - { - SaveSubToFile(nameBox.Text); - saveFrame = null; - msgBox.Close(); - return true; - }; - return true; - } - - var result = SaveSubToFile(nameBox.Text, specialSavePath); + var result = SaveSubToFile(nameBox.Text); saveFrame = null; return result; } @@ -1798,13 +1739,9 @@ namespace Barotrauma if (p is null) { return; } if (!packageReloadQueue.Contains(p)) { packageReloadQueue.Enqueue(p); } } - - private bool SaveSubToFile(string name, string specialSavePath = null) - { - bool canModifyPackage(ContentPackage p) - => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; - Type subFileType = MainSub?.Info.Type switch + public static Type DetermineSubFileType(SubmarineType type) + => type switch { SubmarineType.Outpost => typeof(OutpostFile), SubmarineType.OutpostModule => typeof(OutpostModuleFile), @@ -1812,8 +1749,16 @@ namespace Barotrauma SubmarineType.Wreck => typeof(WreckFile), SubmarineType.BeaconStation => typeof(BeaconStationFile), SubmarineType.EnemySubmarine => typeof(EnemySubmarineFile), - SubmarineType.Player => typeof(SubmarineFile) + SubmarineType.Player => typeof(SubmarineFile), + _ => null }; + + private bool SaveSubToFile(string name) + { + bool canModifyPackage(ContentPackage p) + => p != null && ContentPackageManager.LocalPackages.Contains(p) && p != ContentPackageManager.VanillaCorePackage; + + Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath) { @@ -1858,11 +1803,13 @@ namespace Barotrauma foreach (var illegalChar in Path.GetInvalidFileNameChars()) { - if (!name.Contains(illegalChar)) continue; + if (!name.Contains(illegalChar)) { continue; } GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } + name = name.Trim(); + string newLocalModDir = $"{ContentPackage.LocalModsDir}/{name}"; var vanilla = GameMain.VanillaContent; @@ -1871,33 +1818,7 @@ namespace Barotrauma string savePath = name + ".sub"; string prevSavePath = null; - if (!string.IsNullOrEmpty(specialSavePath)) - { - string directoryName = specialSavePath; - savePath = Path.Combine(directoryName, savePath); - ContentPackage contentPackage = ContentPackageManager.EnabledPackages.All.FirstOrDefault(cp => cp.Files.Any(f => Path.GetDirectoryName(f.Path.Value) == directoryName)); - - if (!contentPackage.Files.Any(f => f.Path == savePath) && canModifyPackage(contentPackage)) - { - var msgBox = new GUIMessageBox("", TextManager.GetWithVariable("addtocontentpackageprompt", "[packagename]", contentPackage.Name), - new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); - msgBox.Buttons[0].OnClicked = (bt, userdata) => - { - ModProject modProject = new ModProject(contentPackage); - addSubAndSaveModProject(modProject, savePath, contentPackage.Path); - EnqueueForReload(contentPackage); - - msgBox.Close(); - return true; - }; - msgBox.Buttons[1].OnClicked = (bt, userdata) => - { - msgBox.Close(); - return true; - }; - } - } - else if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && + if (!string.IsNullOrEmpty(MainSub?.Info.FilePath) && MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = MainSub.Info.FilePath.CleanUpPath(); @@ -2515,7 +2436,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUIStyle.SubHeadingFont); - var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; + var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), MainSub?.Info.PreviewImage, scaleToFit: true); previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -2567,7 +2488,7 @@ 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.35f), rightColumn.RectTransform), style: null); + var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null); var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUIStyle.SmallFont); @@ -2613,11 +2534,30 @@ namespace Barotrauma }; } - var contentPackagesLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform, Anchor.TopRight), + var contentPackagesLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), + 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 contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), - horizontalArea.RectTransform, Anchor.BottomRight)); + var contentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), + contentPackagesLayout.RectTransform)); + + var contentPackFilter + = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.0f), contentPackagesLayout.RectTransform), + createClearButton: true); + contentPackFilter.OnTextChanged += (box, text) => + { + contentPackList.Content.Children.ForEach(c + => c.Visible = !(c is GUITickBox tb && + !tb.Text.Contains(text, StringComparison.OrdinalIgnoreCase))); + return true; + }; if (MainSub != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 177cb11a2..ee74766d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; +using System.Threading; using System.Xml.Linq; namespace Barotrauma @@ -479,6 +480,36 @@ namespace Barotrauma return sound.Play(volume ?? sound.BaseGain, far, freqMult ?? 1.0f, position, muffle: muffle); } + public static void DisposeDisabledMusic() + { + bool musicDisposed = false; + for (int i = 0; i < currentMusic.Length; i++) + { + var music = currentMusic[i]; + if (music is null) { continue; } + + if (!SoundPrefab.Prefabs.Contains(music)) + { + musicChannel[i].Dispose(); + musicDisposed = true; + currentMusic[i] = null; + } + } + + for (int i = 0; i < targetMusic.Length; i++) + { + var music = targetMusic[i]; + if (music is null) { continue; } + + if (!SoundPrefab.Prefabs.Contains(music)) + { + targetMusic[i] = null; + } + } + + if (musicDisposed) { Thread.Sleep(60); } + } + private static void UpdateMusic(float deltaTime) { if (musicClips == null || (GameMain.SoundManager?.Disabled ?? true)) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 8d510a115..14b0b68e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -121,6 +121,7 @@ namespace Barotrauma } if (prefabSelectors.ContainsKey(p.ElementName)) { prefabSelectors[p.ElementName].RemoveIfContains(p); } UpdateSoundsWithTag(); + SoundPlayer.DisposeDisabledMusic(); }, onSort: () => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs new file mode 100644 index 000000000..0542698d4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/BulkDownloader.cs @@ -0,0 +1,125 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Steam +{ + public static class BulkDownloader + { + public static void PrepareUpdates() + { + GUIMessageBox msgBox = new GUIMessageBox(headerText: "", text: TextManager.Get("DeterminingRequiredModUpdates"), + buttons: Array.Empty()); + TaskPool.Add( + "BulkDownloader.PrepareUpdates > GetItemsThatNeedUpdating", + GetItemsThatNeedUpdating(), + t => + { + msgBox.Close(); + if (!t.TryGetResult(out IReadOnlyList items)) { return; } + + InitiateDownloads(items); + }); + } + + internal static void SubscribeToServerMods(IEnumerable missingIds, string rejoinEndpoint, ulong rejoinLobby, string rejoinServerName) + { + GUIMessageBox msgBox = new GUIMessageBox(headerText: "", text: TextManager.Get("PreparingWorkshopDownloads"), + buttons: Array.Empty()); + TaskPool.Add( + "BulkDownloader.SubscribeToServerMods > GetItems", + Task.WhenAll(missingIds.Select(SteamManager.Workshop.GetItem)), + t => + { + msgBox.Close(); + if (!t.TryGetResult(out Steamworks.Ugc.Item?[] itemsNullable)) { return; } + + var items = itemsNullable + .Where(it => it.HasValue) + .Select(it => it ?? default) + .ToArray(); + + items.ForEach(it => it.Subscribe()); + InitiateDownloads(items, onComplete: () => + { + ContentPackageManager.UpdateContentPackageList(); + GameMain.Instance.ConnectEndpoint = rejoinEndpoint; + GameMain.Instance.ConnectLobby = rejoinLobby; + GameMain.Instance.ConnectName = rejoinServerName; + }); + }); + } + + private static async Task> GetItemsThatNeedUpdating() + { + var determiningTasks = ContentPackageManager.WorkshopPackages.Select(async p => (p, await p.IsUpToDate())); + (ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks); + + return (await Task.WhenAll(outOfDatePackages.Where(p => !p.IsUpToDate) + .Select(async p => await SteamManager.Workshop.GetItem(p.Package.SteamWorkshopId)))) + .Where(p => p.HasValue).Select(p => p ?? default).ToArray(); + } + + public static void InitiateDownloads(IReadOnlyList itemsToDownload, Action? onComplete = null) + { + var msgBox = new GUIMessageBox(TextManager.Get("WorkshopItemDownloading"), "", relativeSize: (0.5f, 0.6f), + buttons: new LocalizedString[] { TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked = msgBox.Close; + var modsList = new GUIListBox(new RectTransform((1.0f, 0.8f), msgBox.Content.RectTransform)) + { + HoverCursor = CursorState.Default + }; + foreach (var item in itemsToDownload) + { + var itemFrame = new GUIFrame(new RectTransform((1.0f, 0.08f), modsList.Content.RectTransform), + style: null) + { + CanBeFocused = false + }; + var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform), + text: item.Title); + var itemDownloadProgress + = new GUIProgressBar(new RectTransform((0.5f, 0.75f), + itemFrame.RectTransform, Anchor.CenterRight), 0.0f) + { + Color = GUIStyle.Green + }; + itemDownloadProgress.ProgressGetter = () => + { + float progress = 0.0f; + if (item.IsDownloading) { progress = item.DownloadAmount; } + else if (itemDownloadProgress.BarSize > 0.0f) { progress = 1.0f; } + + return Math.Max(itemDownloadProgress.BarSize, + MathHelper.Lerp(itemDownloadProgress.BarSize, progress, 0.05f)); + }; + } + TaskPool.Add("DownloadItems", DownloadItems(itemsToDownload, msgBox), _ => + { + if (GUIMessageBox.MessageBoxes.Contains(msgBox)) + { + onComplete?.Invoke(); + } + msgBox.Close(); + if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu) + { + mutableWorkshopMenu.PopulateInstalledModLists(); + } + }); + } + + private static async Task DownloadItems(IReadOnlyList itemsToDownload, GUIMessageBox msgBox) + { + foreach (var item in itemsToDownload) + { + await SteamManager.Workshop.Reinstall(item); + if (!GUIMessageBox.MessageBoxes.Contains(msgBox)) { break; } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index f94a64186..5b87ab120 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -1,10 +1,15 @@ #nullable enable +using System; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma.Steam { sealed class ImmutableWorkshopMenu : WorkshopMenu { + private readonly GUIListBox regularList; + private readonly GUITextBox filterBox; + public ImmutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout @@ -20,8 +25,8 @@ namespace Barotrauma.Steam coreBox.TextBlock.Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f); Label(mainLayout, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont); - var regularList = new GUIListBox( - NewItemRectT(mainLayout, heightScale: 12f)) + regularList = new GUIListBox( + NewItemRectT(mainLayout, heightScale: 11f)) { OnSelected = (component, o) => false, HoverCursor = CursorState.Default @@ -31,11 +36,22 @@ namespace Barotrauma.Steam var regularBox = new GUITextBlock( new RectTransform((1.0f, 0.07f), regularList.Content.RectTransform), text: p.Name) { - CanBeFocused = false + CanBeFocused = false, + UserData = p }; } + filterBox = CreateSearchBox(mainLayout, width: 1.0f); Label(mainLayout, TextManager.Get("CannotChangeMods"), GUIStyle.Font); } + + protected override void UpdateModListItemVisibility() + { + string str = filterBox.Text; + regularList.Content.Children + .ForEach(c => c.Visible = str.IsNullOrWhiteSpace() + || (c.UserData is ContentPackage p + && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index a66e87f55..4e01d5ece 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -32,6 +32,7 @@ namespace Barotrauma.Steam 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(); @@ -55,7 +56,8 @@ namespace Barotrauma.Steam out enabledRegularModsList, out disabledRegularModsList, out onInstalledInfoButtonHit, - out modsListFilter); + out modsListFilter, + out bulkUpdateButton); CreatePopularModsTab(out popularModsList); CreatePublishTab(out selfModsList); @@ -179,13 +181,35 @@ namespace Barotrauma.Steam 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 GUITextBox modsListFilter, + out GUIButton bulkUpdateButton) { GUIFrame content = CreateNewContentFrame(Tab.InstalledMods); @@ -196,49 +220,72 @@ namespace Barotrauma.Steam { if (itemOrPackage.TryGet(out Steamworks.Ugc.Item item)) { PopulateFrameWithItemInfo(item, selectedFrame); } }, - onDeselected: PopulateInstalledModLists, + onDeselected: () => PopulateInstalledModLists(), out onInstalledInfoButtonHit, out var deselect); GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter); mainLayout.RectTransform.SetAsFirstChild(); - GUILayoutGroup coreSelectionLayout = - new GUILayoutGroup(new RectTransform((0.5f, 0.15f), mainLayout.RectTransform)); - Label(coreSelectionLayout, TextManager.Get("enabledcore"), GUIStyle.SubHeadingFont, heightScale: 1.0f / 0.15f); - enabledCoreDropdown = Dropdown(coreSelectionLayout, + + 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 / 0.15f); + heightScale: 1.0f / 13.0f); + Label(topLeft, "", GUIStyle.SubHeadingFont, heightScale: 1.0f); + topRight.ChildAnchor = Anchor.CenterLeft; - var (left, center, right) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.78f); - right.ChildAnchor = Anchor.TopRight; - - Action swapFunc(GUIListBox from, GUIListBox to) + var topRightButtons = new GUILayoutGroup(new RectTransform((1.0f, 0.5f), topRight.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft) { - return () => - { - 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); - }; + Stretch = true, + RelativeSpacing = 0.05f + }; + + void padTopRight(float width=1.0f) + { + new GUIFrame(new RectTransform((width, 1.0f), topRightButtons.RectTransform), style: null); } - Action? currentCenterCallback = 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.92f), left.RectTransform)) + var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform)) { CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, @@ -248,7 +295,7 @@ namespace Barotrauma.Steam //disabled mods Label(right, TextManager.Get("disabledregular"), GUIStyle.SubHeadingFont); - var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.92f), right.RectTransform)) + var disabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), right.RectTransform)) { CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, @@ -265,7 +312,7 @@ namespace Barotrauma.Steam Visible = false, OnClicked = (button, o) => { - currentCenterCallback?.Invoke(); + currentSwapFunc?.Invoke(); return false; } }; @@ -277,7 +324,7 @@ namespace Barotrauma.Steam centerButton.Visible = true; centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleRight")); - currentCenterCallback = swapFunc(enabledModsList, disabledModsList); + SetSwapFunc(enabledModsList, disabledModsList); return true; }; @@ -288,30 +335,12 @@ namespace Barotrauma.Steam centerButton.Visible = true; centerButton.ApplyStyle(GUIStyle.GetComponentStyle("GUIButtonToggleLeft")); - currentCenterCallback = swapFunc(disabledModsList, enabledModsList); + SetSwapFunc(disabledModsList, enabledModsList); return true; }; - var searchRectT = NewItemRectT(mainLayout, heightScale: 1.0f); - searchRectT.RelativeSize = (0.5f, 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}, - textColor: Color.DarkGray * 0.6f, - text: TextManager.Get("Search") + "...", - textAlignment: Alignment.CenterLeft) - { - CanBeFocused = false - }; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); }; - - searchBox.OnTextChanged += (sender, str) => - { - UpdateModListItemVisibility(); - return true; - }; + var searchBox = CreateSearchBox(mainLayout, width: 0.5f); modsListFilter = searchBox; new GUICustomComponent(new RectTransform(Vector2.Zero, content.RectTransform), @@ -319,6 +348,18 @@ namespace Barotrauma.Steam { 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) => { @@ -327,7 +368,7 @@ namespace Barotrauma.Steam }); } - private void UpdateModListItemVisibility() + protected override void UpdateModListItemVisibility() { string str = modsListFilter.Text; enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children) @@ -336,8 +377,21 @@ namespace Barotrauma.Steam && p.Name.Contains(str, StringComparison.OrdinalIgnoreCase))); } - private void PopulateInstalledModLists() + 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, @@ -353,6 +407,39 @@ namespace Barotrauma.Steam { 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) { @@ -392,20 +479,14 @@ namespace Barotrauma.Steam { var infoButton = new GUIButton( new RectTransform(Vector2.One, frameContent.RectTransform, scaleBasis: ScaleBasis.Smallest), "", - style: "WorkshopMenu.InfoButton") + style: null) { + CanBeSelected = false, OnClicked = (button, o) => { - 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); - }); + PrepareToShowModInfo(mod); return false; - }, - ToolTip = TextManager.Get("ViewModDetails") + } }; if (!SteamManager.IsInitialized) { @@ -420,27 +501,69 @@ namespace Barotrauma.Steam 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)); - enabledRegularModsList.ClearChildren(); - for (int i = 0; i < ContentPackageManager.EnabledPackages.Regular.Count; i++) - { - var mod = ContentPackageManager.EnabledPackages.Regular[i]; - addRegularModToList(mod, enabledRegularModsList); - } + addRegularModsToList(enabledMods, enabledRegularModsList); + if (refreshDisabled) { addRegularModsToList(disabledMods, disabledRegularModsList); } - disabledRegularModsList.ClearChildren(); - foreach (var mod in ContentPackageManager.RegularPackages) - { - if (ContentPackageManager.EnabledPackages.Regular.Contains(mod)) { continue; } - addRegularModToList(mod, 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(); } @@ -468,7 +591,8 @@ namespace Barotrauma.Steam { ContentPackageManager.EnabledPackages.SetCore(EnabledCorePackage); ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children - .Where(c => c.UserData is RegularPackage).Select(c => (RegularPackage)c.UserData).ToArray()); + .Select(c => c.UserData as RegularPackage).OfType().ToArray()); + PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index 910fe6d1f..78a08a529 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -105,5 +105,29 @@ namespace Barotrauma.Steam protected GUIComponent CreateActionCarrier(GUIComponent parent, Identifier id, Action action) => 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) + { + 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}, + textColor: Color.DarkGray * 0.6f, + text: TextManager.Get("Search") + "...", + textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = searchBox.Text.IsNullOrWhiteSpace(); }; + + searchBox.OnTextChanged += (sender, str) => + { + UpdateModListItemVisibility(); + return true; + }; + return searchBox; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs index 1a3e8f53c..ebe59aaf8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/WorkshopMenu.cs @@ -4,9 +4,8 @@ namespace Barotrauma.Steam { abstract partial class WorkshopMenu { - public WorkshopMenu(GUIFrame parent) - { - - } + public WorkshopMenu(GUIFrame parent) { } + + protected abstract void UpdateModListItemVisibility(); } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 92e9d579c..fceac628c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.5.0 + 0.17.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 9f9f372a2..cc6dc075d 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.5.0 + 0.17.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 8ffe5ccb4..77da65846 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.5.0 + 0.17.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index ab23d88c7..0abfeb8ce 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.5.0 + 0.17.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 3e74a9d79..3c9b6adea 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.5.0 + 0.17.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index c1a0eff87..7638769f0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1116,7 +1116,7 @@ namespace Barotrauma } else { - GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", "", t.Character.Name), Identifier.Empty, TraitorMessageType.Console); + GameMain.Server.SendTraitorMessage(client, string.Format("- Traitor {0} has no current objective.", t.Character.Name), Identifier.Empty, TraitorMessageType.Console); } } //GameMain.Server.SendTraitorMessage(client, "The code words are: " + traitorManager.CodeWords + ", response: " + traitorManager.CodeResponse + ".", TraitorMessageType.Console); @@ -2234,11 +2234,6 @@ namespace Barotrauma if (args.Length < 2) { GameMain.Server.SendConsoleMessage("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks.", senderClient, Color.Red); - return; - } - - if (args.Length < 2) - { ThrowError("Invalid parameters. The command should be formatted as \"setclientcharacter [client] [character]\". If the names consist of multiple words, you should surround them with quotation marks."); return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index 484ebf03d..d04255896 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -7,7 +7,7 @@ namespace Barotrauma { partial class CargoManager { - public void SellBackPurchasedItems(Identifier storeIdentifier, List itemsToSell, Client client = null) + public void SellBackPurchasedItems(Identifier storeIdentifier, List itemsToSell, Client client) { // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab)); @@ -40,7 +40,7 @@ namespace Barotrauma } } - public void SellItems(Identifier storeIdentifier, List itemsToSell, Client client) + public void SellItems(Identifier storeIdentifier, List itemsToSell) { var store = Location.GetStore(storeIdentifier); if (store == null) { return; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index d509c9ae9..cef019215 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -564,6 +564,21 @@ namespace Barotrauma msg.Write((byte)selectedMissionIndex); } + 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)) + { + 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); } @@ -768,8 +783,12 @@ namespace Barotrauma if (Map.SelectedConnection != null) { Map.SelectMission(selectedMissionIndices); } CheckTooManyMissions(Map.CurrentLocation, sender); } - - var prevBuyCrateItems = new Dictionary>(CargoManager.ItemsInBuyCrate); + + var prevBuyCrateItems = new Dictionary>(); + foreach (var kvp in CargoManager.ItemsInBuyCrate) + { + prevBuyCrateItems.Add(kvp.Key, new List(kvp.Value)); + } foreach (var store in prevBuyCrateItems) { foreach (var item in store.Value) @@ -784,10 +803,15 @@ namespace Barotrauma CargoManager.ModifyItemQuantityInBuyCrate(store.Key, item.ItemPrefab, item.Quantity, sender); } } - var prevPurchasedItems = new Dictionary>(CargoManager.PurchasedItems); + + var prevPurchasedItems = new Dictionary>(); + foreach (var kvp in CargoManager.PurchasedItems) + { + prevPurchasedItems.Add(kvp.Key, new List(kvp.Value)); + } foreach (var store in prevPurchasedItems) { - CargoManager.SellBackPurchasedItems(store.Key, store.Value); + CargoManager.SellBackPurchasedItems(store.Key, store.Value, sender); } foreach (var store in purchasedItems) { @@ -813,6 +837,7 @@ namespace Barotrauma } } } + bool allowedToSellInventoryItems = AllowedToManageCampaign(sender, ClientPermissions.SellInventoryItems); if (allowedToSellInventoryItems && allowedToSellSubItems) { @@ -825,7 +850,7 @@ namespace Barotrauma } foreach (var store in soldItems) { - CargoManager.SellItems(store.Key, store.Value, sender); + CargoManager.SellItems(store.Key, store.Value); } } else if (allowedToSellInventoryItems || allowedToSellSubItems) @@ -842,7 +867,7 @@ namespace Barotrauma } foreach (var store in soldItems) { - CargoManager.SellItems(store.Key, store.Value, sender); + CargoManager.SellItems(store.Key, store.Value); } bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs index 8ffcf9742..347ef4ed5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Fabricator.cs @@ -1,9 +1,6 @@ using Barotrauma.Networking; -using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -15,7 +12,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); - if (!item.CanClientAccess(c)) return; + if (!item.CanClientAccess(c)) { return; } if (recipeHash == 0) { @@ -64,6 +61,13 @@ namespace Barotrauma.Items.Components msg.Write(recipeHash); UInt16 userID = fabricatedItem is null || user is null ? (UInt16)0 : user.ID; msg.Write(userID); + + var reachedLimits = fabricationLimits.Where(kvp => kvp.Value <= 0); + msg.Write((ushort)reachedLimits.Count()); + foreach (var kvp in reachedLimits) + { + msg.Write(kvp.Key); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index ad2438c2d..c324baf78 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Networking public bool CompareTo(string endpointCompare) { - if (string.IsNullOrEmpty(EndPoint) || string.IsNullOrEmpty(EndPoint)) { return false; } + if (string.IsNullOrEmpty(EndPoint) || string.IsNullOrEmpty(endpointCompare)) { return false; } if (!IsRangeBan) { return endpointCompare == EndPoint; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 4c1b87008..408c3ae61 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -173,13 +173,14 @@ namespace Barotrauma.Networking } OnStarted(transfer); + GameMain.Server.LastClientListUpdateID++; return transfer; } public void Update(float deltaTime) { - activeTransfers.RemoveAll(t => t.Connection.Status != NetworkConnectionStatus.Connected); + int numRemoved = activeTransfers.RemoveAll(t => t.Connection.Status != NetworkConnectionStatus.Connected); var endedTransfers = activeTransfers.FindAll(t => t.Connection.Status != NetworkConnectionStatus.Connected || @@ -202,6 +203,11 @@ namespace Barotrauma.Networking Send(transfer); } } + + if (numRemoved > 0 || endedTransfers.Count > 0) + { + GameMain.Server.LastClientListUpdateID++; + } } private void Send(FileTransferOut transfer) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs index 93a36099e..f48d85f4d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/ModSender.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Networking string resultFileName = dir.StartsWith(ContentPackage.LocalModsDir) ? $"Local_{mod.Name}" - : $"Workshop_{mod.Name}"; + : $"Workshop_{mod.Name}_{mod.SteamWorkshopId}"; resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_')); resultFileName = $"{resultFileName}{Extension}"; return Path.Combine(UploadFolder, resultFileName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 0e2b53f6e..ce82de0fc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -1587,25 +1587,6 @@ namespace Barotrauma.Networking outmsg.Write(serverSettings.AllowSpectating); c.WritePermissions(outmsg); - - if (gameStarted) - { - string ownedSubmarineIndexes = string.Empty; - for (int i = 0; i < subList.Count; i++) - { - if (GameMain.GameSession.OwnedSubmarines.Contains(subList[i])) - { - ownedSubmarineIndexes += i.ToString(); - ownedSubmarineIndexes += ";"; - } - } - - if (ownedSubmarineIndexes.Length > 0) - { - ownedSubmarineIndexes.Trim(';'); - } - outmsg.Write(ownedSubmarineIndexes); - } } private void ClientWriteIngame(Client c) @@ -1807,29 +1788,30 @@ namespace Barotrauma.Networking outmsg.Write((byte)connectedClients.Count); foreach (Client client in connectedClients) { - outmsg.Write(client.ID); - outmsg.Write(client.SteamID); - outmsg.Write(client.NameID); - outmsg.Write(client.Name); - outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : client.PreferredJob); - outmsg.Write((byte)client.PreferredTeam); - outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); - if (c.HasPermission(ClientPermissions.ServerLog)) + var tempClientData = new TempClient { - outmsg.Write(client.Karma); - } - else - { - outmsg.Write(100.0f); - } - outmsg.Write(client.Muted); - outmsg.Write(client.InGame); - outmsg.Write(client.Permissions != ClientPermissions.None); - outmsg.Write(client.Connection == OwnerConnection); - outmsg.Write(client.Connection != OwnerConnection && - !client.HasPermission(ClientPermissions.Ban) && - !client.HasPermission(ClientPermissions.Kick) && - !client.HasPermission(ClientPermissions.Unban)); //is kicking the player allowed + ID = client.ID, + SteamID = client.SteamID, + NameID = client.NameID, + Name = client.Name, + PreferredJob = client.Character?.Info?.Job != null && gameStarted + ? client.Character.Info.Job.Prefab.Identifier + : client.PreferredJob, + PreferredTeam = client.PreferredTeam, + CharacterID = client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID, + Karma = c.HasPermission(ClientPermissions.ServerLog) ? client.Karma : 100.0f, + Muted = client.Muted, + InGame = client.InGame, + HasPermissions = client.Permissions != ClientPermissions.None, + IsOwner = client.Connection == OwnerConnection, + AllowKicking = client.Connection != OwnerConnection && + !client.HasPermission(ClientPermissions.Ban) && + !client.HasPermission(ClientPermissions.Kick) && + !client.HasPermission(ClientPermissions.Unban), + IsDownloading = FileSender.ActiveTransfers.Any(t => t.Connection == client.Connection) + }; + + outmsg.Write(tempClientData); outmsg.WritePadBits(); } } @@ -3218,7 +3200,7 @@ namespace Barotrauma.Networking { Voting.ActiveVote.Finish(Voting, passed: false); } - else if (yes / max >= serverSettings.VoteRequiredRatio) + else if (yes / (float)max >= serverSettings.VoteRequiredRatio) { Voting.ActiveVote.Finish(Voting, passed: true); } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 86864cc3c..a5861e794 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.5.0 + 0.17.6.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index e01c32de2..0edb7fa1d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -3450,7 +3450,7 @@ namespace Barotrauma { ChangeParams("wall", state, priority / 2); } - if (canAttackDoors) + if (canAttackDoors && IsAggressiveBoarder) { ChangeParams("door", state, priority / 2); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 83bcb0c42..4abcd54ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -376,19 +376,17 @@ namespace Barotrauma { Vector2 diff = currentPath.CurrentNode.WorldPosition - pos; bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; - if (nextLadderSameAsCurrent) + if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) { //climbing ladders -> don't move horizontally diff.X = 0.0f; } //at the same height as the waypoint - if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f) + float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); + float colliderSize = (collider.height / 2 + collider.radius) * 1.25f; + if (heightDiff < colliderSize) { float heightFromFloor = character.AnimController.GetHeightFromFloor(); - if (heightFromFloor <= 0.0f) - { - diff.Y = Math.Max(diff.Y, 100); - } // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. bool isAboveFloor = heightFromFloor > -0.1f; // If the next waypoint is horizontally far, we don't want to keep holding the ladders @@ -402,7 +400,10 @@ namespace Barotrauma // Try to change the ladder (hatches between two submarines) if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition)) { - nextLadder.Item.TryInteract(character, forceSelectKey: true); + if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) + { + NextNode(!doorsChecked); + } } } if (isAboveFloor || nextLadderSameAsCurrent) @@ -461,12 +462,16 @@ namespace Barotrauma bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y; var door = currentPath.CurrentNode.ConnectedDoor; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1)); - if (currentPath.CurrentNode.Stairs != null && currentPath.NextNode?.Stairs == null) + if (currentPath.CurrentNode.Stairs != null) { - margin = 1; - if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) + bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs; + if (!isNextNodeInSameStairs) { - isTargetTooLow = true; + margin = 1; + if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f) + { + isTargetTooLow = true; + } } } float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 9342c4239..9b05c7e0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -630,7 +630,7 @@ namespace Barotrauma public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); - public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T).Objective as T; + public T GetOrder() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T; /// /// Returns the last active objective of the specific type. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index e282061e2..c66746f9a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -24,6 +24,8 @@ namespace Barotrauma public bool IsAiming => wasAiming; public bool IsAimingMelee => wasAimingMelee; + protected bool Aiming => aiming || aimingMelee; + public float ArmLength => upperArmLength + forearmLength; public abstract GroundedMovementParams WalkParams { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index f898ac8d4..81b328b03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -193,7 +193,7 @@ namespace Barotrauma strongestImpact = 0.0f; } - if (aiming) + if (Aiming) { TargetMovement = TargetMovement.ClampLength(2); } @@ -233,7 +233,7 @@ namespace Barotrauma //don't flip when simply physics is enabled if (SimplePhysicsEnabled) { return; } - if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !aiming) + if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !Aiming) { if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 30fc791e6..7953deff1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -597,11 +597,11 @@ namespace Barotrauma { float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - if (Crouching && !movingHorizontally && !aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } + if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; } torsoAngle -= herpesStrength / 150.0f; torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); } - if (!aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) + if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue) { float headAngle = HeadAngle.Value; if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; } @@ -817,48 +817,16 @@ namespace Barotrauma Limb torso = GetLimb(LimbType.Torso); if (head == null) { return; } if (torso == null) { return; } - - //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside + + const float DisableMovementAboveSurfaceThreshold = 50.0f; + if (currentHull != null && character.CurrentHull != null) { - float surfacePos = currentHull.Surface; + float surfacePos = GetSurfaceY(); float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); - //if the hull is almost full of water, check if there's a water-filled hull above it - //and use its water surface instead of the current hull's - if (currentHull.Rect.Y - currentHull.Surface < 5.0f) - { - GetSurfacePos(currentHull, ref surfacePos); - void GetSurfacePos(Hull hull, ref float prevSurfacePos) - { - if (prevSurfacePos > surfaceThreshold) { return; } - foreach (Gap gap in hull.ConnectedGaps) - { - if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; } - if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; } - - //if the gap is above us and leads outside, there's no surface to limit the movement - if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) - { - prevSurfacePos += 100000.0f; - return; - } - - foreach (var linkedTo in gap.linkedTo) - { - if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) - { - prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); - GetSurfacePos(otherHull, ref prevSurfacePos); - break; - } - } - } - } - } - surfaceLimiter = Math.Max(1.0f, surfaceThreshold - surfacePos); - if (surfaceLimiter > 50.0f) { return; } - } + if (surfaceLimiter > DisableMovementAboveSurfaceThreshold) { return; } + } Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); @@ -872,25 +840,30 @@ namespace Barotrauma { rotation += 360; } - if (!character.IsRemotelyControlled && !aiming && Anim != Animation.UsingConstruction && - !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) + float targetSpeed = TargetMovement.Length(); + if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !character.IsKeyDown(InputType.Aim)) { - if (rotation > 20 && rotation < 170) + if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false)) { - TargetDir = Direction.Left; - } - else if (rotation > 190 && rotation < 340) - { - TargetDir = Direction.Right; + if (rotation > 20 && rotation < 170) + { + TargetDir = Direction.Left; + } + else if (rotation > 190 && rotation < 340) + { + TargetDir = Direction.Right; + } } } - float targetSpeed = TargetMovement.Length(); - if (aiming) + if (Aiming) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - torso.SimPosition) * Dir; - float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + if (diff.LengthSquared() > MathUtils.Pow2(0.4f)) + { + float newRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } } else if (targetSpeed > 0.1f) { @@ -911,7 +884,7 @@ namespace Barotrauma torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } - if (!aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue) + if (!Aiming && CurrentSwimParams.FixedHeadAngle && HeadAngle.HasValue) { head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } @@ -940,7 +913,7 @@ namespace Barotrauma head.body.ApplyTorque(Dir); } - movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / 50.0f)); + movement.Y = movement.Y * (1.0f - ((surfaceLimiter - 1.0f) / DisableMovementAboveSurfaceThreshold)); } bool isNotRemote = true; @@ -1141,10 +1114,9 @@ namespace Barotrauma bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y); if (climbFast) { handPos.Y -= stepHeight; } - bool aiming = this.aiming || aimingMelee; //prevent the hands from going above the top of the ladders handPos.Y = Math.Min(-0.5f, handPos.Y); - if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) + if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) { MoveLimb(rightHand, new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X, @@ -1152,7 +1124,7 @@ namespace Barotrauma 5.2f); rightHand.body.ApplyTorque(Dir * 2.0f); } - if (!aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) + if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f) { MoveLimb(leftHand, new Vector2(handPos.X - ladderSimSize.X * 0.5f, @@ -1235,7 +1207,7 @@ namespace Barotrauma //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass); - if (aiming) + if (Aiming) { RotateHead(head); } @@ -1526,11 +1498,14 @@ namespace Barotrauma return; } Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso); - if (targetTorso == null) targetTorso = target.AnimController.MainLimb; - + if (targetTorso == null) + { + targetTorso = target.AnimController.MainLimb; + } if (target.AnimController.Dir != Dir) + { target.AnimController.Flip(); - + } Vector2 transformedTorsoPos = torso.SimPosition; if (character.Submarine == null && target.Submarine != null) { @@ -1574,7 +1549,10 @@ namespace Barotrauma { //only grab with one hand when swimming leftHand.Disabled = true; - if (!inWater) rightHand.Disabled = true; + if (!inWater) + { + rightHand.Disabled = true; + } for (int i = 0; i < 2; i++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 9277a79ee..294285a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -1193,13 +1193,9 @@ namespace Barotrauma headInWater = false; inWater = false; RefreshFloorY(ignoreStairs: Stairs == null); - if (currentHull.WaterVolume > currentHull.Volume * 0.95f) + if (currentHull.WaterPercentage > 0.001f) { - inWater = true; - } - else - { - float waterSurface = ConvertUnits.ToSimUnits(currentHull.Surface); + float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY()); if (targetMovement.Y < 0.0f) { Vector2 colliderBottom = GetColliderBottom(); @@ -1212,11 +1208,8 @@ namespace Barotrauma if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); } } - float standHeight = - HeadPosition.HasValue ? HeadPosition.Value : - TorsoPosition.HasValue ? TorsoPosition.Value : - Collider.GetMaxExtent() * 0.5f; - if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.95f) + float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f; + if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f) { inWater = true; } @@ -1521,7 +1514,6 @@ namespace Barotrauma } } - private float GetFloorY(Vector2 simPosition, bool ignoreStairs = false) { onGround = false; @@ -1640,6 +1632,51 @@ namespace Barotrauma } } + public float GetSurfaceY() + { + //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside + if (currentHull == null || character.CurrentHull == null) + { + return float.PositiveInfinity; + } + + float surfacePos = currentHull.Surface; + float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); + //if the hull is almost full of water, check if there's a water-filled hull above it + //and use its water surface instead of the current hull's + if (currentHull.Rect.Y - currentHull.Surface < 5.0f) + { + GetSurfacePos(currentHull, ref surfacePos); + void GetSurfacePos(Hull hull, ref float prevSurfacePos) + { + if (prevSurfacePos > surfaceThreshold) { return; } + foreach (Gap gap in hull.ConnectedGaps) + { + if (gap.IsHorizontal || gap.Open <= 0.0f || gap.WorldPosition.Y < hull.WorldPosition.Y) { continue; } + if (Collider.SimPosition.X < ConvertUnits.ToSimUnits(gap.Rect.X) || Collider.SimPosition.X > ConvertUnits.ToSimUnits(gap.Rect.Right)) { continue; } + + //if the gap is above us and leads outside, there's no surface to limit the movement + if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y) + { + prevSurfacePos += 100000.0f; + return; + } + + foreach (var linkedTo in gap.linkedTo) + { + if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) + { + prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); + GetSurfacePos(otherHull, ref prevSurfacePos); + break; + } + } + } + } + } + return surfacePos; + } + public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true) { if (!MathUtils.IsValid(simPosition)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index e405a887e..e524f3a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -349,7 +349,6 @@ namespace Barotrauma DamageRange = range; StructureDamage = LevelWallDamage = structureDamage; ItemDamage = itemDamage; - Penetration = Penetration; } public Attack(ContentXElement element, string parentDebugName, Item sourceItem) : this(element, parentDebugName) @@ -359,7 +358,7 @@ namespace Barotrauma public Attack(ContentXElement element, string parentDebugName) { - Deserialize(element); + Deserialize(element, parentDebugName); if (element.GetAttribute("damage") != null || element.GetAttribute("bluntdamage") != null || @@ -423,7 +422,7 @@ namespace Barotrauma } partial void InitProjSpecific(ContentXElement element); - public void ReloadAfflictions(XElement element) + public void ReloadAfflictions(XElement element, string parentDebugName) { Afflictions.Clear(); foreach (var subElement in element.GetChildElements("affliction")) @@ -431,6 +430,11 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; Affliction affliction; Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", ""); + if (!AfflictionPrefab.Prefabs.ContainsKey(afflictionIdentifier)) + { + DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\"."); + continue; + } afflictionPrefab = AfflictionPrefab.Prefabs[afflictionIdentifier]; affliction = afflictionPrefab.Instantiate(0.0f); affliction.Deserialize(subElement); @@ -456,10 +460,10 @@ namespace Barotrauma } } - public void Deserialize(XElement element) + public void Deserialize(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - ReloadAfflictions(element); + ReloadAfflictions(element, parentDebugName); } public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 4d307d4c7..5182ff0b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -556,6 +556,12 @@ namespace Barotrauma #if CLIENT CharacterHealth.SetHealthBarVisibility(value == null); +#elif SERVER + if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet }) + { + Wallet.Give(balance); + grabbedWallet.Deduct(balance); + } #endif } } @@ -1180,7 +1186,7 @@ namespace Barotrauma CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement); } - if (Params.Husk) + if (Params.Husk && speciesName != "husk") { // Get the non husked name and find the ragdoll with it var matchingAffliction = AfflictionPrefab.List @@ -1764,26 +1770,44 @@ namespace Barotrauma } if (!aiControlled && - AnimController.OnGround && - !AnimController.InWater && AnimController.Anim != AnimController.Animation.UsingConstruction && AnimController.Anim != AnimController.Animation.CPR && - (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this)) + (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this) && + (AnimController.OnGround && !AnimController.InWater || IsKeyDown(InputType.Aim) && HeldItems.None(i => i.RequireAimToUse))) { - //Limb head = AnimController.GetLimb(LimbType.Head); - // Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping. - float followMargin = 40; if (dontFollowCursor) { AnimController.TargetDir = Direction.Right; } - else if (cursorPosition.X < AnimController.Collider.Position.X - followMargin) + else { - AnimController.TargetDir = Direction.Left; - } - else if (cursorPosition.X > AnimController.Collider.Position.X + followMargin) - { - AnimController.TargetDir = Direction.Right; + // Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping. + float followMargin = 40; + Vector2 diff = CursorPosition - AnimController.Collider.Position; + if (InWater) + { + followMargin = 80; + diff = Vector2.Transform(diff, Matrix.CreateRotationZ(-AnimController.Collider.Rotation)); + if (diff.X < followMargin) + { + AnimController.TargetDir = Direction.Left; + } + else if (diff.X > followMargin) + { + AnimController.TargetDir = Direction.Right; + } + } + else + { + if (CursorPosition.X < AnimController.Collider.Position.X - followMargin) + { + AnimController.TargetDir = Direction.Left; + } + else if (CursorPosition.X > AnimController.Collider.Position.X + followMargin) + { + AnimController.TargetDir = Direction.Right; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 86957c713..6952cd722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -53,16 +53,18 @@ namespace Barotrauma continue; } var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null); - if (vitalityMultipliers == null) - { - vitalityMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); - } if (vitalityMultipliers != null) { float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); vitalityMultipliers.ForEach(i => VitalityMultipliers.Add(i, multiplier)); } - else + var vitalityTypeMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null); + if (vitalityTypeMultipliers != null) + { + float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f); + vitalityTypeMultipliers.ForEach(i => VitalityTypeMultipliers.Add(i, multiplier)); + } + if (vitalityMultipliers == null && VitalityTypeMultipliers == null) { DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 7db320395..608d7f16f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -1116,14 +1116,13 @@ namespace Barotrauma public AttackParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll) { - var prefab = CharacterPrefab.Prefabs[ragdoll.SpeciesName]; Attack = new Attack(element, ragdoll.SpeciesName.Value); } public override bool Deserialize(XElement element = null, bool recursive = true) { base.Deserialize(element, recursive); - Attack.Deserialize(element ?? Element); + Attack.Deserialize(element ?? Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); return SerializableProperties != null; } @@ -1137,8 +1136,8 @@ namespace Barotrauma public override void Reset() { base.Reset(); - Attack.Deserialize(OriginalElement); - Attack.ReloadAfflictions(OriginalElement); + Attack.Deserialize(OriginalElement, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); + Attack.ReloadAfflictions(OriginalElement, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); } public bool AddNewAffliction() @@ -1149,7 +1148,7 @@ namespace Barotrauma new XAttribute("strength", 0f), new XAttribute("probability", 1.0f)); Element.Add(subElement); - Attack.ReloadAfflictions(Element); + Attack.ReloadAfflictions(Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); Serialize(); return true; } @@ -1158,7 +1157,7 @@ namespace Barotrauma { Serialize(); affliction.Remove(); - Attack.ReloadAfflictions(Element); + Attack.ReloadAfflictions(Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null"); return Serialize(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs index 71f5cd664..bff61b1bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ItemAssemblyFile.cs @@ -2,6 +2,7 @@ using System.Xml.Linq; namespace Barotrauma { + [NotSyncedInMultiplayer] sealed class ItemAssemblyFile : GenericPrefabFile { public ItemAssemblyFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs index de37b623a..9f21480f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/TextFile.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index 70f3ebdaf..add517a0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs @@ -156,6 +156,19 @@ namespace Barotrauma return -1; } + public static void DisableMods(IReadOnlyCollection mods) + { + if (Core != null && mods.Contains(Core)) + { + var newCore = ContentPackageManager.CorePackages.FirstOrDefault(p => !mods.Contains(p)); + if (newCore != null) + { + SetCore(newCore); + } + } + SetRegular(Regular.Where(p => !mods.Contains(p)).ToArray()); + } + public static void DisableRemovedMods() { if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 96bb78082..150aee8b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -40,22 +40,25 @@ namespace Barotrauma public readonly static PrefabCollection Prefabs = new PrefabCollection(); #if CLIENT - private static readonly Dictionary EventSprites = new Dictionary(); - public static Sprite GetEventSprite(string identifier) { if (string.IsNullOrWhiteSpace(identifier)) { return null; } - foreach (var (key, value) in EventSprites) + if (EventSprite.Prefabs.TryGet(identifier.ToIdentifier(), out EventSprite sprite)) { - if (key.Equals(identifier, StringComparison.OrdinalIgnoreCase)) { return value; } + return sprite.Sprite; } +#if DEBUG || UNSTABLE + DebugConsole.ThrowError($"Could not find the event sprite \"{identifier}\""); +#else + DebugConsole.AddWarning($"Could not find the event sprite \"{identifier}\""); +#endif return null; } #endif - public static List GetAllEventPrefabs() + public static List GetAllEventPrefabs() { List eventPrefabs = EventPrefab.Prefabs.ToList(); foreach (var eventSet in Prefabs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index d7d042774..11ed0fd52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -172,7 +172,7 @@ namespace Barotrauma ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); } GiveReward(); - if (level?.LevelData != null) + if (level.LevelData != null) { level.LevelData.IsBeaconActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index ac1b2e40b..44a2cdf9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -95,23 +95,10 @@ namespace Barotrauma partial void InitProjSpecific(); - private GameSession(SubmarineInfo submarineInfo, List? ownedSubmarines = null) + private GameSession(SubmarineInfo submarineInfo) { InitProjSpecific(); SubmarineInfo = submarineInfo; - -#if CLIENT - if (ownedSubmarines == null && GameMode is MultiPlayerCampaign && GameMain.NetLobbyScreen.ServerOwnedSubmarines != null) - { - ownedSubmarines = GameMain.NetLobbyScreen.ServerOwnedSubmarines; - } -#endif - - OwnedSubmarines = ownedSubmarines ?? new List(); - if (!OwnedSubmarines.Any(s => s.Name == submarineInfo.Name)) - { - OwnedSubmarines.Add(submarineInfo); - } GameMain.GameSession = this; EventManager = new EventManager(); } @@ -125,6 +112,7 @@ namespace Barotrauma this.SavePath = savePath; CrewManager = new CrewManager(gameModePreset.IsSinglePlayer); GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType); + InitOwnedSubs(submarineInfo); } /// @@ -135,12 +123,13 @@ namespace Barotrauma { CrewManager = new CrewManager(gameModePreset.IsSinglePlayer); GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs); + InitOwnedSubs(submarineInfo); } /// /// Load a game session from the specified XML document. The session will be saved to the specified path. /// - public GameSession(SubmarineInfo submarineInfo, List ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo, ownedSubmarines) + public GameSession(SubmarineInfo submarineInfo, List ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo) { this.SavePath = saveFile; GameMain.GameSession = this; @@ -173,6 +162,16 @@ namespace Barotrauma break; } } + InitOwnedSubs(submarineInfo); + } + + private void InitOwnedSubs(SubmarineInfo submarineInfo, List? ownedSubmarines = null) + { + OwnedSubmarines = ownedSubmarines ?? new List(); + if (submarineInfo != null && !OwnedSubmarines.Any(s => s.Name == submarineInfo.Name)) + { + OwnedSubmarines.Add(submarineInfo); + } } private GameMode InstantiateGameMode(GameModePreset gameModePreset, string? seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable? missionPrefabs = null, MissionType missionType = MissionType.None) @@ -295,7 +294,8 @@ namespace Barotrauma Campaign!.GetWallet(client).TryDeduct(cost); } GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name); - + Campaign!.PendingSubmarineSwitch = newSubmarine; + return newSubmarine; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index ee10aee6e..4c6a29915 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -116,8 +116,6 @@ namespace Barotrauma.Items.Components this.fabricationRecipes = fabricationRecipes.ToImmutableDictionary(); state = FabricatorState.Stopped; - - InitProjSpecific(); } public override void OnItemLoaded() @@ -146,9 +144,6 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific(); - - partial void InitProjSpecific(); - public override bool Select(Character character) { SelectProjSpecific(character); @@ -192,7 +187,7 @@ namespace Barotrauma.Items.Components if (!isClient) { MoveIngredientsToInputContainer(selectedItem); - if (selectedItem.RequiredMoney > 0) + if (selectedItem.RequiredMoney > 0 && CanBeFabricated(fabricatedItem, availableIngredients, user)) { if (GameMain.GameSession?.GameMode is MultiPlayerCampaign) { @@ -395,14 +390,17 @@ namespace Barotrauma.Items.Components var fabricationitemAmount = new AbilityFabricationItemAmount(fabricatedItem.TargetItem, fabricatedItem.Amount); int quality = 0; - if (user?.Info != null) + if (fabricatedItem.Quality.HasValue) + { + quality = fabricatedItem.Quality.Value; + } + else if (user?.Info != null) { foreach (Character character in Character.GetFriendlyCrew(user)) { character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } - user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); - + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); quality = GetFabricatedItemQuality(fabricatedItem, user); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 411627eef..028e59a8e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1196,7 +1196,7 @@ namespace Barotrauma drawableComponents.Add(drawable); hasComponentsToDraw = true; #if CLIENT - cachedVisibleSize = null; + cachedVisibleExtents = null; #endif } } @@ -1208,7 +1208,7 @@ namespace Barotrauma drawableComponents.Remove(drawable); hasComponentsToDraw = drawableComponents.Count > 0; #if CLIENT - cachedVisibleSize = null; + cachedVisibleExtents = null; #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 8e25476cc..505837205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -118,6 +118,7 @@ namespace Barotrauma public readonly ImmutableArray RequiredSkills; public readonly uint RecipeHash; public readonly int Amount; + public readonly int? Quality; /// /// How many of this item the fabricator can create (< 0 = unlimited) @@ -150,6 +151,11 @@ namespace Barotrauma FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault); FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault); + if (element.GetAttribute(nameof(Quality)) != null) + { + Quality = element.GetAttributeInt(nameof(Quality), 0); + } + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 05539c6e2..227f59f3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -15,7 +15,6 @@ namespace Barotrauma public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies"); - public static readonly string SaveFolder = "ItemAssemblies"; private readonly XElement configElement; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index c3b30aac8..5fd728e3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -282,7 +282,7 @@ namespace Barotrauma private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level, Level.Cave parentCave = null) { float rotation = 0.0f; - if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null) + if (prefab.AlignWithSurface && spawnPosition != null && spawnPosition.Normal.LengthSquared() > 0.001f) { rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 84d2510c2..f39a30ee4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -359,8 +359,8 @@ namespace Barotrauma /// How many map progress steps it takes before the discounts should be updated. /// private const int SpecialsUpdateInterval = 3; - private int DailySpecialsCount => Type.DailySpecialsCount; - private int RequestedGoodsCount => Type.RequestedGoodsCount; + public int DailySpecialsCount => Type.DailySpecialsCount; + public int RequestedGoodsCount => Type.RequestedGoodsCount; private int StepsSinceSpecialsUpdated { get; set; } public HashSet StoreIdentifiers { get; } = new HashSet(); @@ -1138,7 +1138,7 @@ namespace Barotrauma { store.Balance = Math.Min(store.Balance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance); } - var stock = store.Stock; + var stock = new List(store.Stock); var stockToRemove = new List(); foreach (var item in stock) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 7e9901866..96a8e9028 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -89,22 +89,18 @@ namespace Barotrauma { backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}"; } - string[] storeIdentifiers = childElement.GetAttributeStringArray("storeidentifiers", new string[1] { backwardsCompatibleIdentifier }); - foreach (string id in storeIdentifiers) - { - if (string.IsNullOrEmpty(id)) { continue; } - // TODO: Add some error messages if we have defined the min or max amount while the item is not sold - var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), - sold, - sold ? GetMinAmount(childElement, minAmount) : 0, - sold ? GetMaxAmount(childElement, maxAmount) : 0, - canBeSpecial, - storeMinLevelDifficulty, - storeBuyingMultiplier, - displayNonEmpty, - id); - priceInfos.Add(priceInfo); - } + string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier); + // TODO: Add some error messages if we have defined the min or max amount while the item is not sold + var priceInfo = new PriceInfo((int)(priceMultiplier * basePrice), + sold, + sold ? GetMinAmount(childElement, minAmount) : 0, + sold ? GetMaxAmount(childElement, maxAmount) : 0, + canBeSpecial, + storeMinLevelDifficulty, + storeBuyingMultiplier, + displayNonEmpty, + storeIdentifier); + priceInfos.Add(priceInfo); } bool soldElsewhere = soldByDefault && element.GetAttributeBool("soldelsewhere", element.GetAttributeBool("soldeverywhere", false)); defaultPrice = new PriceInfo(basePrice, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 11b32da05..23a7d0e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -313,7 +313,16 @@ namespace Barotrauma for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) { var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); - if (previousWaypoint != null) { wayPoint.ConnectTo(previousWaypoint); } + // Too close to stairs, will be assigned as a stair point -> remove + if (wayPoint.FindStairs() != null) + { + removals.Add(wayPoint); + continue; + } + if (previousWaypoint != null) + { + wayPoint.ConnectTo(previousWaypoint); + } previousWaypoint = wayPoint; } if (previousWaypoint == null) @@ -510,25 +519,29 @@ namespace Barotrauma } } } + removals.ForEach(wp => wp.Remove()); + removals.Clear(); + // Stairs foreach (MapEntity mapEntity in mapEntityList.ToList()) { if (!(mapEntity is Structure structure)) { continue; } if (structure.StairDirection == Direction.None) { continue; } WayPoint[] stairPoints = new WayPoint[3]; + float margin = -32; - stairPoints[0] = new WayPoint( - new Vector2(structure.Rect.X - 32.0f, - structure.Rect.Y - (structure.StairDirection == Direction.Left ? 80 : structure.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); + stairPoints[0] = new WayPoint(new Vector2( + structure.Rect.X + 5, + structure.Rect.Y - (structure.StairDirection == Direction.Left ? margin : structure.Rect.Height - 100)), SpawnType.Path, submarine); - stairPoints[1] = new WayPoint( - new Vector2(structure.Rect.Right + 32.0f, - structure.Rect.Y - (structure.StairDirection == Direction.Left ? structure.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); + stairPoints[1] = new WayPoint(new Vector2( + structure.Rect.Right - 5, + structure.Rect.Y - (structure.StairDirection == Direction.Left ? structure.Rect.Height - 100 : margin)), SpawnType.Path, submarine); for (int i = 0; i < 2; i++) { for (int dir = -1; dir <= 1; dir += 2) { - WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70)); + WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.5f, minDist / 2)); if (closest == null) { continue; } stairPoints[i].ConnectTo(closest); } @@ -537,9 +550,8 @@ namespace Barotrauma stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine); stairPoints[0].ConnectTo(stairPoints[2]); stairPoints[2].ConnectTo(stairPoints[1]); + stairPoints.ForEach(wp => wp.FindStairs()); } - removals.ForEach(wp => wp.Remove()); - removals.Clear(); foreach (Item item in Item.ItemList) { @@ -840,7 +852,11 @@ namespace Barotrauma var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false); if (body != null && body != ignoredBody && !(body.UserData is Submarine)) { - if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + if (body.UserData is Structure) + { + continue; + } + if (body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall) && body.UserData is Item i && i.GetComponent() != null) { continue; } @@ -960,14 +976,15 @@ namespace Barotrauma FindStairs(); } - private void FindStairs() + private Structure FindStairs() { Stairs = null; - Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - Vector2.UnitY * 2.0f, null, Physics.CollisionStairs); + Body pickedBody = Submarine.PickBody(SimPosition, SimPosition - new Vector2(0, 1.2f), null, Physics.CollisionStairs); if (pickedBody != null && pickedBody.UserData is Structure structure && structure.StairDirection != Direction.None) { - Stairs = structure; + Stairs = structure; } + return Stairs; } public void InitializeLinks() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index daf4b37c4..b7edffbbb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -6,6 +6,25 @@ using System.Linq; namespace Barotrauma.Networking { + [NetworkSerialize] + struct TempClient : INetSerializableStruct + { + public string Name; + public Identifier PreferredJob; + public CharacterTeamType PreferredTeam; + public UInt16 NameID; + public UInt64 SteamID; + public byte ID; + public UInt16 CharacterID; + public float Karma; + public bool Muted; + public bool InGame; + public bool HasPermissions; + public bool IsOwner; + public bool AllowKicking; + public bool IsDownloading; + } + partial class Client : IDisposable { public const int MaxNameLength = 32; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 84690b56b..e7c0cecab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -58,10 +58,13 @@ namespace Barotrauma cam.Position = Submarine.MainSub.WorldPosition; cam.UpdateTransform(true); } + GameMain.GameSession?.CrewManager?.AutoShowCrewList(); #endif foreach (MapEntity entity in MapEntity.mapEntityList) + { entity.IsHighlighted = false; + } #if RUN_PHYSICS_IN_SEPARATE_THREAD var physicsThread = new Thread(ExecutePhysics) @@ -78,6 +81,10 @@ namespace Barotrauma base.Deselect(); #if CLIENT + var config = GameSettings.CurrentConfig; + config.CrewMenuOpen = CrewManager.PreferCrewMenuOpen; + config.ChatOpen = ChatBox.PreferChatBoxOpen; + GameSettings.SetCurrentConfig(config); GameSettings.SaveCurrentConfig(); GameMain.SoundManager.SetCategoryMuffle("default", false); GUI.ClearMessages(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index e44c75dd2..8f843ba7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -112,15 +112,17 @@ namespace Barotrauma.Steam var toUninstall = ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id) .ToHashSet(); + ContentPackageManager.EnabledPackages.DisableMods(toUninstall); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); ContentPackageManager.WorkshopPackages.Refresh(); ContentPackageManager.EnabledPackages.DisableRemovedMods(); } - public static async Task ForceRedownload(Steamworks.Ugc.Item item) + public static async Task ForceRedownload(Steamworks.Ugc.Item item, CancellationTokenSource? cancellationTokenSrc = null) { NukeDownload(item); - await item.DownloadAsync(); + cancellationTokenSrc ??= new CancellationTokenSource(); + await item.DownloadAsync(ct: cancellationTokenSrc.Token); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 30054648d..2b1722674 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -57,6 +57,20 @@ namespace Barotrauma return isCJK.IsMatch(text); } + /// + /// Check if the currently selected language is available, and switch to English if not + /// + public static void VerifyLanguageAvailable() + { + if (!TextPacks.ContainsKey(GameSettings.CurrentConfig.Language)) + { + DebugConsole.ThrowError($"Could not find the language \"{GameSettings.CurrentConfig.Language}\". Trying to switch to English..."); + var config = GameSettings.CurrentConfig; + config.Language = "English".ToLanguageIdentifier();; + GameSettings.SetCurrentConfig(config); + } + } + public static bool ContainsTag(string tag) { return ContainsTag(tag.ToIdentifier()); diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index b47978352..e2b509a75 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,65 @@ +--------------------------------------------------------------------------------------------------------- +v0.17.6.0 +--------------------------------------------------------------------------------------------------------- + +Changes: +- Buffed ethanol's and tobacco's effects. +- Renamed "details" to "manage" and "permissions" to "rank" in the client management context menu to make them a little more clear. +- Added an indicator for when players are downloading files from the server to the player list in the lobby. +- Adjustments, tweaks, and polish for the new abyss monster, now called "Latcher". Updated texture. +- Adjusted the kill hammerhead missions. +- Changes to character aiming behavior. +- Giant Spineling doesn't flee anymore when being shot with coilgun, chaingun, or small arms. + +Changes (unstable only): +- Added separate icons for mods that you've published and mods that you've subscribed to. +- Added a button to the prompt asking you to download mods from the server that will subscribe to missing Workshop items. +- Added a context menu to the items in the Installed Mods tab. +- Added a button to update all mods that are out of date. +- Added a search box to the locked mods list. +- Added a search box to the required mods list in the submarine editor. +- Double-clicking now enables/disables items in the Installed Mods tab. + +Fixes: +- Fixed swimming characters sometimes being unable to stand up on stairs/platforms even if the water is shallow enough. +- Fixed guitar and harmonica being rendered on top of the water effect. +- Fixed guitar, harmonica, accordion and captains pipe having neutral buoyancy. +- Fixed mid-round joining clients not seeing subs purchased during that round. +- Fixed research station being repairable by clicking on it instead of pressing E. +- Fixed medical curtains disappearing before they're off-screen. +- Fixed karma preset being forced to default when starting a new server. +- Fixed calyxanide not damaging the "naturally spawning" husks. +- Fixed Herja's rear motion detector being connected to an incorrect display, and the bottom turret display having an incorrect text. +- Fixed crash caused by selection not being cleared when autocompleting or running a console command. +- Waypointfixes on abandoned outpost modules, some regular outpost modules, and Winterhalter. +- Fixed bots occasionally getting stuck while climbing ladders connecting outpost modules. +- Fixes to waypoint generator, mainly on stairs. +- Fixed a null reference exception when a bot is dismissed while being told to follow the player and still in the combat state. +- Fixed Giant Spineling targeting doors after being attacked, which it shouldn't do by design. Might affect other creatures too. + +Fixes (unstable only): +- Fixes and improvements to colony modules. +- Fixed items in vending machines not being displayed as "out of stock" client-side, fixed money getting deducted when trying to buy an item that's out of stock. +- Fixed crashing on startup if the selected language cannot be found (e.g. if you've previously used a modded language and no longer have that mod installed). +- Fixed chat messages about money transfer votes not showing up. +- Fixed voting not finishing until the timer has elapsed even if there's already enough yes/no votes. +- Fixed vitality multipliers not working (e.g. damage to the head not having a bigger effect than damage to the limbs). +- Fixed "create level object" crashing the level editor. +- Fixed crashing when trying to save a sub with whitespace at the end of the name. +- Fixed sub editor's tag picker not working. +- Fixed event sprites not appearing. +- Fixed submarine switching not working. +- Fixed crew list and chatbox refusing to stay closed. +- Fixed character names being in upper case when using the health scanner. +- Fixed explosive coilgun ammo not being sold by armory merchants. +- Fixed physicorium not being sold at research outposts. +- Fixed issues with store interface displaying incorrect information. +- Fixed issues with buying items in multiplayer campaign. +- Fixed issues with generating daily specials and requested goods for campaign stores. +- Fixed double title in mod download prompt. +- Fixed mod transfer skipping item assemblies. +- Waypoint fixes on new colony modules. + --------------------------------------------------------------------------------------------------------- v0.17.5.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index db06eeae8..691bf0688 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -99,11 +99,22 @@ namespace Steamworks onDownloadStarted = (r, id) => downloadStarted = true; OnDownloadItemResult += onDownloadStarted; + int iters = 0; while ( downloadStarted == false ) { - if ( ct.IsCancellationRequested ) - break; + ct.ThrowIfCancellationRequested(); + iters++; + if (iters >= 1000 / milisecondsUpdateDelay) + { + if (!item.IsDownloading && !item.IsInstalled) + { + //force download to start if it's not started + if ( Download( fileId, highPriority: true ) == false ) + return item.IsInstalled; + } + iters = 0; + } await Task.Delay( milisecondsUpdateDelay ); } } @@ -120,8 +131,7 @@ namespace Steamworks { while ( true ) { - if ( ct.IsCancellationRequested ) - break; + ct.ThrowIfCancellationRequested(); progress?.Invoke( 0.2f + item.DownloadAmount * 0.8f ); From 164d72ae3a45f219e2153ce4f56171e88fdaa825 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Fri, 8 Apr 2022 00:34:17 +0900 Subject: [PATCH 9/9] Unstable 0.17.7.0 --- .../ClientSource/Characters/CharacterHUD.cs | 4 +- .../ClientSource/Characters/CharacterInfo.cs | 26 +++ .../ClientSource/GUI/GUIListBox.cs | 23 ++- .../ClientSource/GUI/GUIStyle.cs | 7 + .../ClientSource/GUI/Store.cs | 52 +++++- .../ClientSource/GUI/TabMenu.cs | 173 ++++++++++++------ .../BarotraumaClient/ClientSource/GameMain.cs | 2 + .../Items/Components/Repairable.cs | 2 + .../BarotraumaClient/ClientSource/Map/Hull.cs | 11 +- .../ClientSource/Map/ItemAssemblyPrefab.cs | 4 +- .../ClientSource/Map/Structure.cs | 8 +- .../ClientSource/Map/Submarine.cs | 3 +- .../ClientSource/Map/WayPoint.cs | 4 +- .../ClientSource/Networking/Client.cs | 4 +- .../Networking/FileTransfer/FileReceiver.cs | 25 ++- .../ClientSource/Networking/GameClient.cs | 18 +- .../ClientSource/Networking/ServerInfo.cs | 69 +++---- .../ClientSource/Particles/Particle.cs | 27 ++- .../ClientSource/Screens/EditorScreen.cs | 1 + .../Screens/EventEditor/EditorNode.cs | 4 +- .../Screens/EventEditor/EventEditorScreen.cs | 14 +- .../ClientSource/Screens/LevelEditorScreen.cs | 11 +- .../ClientSource/Screens/ModDownloadScreen.cs | 5 +- .../ClientSource/Screens/NetLobbyScreen.cs | 2 +- .../ClientSource/Screens/ServerListScreen.cs | 22 +-- .../ClientSource/Screens/SubEditorScreen.cs | 2 +- .../ClientSource/Settings/SettingsMenu.cs | 7 +- .../ClientSource/Sounds/SoundManager.cs | 11 +- .../ClientSource/Sounds/SoundPlayer.cs | 1 + .../ClientSource/Sounds/SoundPrefab.cs | 5 + .../ClientSource/Steam/Workshop.cs | 3 +- .../Immutable/ImmutableWorkshopMenu.cs | 6 + .../Mutable/MutableWorkshopMenu.cs | 48 ++++- .../ClientSource/Steam/WorkshopMenu/UiUtil.cs | 15 ++ .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../BarotraumaServer/ServerSource/GameMain.cs | 1 + .../GameModes/MultiPlayerCampaign.cs | 14 +- .../Networking/FileTransfer/FileSender.cs | 16 +- .../ServerSource/Networking/Voting.cs | 11 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Characters/AI/HumanAIController.cs | 5 +- .../Characters/AI/IndoorsSteeringManager.cs | 24 ++- .../AI/Objectives/AIObjectiveCombat.cs | 10 +- .../Characters/Animation/AnimController.cs | 2 +- .../SharedSource/Characters/Attack.cs | 2 +- .../SharedSource/Characters/Character.cs | 37 ++-- .../SharedSource/Characters/CharacterInfo.cs | 7 +- .../ContentFile/ContentFile.cs | 8 +- .../ContentFile/GenericPrefabFile.cs | 10 +- .../ContentPackage/ContentPackage.cs | 26 ++- .../ContentPackageManager.cs | 25 ++- .../SharedSource/DebugConsole.cs | 27 ++- .../SharedSource/Decals/DecalManager.cs | 10 +- .../Events/Missions/BeaconMission.cs | 2 +- .../SharedSource/GameSession/Data/Wallet.cs | 7 + .../SharedSource/GameSession/GameSession.cs | 23 ++- .../Items/Components/Holdable/RepairTool.cs | 9 +- .../SharedSource/Items/Item.cs | 28 ++- .../SharedSource/Map/Explosion.cs | 9 +- .../BarotraumaShared/SharedSource/Map/Hull.cs | 57 ++---- .../Map/Levels/Ruins/RuinGenerationParams.cs | 8 +- .../SharedSource/Map/MapEntityPrefab.cs | 26 +-- .../Map/Outposts/OutpostGenerator.cs | 4 +- .../SharedSource/Map/StructurePrefab.cs | 1 + .../SharedSource/Networking/Client.cs | 2 +- .../SharedSource/Networking/ServerLog.cs | 5 +- .../Serialization/SerializableProperty.cs | 13 ++ .../SharedSource/Settings/GameSettings.cs | 2 + .../StatusEffects/PropertyConditional.cs | 21 ++- .../SharedSource/Steam/Workshop.cs | 11 +- .../SharedSource/Text/RichString.cs | 2 +- .../SharedSource/Text/TextPack.cs | 40 ++-- .../SharedSource/Utils/Result.cs | 10 +- .../SharedSource/Utils/SafeIO.cs | 24 ++- .../SharedSource/Utils/SaveUtil.cs | 2 +- .../Submarines/PowerTestSub.sub | Bin 8754 -> 0 bytes Barotrauma/BarotraumaShared/changelog.txt | 57 ++++++ Libraries/Facepunch.Steamworks/SteamUgc.cs | 8 + 82 files changed, 852 insertions(+), 385 deletions(-) delete mode 100644 Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index a911b5c70..b6304d486 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -484,7 +484,9 @@ namespace Barotrauma (int)(HUDLayoutSettings.BottomRightInfoArea.Y + HUDLayoutSettings.BottomRightInfoArea.Height * 0.1f), (int)(HUDLayoutSettings.BottomRightInfoArea.Width / 2), (int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f)), character.Info.IsDisguisedAsAnother); - character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, 4 * GUI.Scale), targetWidth: HUDLayoutSettings.PortraitArea.Width, true, character.Info.IsDisguisedAsAnother); + float yOffset = (GameMain.GameSession?.Campaign is MultiPlayerCampaign ? -10 : 4) * GUI.Scale; + character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, yOffset), targetWidth: HUDLayoutSettings.PortraitArea.Width, true, character.Info.IsDisguisedAsAnother); + character.Info.DrawForeground(spriteBatch); } mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); if (mouseOnPortrait) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 8ee874c7d..95e85187d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -309,6 +309,32 @@ namespace Barotrauma HUDLayoutSettings.BottomRightInfoArea.Height / (float)infoAreaPortraitBG.SourceRect.Height)); } + public void DrawForeground(SpriteBatch spriteBatch) + { + if (Character is null || GameMain.IsSingleplayer) { return; } + const int million = 1000000; + int xfraction = (int)(HUDLayoutSettings.BottomRightInfoArea.Width * 0.2f); + int yoffset = GUI.IntScale(6); + + int walletAmount = Character.Wallet.Balance; + + LocalizedString str = walletAmount >= million ? TextManager.Get("crewwallet.balance.toomuchtoshow") : TextManager.FormatCurrency(walletAmount); + Vector2 size = GUIStyle.Font.MeasureString(str); + int barHeight = GUI.IntScale(18); + + Rectangle barRect = new Rectangle((int)(HUDLayoutSettings.BottomRightInfoArea.X + xfraction / 2.5f), HUDLayoutSettings.BottomRightInfoArea.Bottom - barHeight - yoffset, HUDLayoutSettings.BottomRightInfoArea.Width - xfraction, barHeight); + float textScale = Math.Max(0.1f, Math.Min(barRect.Width / size.X, barRect.Height / size.Y)) - 0.01f; + + GUIStyle.WalletPortraitBG.Draw(spriteBatch, barRect, Color.White); + + int iconSize = GUI.IntScale(28); + int iconXOffset = iconSize / 2; + Rectangle iconRect = new Rectangle(barRect.Right - iconXOffset, barRect.Top - iconSize / 4, iconSize, iconSize); + GUIStyle.CrewWalletIconSmall.Draw(spriteBatch, iconRect, Color.White); + var (scaledTextSizeX, scaledTextSizeY) = size * textScale; + GUIStyle.Font.DrawString(spriteBatch, str, new Vector2(barRect.Right - iconXOffset - scaledTextSizeX - GUI.IntScale(4), barRect.Center.Y - scaledTextSizeY / 2), GUIStyle.TextColorNormal, 0f, Vector2.Zero, textScale, SpriteEffects.None, 0f); + } + public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index efaaebbc6..0089c3e94 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -512,23 +512,36 @@ namespace Barotrauma } /// - /// Scrolls the list to the specific element, currently only works when smooth scrolling and PadBottom are enabled. + /// Scrolls the list to the specific element. /// /// - public void ScrollToElement(GUIComponent component) + public void ScrollToElement(GUIComponent component, bool playSound = true) { - SoundPlayer.PlayUISound(GUISoundType.Click); + if (playSound) { SoundPlayer.PlayUISound(GUISoundType.Click); } List children = Content.Children.ToList(); int index = children.IndexOf(component); if (index < 0) { return; } + void performScroll(GUIComponent c) + { + if (SmoothScroll && PadBottom) + { + scrollToElement = c; + } + else + { + float diff = isHorizontal ? c.Rect.X - Content.Rect.X : c.Rect.Y - Content.Rect.Y; + ScrollBar.BarScroll += diff / TotalSize; + } + } + if (!Content.Children.Contains(component) || !component.Visible) { - scrollToElement = null; + performScroll(null); } else { - scrollToElement = component; + performScroll(component); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 0c8828ae5..850f347bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -64,6 +64,8 @@ namespace Barotrauma public readonly static GUISprite UIGlowSolidCircular = new GUISprite("UIGlowSolidCircular"); public readonly static GUISprite UIThermalGlow = new GUISprite("UIGlowSolidCircular"); public readonly static GUISprite ButtonPulse = new GUISprite("ButtonPulse"); + public readonly static GUISprite WalletPortraitBG = new GUISprite("WalletPortraitBG"); + public readonly static GUISprite CrewWalletIconSmall = new GUISprite("CrewWalletIconSmall"); public readonly static GUISprite EndRoundButtonPulse = new GUISprite("EndRoundButtonPulse"); @@ -96,6 +98,11 @@ namespace Barotrauma /// public readonly static GUIColor Yellow = new GUIColor("Yellow"); + /// + /// Color to display the name of modded servers in the server list. + /// + public readonly static GUIColor ModdedServerColor = new GUIColor("ModdedServerColor"); + public readonly static GUIColor ColorInventoryEmpty = new GUIColor("ColorInventoryEmpty"); public readonly static GUIColor ColorInventoryHalf = new GUIColor("ColorInventoryHalf"); public readonly static GUIColor ColorInventoryFull = new GUIColor("ColorInventoryFull"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 8a495614b..bdad817ee 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -50,6 +50,7 @@ namespace Barotrauma private GUIImage sellValueChangeArrow; private GUIDropDown sortingDropDown; private GUITextBox searchBox; + private GUILayoutGroup categoryButtonContainer; private GUIListBox storeBuyList, storeSellList, storeSellFromSubList; /// /// Can be null when there are no deals at the current location @@ -482,7 +483,7 @@ namespace Barotrauma }; // Item category buttons ------------------------------------------------ - var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.08f, 1.0f), storeInventoryContainer.RectTransform)) + categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.08f, 1.0f), storeInventoryContainer.RectTransform)) { RelativeSpacing = 0.02f }; @@ -765,6 +766,34 @@ namespace Barotrauma } } + private void UpdateCategoryButtons() + { + var tabItems = activeTab switch + { + StoreTab.Buy => ActiveStore?.Stock, + StoreTab.Sell => itemsToSell, + StoreTab.SellSub => itemsToSellFromSub, + _ => null + } ?? Enumerable.Empty(); + foreach (var button in itemCategoryButtons) + { + if (!(button.UserData is MapEntityCategory category)) + { + continue; + } + bool isButtonEnabled = false; + foreach (var item in tabItems) + { + if (item.ItemPrefab.Category.HasFlag(category)) + { + isButtonEnabled = true; + break; + } + } + button.Enabled = isButtonEnabled; + } + } + private void ChangeStoreTab(StoreTab tab) { activeTab = tab; @@ -774,6 +803,7 @@ namespace Barotrauma } sortingDropDown.SelectItem(tabSortingMethods[tab]); relevantBalanceName.Text = IsBuying ? TextManager.Get("campaignstore.balance") : TextManager.Get("campaignstore.storebalance"); + UpdateCategoryButtons(); SetShoppingCrateTotalText(); SetClearAllButtonStatus(); SetConfirmButtonBehavior(); @@ -879,7 +909,7 @@ namespace Barotrauma prevDailySpecialCount = dailySpecialCount; } - bool hasPermissions = HasTabPermissions(StoreTab.Sell); + bool hasPermissions = HasTabPermissions(StoreTab.Buy); var existingItemFrames = new HashSet(); foreach (PurchasedItem item in ActiveStore.Stock) { @@ -931,7 +961,11 @@ namespace Barotrauma removedItemFrames.AddRange(storeDailySpecialsGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (activeTab == StoreTab.Buy) { FilterStoreItems(); } + if (activeTab == StoreTab.Buy) + { + UpdateCategoryButtons(); + FilterStoreItems(); + } SortItems(StoreTab.Buy); storeBuyList.BarScroll = prevBuyListScroll; @@ -1011,7 +1045,11 @@ namespace Barotrauma } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (activeTab == StoreTab.Sell) { FilterStoreItems(); } + if (activeTab == StoreTab.Sell) + { + UpdateCategoryButtons(); + FilterStoreItems(); + } SortItems(StoreTab.Sell); storeSellList.BarScroll = prevSellListScroll; @@ -1091,7 +1129,11 @@ namespace Barotrauma } removedItemFrames.ForEach(f => f.RectTransform.Parent = null); - if (activeTab == StoreTab.SellSub) { FilterStoreItems(); } + if (activeTab == StoreTab.SellSub) + { + UpdateCategoryButtons(); + FilterStoreItems(); + } SortItems(StoreTab.SellSub); storeSellFromSubList.BarScroll = prevSellListScroll; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index da6b8d6d1..d8125d1fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -50,19 +50,23 @@ namespace Barotrauma private const ushort lowPingThreshold = 100; private const ushort mediumPingThreshold = 200; + public readonly Client Client; + private ushort currentPing; - private readonly Client client; private readonly Character character; private readonly bool hasCharacter; private readonly GUITextBlock textBlock; private readonly GUIFrame frame; - public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + private readonly GUIImage permissionIcon; + + public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock, GUIImage permissionIcon) { - this.client = client; + this.Client = client; this.textBlock = textBlock; this.frame = frame; this.hasCharacter = hasCharacter; + this.permissionIcon = permissionIcon; } public LinkedGUI(Character character, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) @@ -75,33 +79,39 @@ namespace Barotrauma public bool HasMultiplayerCharacterChanged() { - if (client == null) return false; - bool characterState = client.Character != null; - if (characterState && client.Character.IsDead) characterState = false; + if (Client == null) { return false; } + bool characterState = Client.Character != null; + if (characterState && Client.Character.IsDead) characterState = false; return hasCharacter != characterState; } public bool HasMultiplayerCharacterDied() { - if (client == null || !hasCharacter || client.Character == null) return false; - return client.Character.IsDead; + if (Client == null || !hasCharacter || Client.Character == null) { return false; } + return Client.Character.IsDead; } public bool HasAICharacterDied() { - if (character == null) return false; + if (character == null) { return false; } return character.IsDead; } public void TryPingRefresh() { - if (client == null) return; - if (currentPing == client.Ping) return; - currentPing = client.Ping; + if (Client == null) { return; } + if (currentPing == Client.Ping) { return; } + currentPing = Client.Ping; textBlock.Text = currentPing.ToString(); textBlock.TextColor = GetPingColor(); } + public void TryPermissionIconRefresh(Sprite icon) + { + if (Client == null || permissionIcon == null) { return; } + permissionIcon.Sprite = icon; + } + private Color GetPingColor() { if (currentPing < lowPingThreshold) @@ -196,6 +206,7 @@ namespace Barotrauma for (int i = 0; i < linkedGUIList.Count; i++) { linkedGUIList[i].TryPingRefresh(); + linkedGUIList[i].TryPermissionIconRefresh(GetPermissionIcon(linkedGUIList[i].Client)); if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasMultiplayerCharacterDied() || linkedGUIList[i].HasAICharacterDied()) { RemoveCurrentElements(); @@ -549,31 +560,42 @@ 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, null)); + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, textBlock: null)); } private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame) { + bool isCampaign = GameMain.GameSession?.Campaign is MultiPlayerCampaign; GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale"); - GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale"); + if (isCampaign) + { + GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform) + { + RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f) + }, TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale") + { + TextBlock = { Font = GUIStyle.HotkeyFont }, + CanBeFocused = false, + ForceUpperCase = ForceUpperCase.Yes + }; + walletColumnWidth = walletButton.Rect.Width; + } sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); - characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); + characterButton.RectTransform.RelativeSize = new Vector2((characterColumnWidthPercentage + (isCampaign ? 0 : walletColumnWidthPercentage)) * sizeMultiplier, 1f); pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); - walletButton.RectTransform.RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f); - jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = walletButton.TextBlock.Font = GUIStyle.HotkeyFont; - jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = walletButton.CanBeFocused = false; - jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = walletButton.ForceUpperCase = ForceUpperCase.Yes; + jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes; jobColumnWidth = jobButton.Rect.Width; characterColumnWidth = characterButton.Rect.Width; pingColumnWidth = pingButton.Rect.Width; - walletColumnWidth = walletButton.Rect.Width; } private void CreateMultiPlayerList(bool refresh) @@ -634,8 +656,10 @@ namespace Barotrauma if (client != null) { - CreateNameWithPermissionIcon(client, paddedFrame); - linkedGUIList.Add(new LinkedGUI(client, frame, true, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); + CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); + linkedGUIList.Add(new LinkedGUI(client, frame, true, + new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), + permissionIcon)); } else { @@ -644,11 +668,12 @@ namespace Barotrauma if (character is AICharacter) { - linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes })); + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, + 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, null)); + linkedGUIList.Add(new LinkedGUI(client: null, frame, true, 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)) { @@ -686,12 +711,15 @@ namespace Barotrauma SelectedColor = Color.White }; - CreateNameWithPermissionIcon(client, paddedFrame); + CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon); + linkedGUIList.Add(new LinkedGUI(client, frame, false, + new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center), + permissionIcon)); + if (client.Character is { } character) { CreateWalletCrewFrame(character, paddedFrame); } - linkedGUIList.Add(new LinkedGUI(client, frame, false, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); } private int GetTeamIndex(Client client) @@ -729,21 +757,47 @@ namespace Barotrauma private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame) { + if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign)) { return; } + GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { CanBeFocused = false }; - if (character.IsBot) { return; } - GUILayoutGroup paddedLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1f), walletLayout.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true }; - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "StoreTradingIcon", scaleToFit: true); - GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.SubHeadingFont); - SetWalletText(walletBlock, character.Wallet); + 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); + GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.Font) + { + AutoScaleHorizontal = true, + Padding = Vector4.Zero + }; + + GUIImage largeIcon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), walletSprite, scaleToFit: true) + { + IgnoreLayoutGroups = true, + Visible = false + }; + + if (character.IsBot) + { + largeIcon.Visible = true; + icon.Visible = false; + walletBlock.Visible = false; + largeIcon.Enabled = false; + return; + } + + walletLayout.Recalculate(); + paddedLayoutGroup.Recalculate(); + SetWalletText(walletBlock, character.Wallet, icon, largeIcon); if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign) { @@ -751,47 +805,56 @@ namespace Barotrauma campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e => { if (!(e.Owner is Some { Value: var owner }) || owner != character) { return; } - SetWalletText(walletBlock, e.Wallet); + SetWalletText(walletBlock, e.Wallet, icon, largeIcon); }); registeredEvents.Add(eventIdentifier); } - static void SetWalletText(GUITextBlock block, Wallet wallet) + static void SetWalletText(GUITextBlock block, Wallet wallet, GUIImage icon, GUIImage largeIcon) { + const int million = 1000000, + tooSmallPixelTreshold = 50; // 50 pixels is just not enough to see any meaningful info + block.Text = TextManager.FormatCurrency(wallet.Balance); block.ToolTip = string.Empty; - if (block.TextSize.X + block.Padding.X + block.Padding.Z > block.Rect.Width) + + if (wallet.Balance >= million) { - block.ToolTip = block.Text; block.Text = TextManager.Get("crewwallet.balance.toomuchtoshow"); + block.ToolTip = block.Text; + } + + largeIcon.Visible = false; + icon.Visible = true; + block.Visible = true; + + if (tooSmallPixelTreshold > block.Rect.Width) + { + largeIcon.Visible = true; + icon.Visible = false; + block.Visible = false; + largeIcon.ToolTip = block.Text; } } } - private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame) + private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame, out GUIImage permissionIcon) { GUITextBlock characterNameBlock; - Sprite permissionIcon = GetPermissionIcon(client); + Sprite permissionIconSprite = GetPermissionIcon(client); JobPrefab prefab = client.Character?.Info?.Job?.Prefab; Color nameColor = prefab != null ? prefab.UIColor : Color.White; - if (permissionIcon != null) - { - Point iconSize = permissionIcon.SourceRect.Size; - float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; + Point iconSize = new Point((int)(paddedFrame.Rect.Height * 0.8f)); + float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; - characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUIStyle.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); + characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(client.Name, GUIStyle.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); - float iconWidth = iconSize.X / (float)characterColumnWidth; - int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); - new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; - } - else - { - characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), - ToolBox.LimitString(client.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); - } + float iconWidth = iconSize.X / (float)characterColumnWidth; + int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); + permissionIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIconSprite) { IgnoreLayoutGroups = true }; + if (client.Character != null && client.Character.IsDead) { @@ -801,7 +864,7 @@ namespace Barotrauma private Sprite GetPermissionIcon(Client client) { - if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) return null; + if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) { return null; } if (client.IsOwner) // Owner cannot be kicked { @@ -898,7 +961,7 @@ namespace Barotrauma GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(walletFrame.RectTransform, 0.9f), walletFrame.RectTransform, anchor: Anchor.Center)); GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), walletLayout.RectTransform), isHorizontal: true); - GUIImage icon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "StoreTradingIcon", scaleToFit: true); + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewWalletIconLarge", scaleToFit: true); 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); @@ -950,7 +1013,7 @@ namespace Barotrauma GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red }; GUILayoutGroup centerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), mainLayout.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { IgnoreLayoutGroups = true }; new GUIFrame(new RectTransform(new Vector2(0f, 1f), centerLayout.RectTransform, Anchor.Center), style: "VerticalLine") { IgnoreLayoutGroups = true }; - GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(0.6f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow"); + 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index de83074a1..3c401566c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -477,6 +477,8 @@ namespace Barotrauma DebugConsole.Init(); + ContentPackageManager.LogEnabledRegularPackageErrors(); + #if !DEBUG && !OSX GameAnalyticsManager.InitIfConsented(); #endif diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 87a2c21e7..5d3e959c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -59,6 +59,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { + if (item.HiddenInGame) { return false; } if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) { return false; } if (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition) { return true; } @@ -222,6 +223,7 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { + if (item.HiddenInGame) { return; } if (FakeBrokenTimer > 0.0f) { item.FakeBroken = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 665a42380..01bab48a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -358,9 +358,9 @@ namespace Barotrauma WorldRect.Width, WorldRect.Height); GUI.DrawLine(spriteBatch, - new Vector2(currentHullRect.X, -currentHullRect.Y), - new Vector2(connectedHullRect.X, -connectedHullRect.Y), - GUIStyle.Green, width: 2); + new Vector2(currentHullRect.X, -currentHullRect.Y), + new Vector2(connectedHullRect.X, -connectedHullRect.Y), + GUIStyle.Green, width: 2); } } } @@ -378,7 +378,7 @@ namespace Barotrauma if (section.ColorStrength < 0.01f || section.Color.A < 1) { continue; } - if (DecalManager.GrimeSprites.None()) + if (section.GrimeSprite == null) { GUI.DrawRectangle(spriteBatch, new Vector2(drawOffset.X + rect.X + section.Rect.X, -(drawOffset.Y + rect.Y + section.Rect.Y)), @@ -389,8 +389,7 @@ namespace Barotrauma { Vector2 sectionPos = new Vector2(drawPos.X + section.Rect.Location.X, -(drawPos.Y + section.Rect.Location.Y)); Vector2 randomOffset = new Vector2(section.Noise.X - 0.5f, section.Noise.Y - 0.5f) * 15.0f; - var sprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{i % DecalManager.GrimeSpriteCount}"].Sprite; - sprite.Draw(spriteBatch, sectionPos + randomOffset, section.GetStrengthAdjustedColor(), scale: 1.25f); + section.GrimeSprite.Draw(spriteBatch, sectionPos + randomOffset, section.GetStrengthAdjustedColor(), scale: 1.25f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 0db354aea..01fb485ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -81,8 +81,8 @@ namespace Barotrauma } Vector2 center = new Vector2((minX + maxX) / 2.0f, (minY + maxY) / 2.0f); if (Submarine.MainSub != null) { center -= Submarine.MainSub.HiddenSubPosition; } - center.X -= center.X % Submarine.GridSize.X; - center.Y -= center.Y % Submarine.GridSize.Y; + center.X -= MathUtils.RoundTowardsClosest(center.X, Submarine.GridSize.X); + center.Y -= MathUtils.RoundTowardsClosest(center.Y, Submarine.GridSize.Y); MapEntity.SelectedList.Clear(); assemblyEntities.ForEach(e => MapEntity.AddSelection(e)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index a36e4216b..596935d5b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -143,7 +143,7 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.01f }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => @@ -156,7 +156,7 @@ namespace Barotrauma return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => @@ -169,7 +169,7 @@ namespace Barotrauma return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall") { OnClicked = (button, data) => { @@ -178,7 +178,7 @@ namespace Barotrauma return true; } }; - new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ResetToPrefab")) + new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ResetToPrefab"), style: "GUIButtonSmall") { OnClicked = (button, data) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 8f48882e1..d6d47fdf7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -186,9 +186,8 @@ namespace Barotrauma { if (predicate != null) { - if (!predicate(e)) continue; + if (!predicate(e)) { continue; } } - hull.DrawSectionColors(spriteBatch); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index e69d0bbb0..1d862e8a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -299,7 +299,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), spawnTypeContainer.RectTransform), TextManager.Get("SpawnType")); - var button = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), spawnTypeContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") + var button = new GUIButton(new RectTransform(Vector2.One, spawnTypeContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton") { UserData = -1, OnClicked = ChangeSpawnType @@ -308,7 +308,7 @@ namespace Barotrauma { UserData = "spawntypetext" }; - button = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), spawnTypeContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton") + button = new GUIButton(new RectTransform(Vector2.One, spawnTypeContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton") { UserData = 1, OnClicked = ChangeSpawnType diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index c59c2d2a6..4ebdb2c37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -108,7 +108,7 @@ namespace Barotrauma.Networking { return; } - if (!Permissions.HasFlag(permission)) Permissions |= permission; + if (!Permissions.HasFlag(permission)) { Permissions |= permission; } } public void RemovePermission(ClientPermissions permission) @@ -117,7 +117,7 @@ namespace Barotrauma.Networking { return; } - if (Permissions.HasFlag(permission)) Permissions &= ~permission; + if (Permissions.HasFlag(permission)) { Permissions &= ~permission; } } public bool HasPermission(ClientPermissions permission) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 8f046b69d..75401668a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -51,6 +51,17 @@ namespace Barotrauma.Networking set; } + public DateTime LastOffsetAckTime + { + get; + private set; + } + + public void RecordOffsetAckTime() + { + LastOffsetAckTime = DateTime.Now; + } + public float BytesPerSecond { get; @@ -91,6 +102,8 @@ namespace Barotrauma.Networking Connection = connection; Status = FileTransferStatus.NotStarted; + + LastOffsetAckTime = DateTime.Now - new TimeSpan(days: 0, hours: 0, minutes: 5, seconds: 0); } public void OpenStream() @@ -214,11 +227,11 @@ namespace Barotrauma.Networking fileName != existingTransfer.FileName) { GameMain.Client.CancelFileTransfer(transferId); - DebugConsole.ThrowError("File transfer error: file transfer initiated with an ID that's already in use"); + DebugConsole.AddWarning("File transfer error: file transfer initiated with an ID that's already in use"); } else //resend acknowledgement packet { - GameMain.Client.UpdateFileTransfer(transferId, existingTransfer.Received, existingTransfer.LastSeen); + GameMain.Client.UpdateFileTransfer(existingTransfer, existingTransfer.Received, existingTransfer.LastSeen); } return; } @@ -285,7 +298,7 @@ namespace Barotrauma.Networking } activeTransfers.Add(newTransfer); - GameMain.Client.UpdateFileTransfer(transferId, 0, 0); //send acknowledgement packet + GameMain.Client.UpdateFileTransfer(newTransfer, 0, 0); //send acknowledgement packet } break; case (byte)FileTransferMessageType.TransferOnSameMachine: @@ -333,7 +346,7 @@ namespace Barotrauma.Networking if (!finishedTransfers.Any(t => t.transferId == transferId)) { GameMain.Client.CancelFileTransfer(transferId); - DebugConsole.ThrowError("File transfer error: received data without a transfer initiation message"); + DebugConsole.AddWarning("File transfer error: received data without a transfer initiation message"); } return; } @@ -344,7 +357,7 @@ namespace Barotrauma.Networking { activeTransfer.LastSeen = Math.Max(offset, activeTransfer.LastSeen); DebugConsole.Log($"Received {bytesToRead} bytes of the file {activeTransfer.FileName} (ignoring: offset {offset}, waiting for {activeTransfer.Received})"); - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, activeTransfer.LastSeen); + GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen); return; } activeTransfer.LastSeen = offset; @@ -375,7 +388,7 @@ namespace Barotrauma.Networking return; } - GameMain.Client.UpdateFileTransfer(activeTransfer.ID, activeTransfer.Received, activeTransfer.LastSeen, reliable: activeTransfer.Status == FileTransferStatus.Finished); + GameMain.Client.UpdateFileTransfer(activeTransfer, activeTransfer.Received, activeTransfer.LastSeen, reliable: activeTransfer.Status == FileTransferStatus.Finished); if (activeTransfer.Status == FileTransferStatus.Finished) { activeTransfer.Dispose(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 57ff660c5..08c87776e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -1947,7 +1947,6 @@ namespace Barotrauma.Networking existingClient.Character = null; existingClient.Karma = tc.Karma; existingClient.Muted = tc.Muted; - existingClient.HasPermissions = tc.HasPermissions; existingClient.InGame = tc.InGame; existingClient.IsOwner = tc.IsOwner; existingClient.AllowKicking = tc.AllowKicking; @@ -2493,12 +2492,18 @@ namespace Barotrauma.Networking CancelFileTransfer(transfer.ID); } - public void UpdateFileTransfer(int id, int expecting, int lastSeen, bool reliable = false) + public void UpdateFileTransfer(FileReceiver.FileTransferIn transfer, int expecting, int lastSeen, bool reliable = false) { + if (!reliable && (DateTime.Now - transfer.LastOffsetAckTime).TotalSeconds < 1) + { + return; + } + transfer.RecordOffsetAckTime(); + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.FILE_REQUEST); msg.Write((byte)FileTransferMessageType.Data); - msg.Write((byte)id); + msg.Write((byte)transfer.ID); msg.Write(expecting); msg.Write(lastSeen); clientPeer.Send(msg, reliable ? DeliveryMethod.Reliable : DeliveryMethod.Unreliable); @@ -2784,7 +2789,9 @@ namespace Barotrauma.Networking public void ShowSubmarineChangeVoteInterface(Client starter, SubmarineInfo info, VoteType type, float timeOut) { - if (info == null || votingInterface != null) { return; } + if (info == null) { return; } + if (votingInterface != null && votingInterface.VoteRunning) { return; } + votingInterface?.Remove(); votingInterface = VotingInterface.CreateSubmarineVotingInterface(starter, info, type, timeOut); } #endregion @@ -2792,12 +2799,13 @@ namespace Barotrauma.Networking #region Money Transfer Voting public void ShowMoneyTransferVoteInterface(Client starter, Client from, int amount, Client to, float timeOut) { - if (votingInterface != null) { return; } + if (votingInterface != null && votingInterface.VoteRunning) { return; } if (from == null && to == null) { DebugConsole.ThrowError("Tried to initiate a vote for transferring from null to null!"); return; } + votingInterface?.Remove(); votingInterface = VotingInterface.CreateMoneyTransferVotingInterface(starter, from, to, amount, timeOut); } #endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 44e51dfdd..797343808 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -78,24 +78,6 @@ namespace Barotrauma.Networking get; private set; } = new List(); - - public bool ContentPackagesMatch() - { - //make sure we have all the packages the server requires - if (ContentPackageHashes.Count != ContentPackageWorkshopIds.Count) { return false; } - for (int i = 0; i < ContentPackageWorkshopIds.Count; i++) - { - string hash = ContentPackageHashes[i]; - UInt64 id = ContentPackageWorkshopIds[i]; - if (!GameMain.ServerListScreen.ContentPackagesByHash.ContainsKey(hash)) - { - if (GameMain.ServerListScreen.ContentPackagesByWorkshopId.ContainsKey(id)) { return false; } - if (id == 0) { return false; } - } - } - - return true; - } public void CreatePreviewWindow(GUIFrame frame) { @@ -105,7 +87,8 @@ namespace Barotrauma.Networking var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont) { - ToolTip = ServerName + ToolTip = ServerName, + CanBeFocused = false }; title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f)); @@ -130,7 +113,11 @@ namespace Barotrauma.Networking }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)); + TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), + string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)) + { + CanBeFocused = false + }; bool hidePlaystyleBanner = !PlayStyle.HasValue; if (!hidePlaystyleBanner) @@ -141,15 +128,23 @@ namespace Barotrauma.Networking var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform), playStyleBannerSprite, null, true); - var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.06f) }, - TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, - font: GUIStyle.SmallFont, textAlignment: Alignment.Center, + var playStyleName = new GUITextBlock( + new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) + { RelativeOffset = new Vector2(0.0f, 0.06f) }, + TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), + TextManager.Get("servertag." + playStyle)), textColor: Color.White, + font: GUIStyle.SmallFont, textAlignment: Alignment.Center, color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); playStyleName.RectTransform.IsFixedSize = true; } + var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), - TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), textAlignment: Alignment.TopLeft); + TextManager.Get((OwnerID != 0 || LobbyID != 0) ? "SteamP2PServer" : "DedicatedServer"), + textAlignment: Alignment.TopLeft) + { + CanBeFocused = false + }; serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f)); var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform)) @@ -270,7 +265,11 @@ namespace Barotrauma.Networking new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); - var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) { ScrollBarVisible = true }; + var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform)) + { + ScrollBarVisible = true, + OnSelected = (component, o) => false + }; if (ContentPackageNames.Count == 0) { new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) @@ -282,28 +281,30 @@ namespace Barotrauma.Networking { for (int i = 0; i < ContentPackageNames.Count; i++) { - var packageText = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) { MinSize = new Point(0, 15) }, + var packageText = new GUITickBox( + new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform) + { MinSize = new Point(0, 15) }, ContentPackageNames[i]) { - CanBeFocused = false + Enabled = false }; + packageText.Box.Enabled = true; + packageText.TextBlock.Enabled = true; if (i < ContentPackageHashes.Count) { if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i])) { + packageText.TextColor = GUIStyle.Green; packageText.Selected = true; - continue; } - //workshop download link found - if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) + else if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0) { - packageText.TextColor = GUIStyle.Yellow; packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]); } - else //no package or workshop download link found, tough luck + else //no package or workshop download link found (TODO: update text to say that they could be downloaded through the server) { - packageText.TextColor = GUIStyle.Red; + packageText.TextColor = GameMain.VanillaContent.NameMatches(ContentPackageNames[i]) ? GUIStyle.Red : GUIStyle.Yellow; packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage", ("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i])); } @@ -342,7 +343,7 @@ namespace Barotrauma.Networking } if (ContentPackageNames.Count > 0) { - tags.Add(ContentPackageNames.Count > 1 || ContentPackageNames[0] != GameMain.VanillaContent?.Name ? "modded.true" : "modded.false"); + tags.Add(ContentPackageNames.Count > 1 || !GameMain.VanillaContent.NameMatches(ContentPackageNames[0]) ? "modded.true" : "modded.false"); } return tags; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index dbe56f22d..fa77bcd3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -336,7 +336,7 @@ namespace Barotrauma.Particles Hull collidedHull = Hull.FindHull(position); if (collidedHull != null) { - if (prefab.DeleteOnCollision) return UpdateResult.Delete; + if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } OnWallCollisionOutside(collidedHull); } } @@ -346,20 +346,10 @@ namespace Barotrauma.Particles Vector2 collisionNormal = Vector2.Zero; if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height) { - if (prefab.DeleteOnCollision) - { - OnCollision?.Invoke(position, currentHull); - return UpdateResult.Delete; - } collisionNormal = new Vector2(0.0f, 1.0f); } else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y) { - if (prefab.DeleteOnCollision) - { - OnCollision?.Invoke(position, currentHull); - return UpdateResult.Delete; - } collisionNormal = new Vector2(0.0f, -1.0f); } @@ -379,18 +369,21 @@ namespace Barotrauma.Particles break; } + if (prefab.DeleteOnCollision && !gapFound) + { + OnCollision?.Invoke(position, currentHull); + return UpdateResult.Delete; + } handleCollision(gapFound, collisionNormal); } collisionNormal = Vector2.Zero; if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X) { - if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } collisionNormal = new Vector2(1.0f, 0.0f); } else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right) { - if (prefab.DeleteOnCollision) { return UpdateResult.Delete; } collisionNormal = new Vector2(-1.0f, 0.0f); } @@ -408,7 +401,11 @@ namespace Barotrauma.Particles gapFound = true; break; } - + if (prefab.DeleteOnCollision && !gapFound) + { + OnCollision?.Invoke(position, currentHull); + return UpdateResult.Delete; + } handleCollision(gapFound, collisionNormal); } @@ -512,7 +509,7 @@ namespace Barotrauma.Particles { Rectangle hullRect = collisionHull.WorldRect; - Vector2 center = new Vector2(hullRect.X + hullRect.Width /2, hullRect.Y - hullRect.Height / 2); + Vector2 center = new Vector2(hullRect.X + hullRect.Width / 2, hullRect.Y - hullRect.Height / 2); if (position.Y < center.Y) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 7108479ce..9e690bb7b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -17,6 +17,7 @@ namespace Barotrauma Hull.EditFire = false; Hull.EditWater = false; #endif + HumanAIController.DisableCrewAI = false; } protected virtual void DeselectEditorSpecific() { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs index 3b8f0f22b..5383d7614 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EditorNode.cs @@ -131,7 +131,7 @@ namespace Barotrauma } else { - connection.OverrideValue = Convert.ChangeType(overrideValue, type); + connection.OverrideValue = EventEditorScreen.ChangeType(overrideValue, type); } } } @@ -513,7 +513,7 @@ namespace Barotrauma } else { - newNode.Value = Convert.ChangeType(value, type); + newNode.Value = EventEditorScreen.ChangeType(value, type); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 395391803..2e450a813 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -440,7 +440,7 @@ namespace Barotrauma } else { - connection.OverrideValue = Convert.ChangeType(attribute.Value, connection.ValueType); + connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType); } } } @@ -524,6 +524,18 @@ namespace Barotrauma GuiFrame.AddToGUIUpdateList(); } + public static object? ChangeType(string value, Type type) + { + if (type == typeof(Identifier)) + { + return value.ToIdentifier(); + } + else + { + return Convert.ChangeType(value, type); + } + } + private XElement? ExportXML() { XElement mainElement = new XElement("ScriptedEvent", new XAttribute("identifier", projectName.RemoveWhitespace().ToLowerInvariant())); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index b9aed65dd..146f41f13 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -883,12 +883,17 @@ namespace Barotrauma private void SerializeAll() { + IEnumerable packages = ContentPackageManager.LocalPackages; +#if DEBUG + packages = packages.Union(ContentPackageManager.VanillaCorePackage.ToEnumerable()); +#endif + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true }; - foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) + foreach (var configFile in packages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -922,7 +927,7 @@ namespace Barotrauma } } - foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) + foreach (var configFile in packages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -957,7 +962,7 @@ namespace Barotrauma } settings.NewLineOnAttributes = false; - foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) + foreach (var configFile in packages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs index c07116c26..6e278ac0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ModDownloadScreen.cs @@ -8,7 +8,6 @@ using Barotrauma.Networking; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Steamworks.Data; using Color = Microsoft.Xna.Framework.Color; using ServerContentPackage = Barotrauma.Networking.ClientPeer.ServerContentPackage; @@ -164,7 +163,7 @@ namespace Barotrauma => wp.SteamWorkshopId != mp.WorkshopId)) .Select(mp => mp.WorkshopId) .ToArray(); - if (missingIds.Any()) + if (missingIds.Any() && SteamManager.IsInitialized) { buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), innerLayout.RectTransform), isHorizontal: true); buttonContainerSpacing(0.15f); @@ -208,7 +207,7 @@ namespace Barotrauma { if (currentDownload == p) { - FileReceiver.FileTransferIn? getTransfer() => GameMain.Client.FileReceiver.ActiveTransfers.FirstOrDefault(t => t.FileType == FileTransferType.Mod); + FileReceiver.FileTransferIn? getTransfer() => GameMain.Client?.FileReceiver.ActiveTransfers.FirstOrDefault(t => t.FileType == FileTransferType.Mod); if (downloadProgress.GetAnyChild() is null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index b3949ac46..a0cb2b9f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -2298,7 +2298,7 @@ namespace Barotrauma text: selectedClient.Name, font: GUIStyle.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); - if (hasManagePermissions) + if (hasManagePermissions && !selectedClient.IsOwner) { PlayerFrame.UserData = selectedClient; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index bd2b9b4c9..1bf91b69e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -557,7 +557,9 @@ namespace Barotrauma }; serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) { - Padding = Vector4.One * 10 * GUI.Scale + Padding = Vector4.One * 10 * GUI.Scale, + HoverCursor = CursorState.Default, + OnSelected = (component, o) => false }; // Spacing @@ -915,12 +917,7 @@ namespace Barotrauma { case "ServerListCompatible": bool? s1Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s1.GameVersion); - if (!s1.ContentPackageHashes.Any()) { s1Compatible = null; } - if (s1Compatible.HasValue) { s1Compatible = s1Compatible.Value && s1.ContentPackagesMatch(); }; - bool? s2Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s2.GameVersion); - if (!s2.ContentPackageHashes.Any()) { s2Compatible = null; } - if (s2Compatible.HasValue) { s2Compatible = s2Compatible.Value && s2.ContentPackagesMatch(); }; //convert to int to make sorting easier //1 Compatible @@ -1028,8 +1025,7 @@ namespace Barotrauma foreach (GUIComponent child in serverList.Content.Children) { - if (!(child.UserData is ServerInfo)) continue; - ServerInfo serverInfo = (ServerInfo)child.UserData; + if (!(child.UserData is ServerInfo serverInfo)) { continue; } Version remoteVersion = null; if (!string.IsNullOrEmpty(serverInfo.GameVersion)) @@ -1047,8 +1043,7 @@ namespace Barotrauma else { bool incompatible = - (serverInfo.ContentPackageHashes.Any() && !serverInfo.ContentPackagesMatch()) || - (remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion)); + remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion); var karmaFilterPassed = filterKarmaValue == TernaryOption.Any|| (filterKarmaValue == TernaryOption.Enabled) == serverInfo.KarmaEnabled; var friendlyFireFilterPassed = filterFriendlyFireValue == TernaryOption.Any || (filterFriendlyFireValue == TernaryOption.Enabled) == serverInfo.FriendlyFireEnabled; @@ -1798,8 +1793,7 @@ namespace Barotrauma { CanBeFocused = false, Selected = - (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true) && - serverInfo.ContentPackagesMatch(), + (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true), UserData = "compatible" }; @@ -1826,9 +1820,9 @@ namespace Barotrauma if (serverInfo.ContentPackageNames.Any()) { - if (serverInfo.ContentPackageNames.Any(cp => !cp.Equals(GameMain.VanillaContent.Name, StringComparison.OrdinalIgnoreCase))) + if (serverInfo.ContentPackageNames.Any(p => !GameMain.VanillaContent.NameMatches(p))) { - serverName.TextColor = new Color(219, 125, 217); + serverName.TextColor = GUIStyle.ModdedServerColor; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 4d72c0d65..6e6826302 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -2841,6 +2841,7 @@ namespace Barotrauma private void CreateLoadScreen() { CloseItem(); + SubmarineInfo.RefreshSavedSubs(); SetMode(Mode.Default); loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) @@ -3162,7 +3163,6 @@ namespace Barotrauma } sub.Dispose(); - SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } catch (Exception e) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 823439ace..977add7cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -168,7 +168,10 @@ namespace Barotrauma { var dropdown = new GUIDropDown(NewItemRectT(parent)); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); - dropdown.Select(values.IndexOf(currentValue)); + int childIndex = values.IndexOf(currentValue); + dropdown.Select(childIndex); + dropdown.ListBox.ForceLayoutRecalculation(); + dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex), playSound: false); dropdown.OnSelected = (dd, obj) => { setter((T)obj); @@ -231,6 +234,8 @@ namespace Barotrauma GameMain.GraphicsDeviceManager.GraphicsDevice.Adapter.SupportedDisplayModes .Where(m => m.Format == SurfaceFormat.Color) .Select(m => (m.Width, m.Height)) + .Where(m => m.Width >= GameSettings.Config.GraphicsSettings.MinSupportedResolution.X + && m.Height >= GameSettings.Config.GraphicsSettings.MinSupportedResolution.Y) .ToList(); var currentResolution = (unsavedConfig.Graphics.Width, unsavedConfig.Graphics.Height); if (!supportedResolutions.Contains(currentResolution)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index ac40b98bd..7fe005bb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -372,13 +372,10 @@ namespace Barotrauma.Sounds } var newSound = new OggSound(this, filePath, stream, xElement: element); - if (newSound != null) - { - newSound.BaseGain = element.GetAttributeFloat("volume", 1.0f); - float range = element.GetAttributeFloat("range", 1000.0f); - newSound.BaseNear = range * 0.4f; - newSound.BaseFar = range; - } + newSound.BaseGain = element.GetAttributeFloat("volume", 1.0f); + float range = element.GetAttributeFloat("range", 1000.0f); + newSound.BaseNear = range * 0.4f; + newSound.BaseFar = range; lock (loadedSounds) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index ee74766d9..cdd692671 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -178,6 +178,7 @@ namespace Barotrauma SoundChannel chn = waterAmbienceChannels.FirstOrDefault(c => c.Sound == sound); if (chn is null || !chn.IsPlaying) { + if (volume < 0.01f) { return; } if (!(chn is null)) { waterAmbienceChannels.Remove(chn); } chn = sound.Play(volume, "waterambience"); chn.Looping = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs index 14b0b68e2..9eab19935 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPrefab.cs @@ -202,6 +202,11 @@ 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 3a90d6ea1..37a04c2a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/Workshop.cs @@ -270,8 +270,9 @@ namespace Barotrauma.Steam .ToHashSet(); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh()); + var installWaiter = WaitForInstall(workshopItem); DownloadModThenEnqueueInstall(workshopItem); - await WaitForInstall(workshopItem); + await installWaiter; } public static async Task WaitForInstall(Steamworks.Ugc.Item item) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs index 5b87ab120..484fe3182 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Immutable/ImmutableWorkshopMenu.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Linq; using Barotrauma.Extensions; using Microsoft.Xna.Framework; @@ -39,6 +40,11 @@ namespace Barotrauma.Steam CanBeFocused = false, UserData = p }; + if (p.Errors.Any()) + { + CreateModErrorInfo(p, regularBox, regularBox); + regularBox.CanBeFocused = true; + } } filterBox = CreateSearchBox(mainLayout, width: 1.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index 4e01d5ece..d886a30dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -20,10 +20,10 @@ namespace Barotrauma.Steam Publish } - protected readonly GUILayoutGroup tabber; - protected readonly Dictionary tabContents; + private readonly GUILayoutGroup tabber; + private readonly Dictionary tabContents; - protected readonly GUIFrame contentFrame; + private readonly GUIFrame contentFrame; private CorePackage EnabledCorePackage => enabledCoreDropdown.SelectedData as CorePackage ?? throw new Exception("Valid core package not selected"); @@ -40,6 +40,8 @@ namespace Barotrauma.Steam private readonly GUIListBox popularModsList; private readonly GUIListBox selfModsList; + private uint memSubscribedModCount = 0; + public MutableWorkshopMenu(GUIFrame parent) : base(parent) { var mainLayout @@ -50,6 +52,9 @@ namespace Barotrauma.Steam tabContents = new Dictionary(); contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null); + + new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform), + onUpdate: (f, component) => UpdateSubscribedModInstalls()); CreateInstalledModsTab( out enabledCoreDropdown, @@ -64,6 +69,38 @@ namespace Barotrauma.Steam SelectTab(Tab.InstalledMods); } + private void UpdateSubscribedModInstalls() + { + if (!SteamManager.IsInitialized) { return; } + + uint numSubscribedMods = Steamworks.SteamUGC.NumSubscribedItems; + if (numSubscribedMods == memSubscribedModCount) { return; } + memSubscribedModCount = numSubscribedMods; + + var subscribedIds = Steamworks.SteamUGC.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(); + foreach (var id in installedIds.Where(id2 => !allRequiredInstalled.Contains(id2))) + { + Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); + SteamManager.Workshop.Uninstall(item); + } + }); + } + private void SwitchContent(GUIFrame newContent) { contentFrame.Children.ForEach(c => c.Visible = false); @@ -462,6 +499,10 @@ namespace Barotrauma.Steam { 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), "", @@ -593,6 +634,7 @@ namespace Barotrauma.Steam ContentPackageManager.EnabledPackages.SetRegular(enabledRegularModsList.Content.Children .Select(c => c.UserData as RegularPackage).OfType().ToArray()); PopulateInstalledModLists(forceRefreshEnabled: true, refreshDisabled: true); + ContentPackageManager.LogEnabledRegularPackageErrors(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs index 78a08a529..692bc61b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/UiUtil.cs @@ -129,5 +129,20 @@ namespace Barotrauma.Steam }; return searchBox; } + + protected void CreateModErrorInfo(ContentPackage mod, GUIComponent uiElement, GUITextBlock nameText) + { + if (mod.Errors.Any()) + { + 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)); + if (mod.Errors.Count() > maxErrorsToShow) + { + uiElement.ToolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (mod.Errors.Count() - maxErrorsToShow).ToString()); + } + } + } } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index fceac628c..a49d98a1d 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.6.0 + 0.17.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index cc6dc075d..b8ef2e701 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.6.0 + 0.17.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 77da65846..c4137dcdf 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.17.6.0 + 0.17.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 0abfeb8ce..6e72c14b2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.6.0 + 0.17.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 3c9b6adea..989b6a1f4 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.6.0 + 0.17.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 532d46b51..4af9addf0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -106,6 +106,7 @@ namespace Barotrauma GameModePreset.Init(); ContentPackageManager.Init().Consume(); + ContentPackageManager.LogEnabledRegularPackageErrors(); SubmarineInfo.RefreshSavedSubs(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index cef019215..ff0fc0357 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -917,18 +917,17 @@ namespace Barotrauma TransferMoney(wallet); break; case None _: - if (!AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) + if (!AllowedToManageCampaign(sender, ClientPermissions.ManageMoney)) { if (transfer.Receiver is Some { Value: var receiverId } && receiverId == sender.CharacterID) { 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; - } - else - { - TransferMoney(Bank); + return; } + + TransferMoney(Bank); break; } @@ -943,9 +942,11 @@ namespace Barotrauma if (wallet is InvalidWallet) { return; } wallet.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); break; case None _: Bank.Give(transfer.Amount); + GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money); break; } } @@ -965,6 +966,7 @@ namespace Barotrauma Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target); targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution); + GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name ?? "the bank"} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money); } public void ServerReadCrew(IReadMessage msg, Client sender) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 408c3ae61..ae0f4c4c6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -46,7 +46,7 @@ namespace Barotrauma.Networking { //setting a wait timer means that network conditions //aren't ideal, slow down the packet rate - PacketsPerUpdate = Math.Max(PacketsPerUpdate / 2.0f, 1.0f); + PacketsPerUpdate = Math.Max(PacketsPerUpdate / 4.0f, 1.0f); } waitTimer = value; } @@ -130,7 +130,7 @@ namespace Barotrauma.Networking public FileSender(ServerPeer serverPeer, int mtu) { peer = serverPeer; - chunkLen = mtu - 100; + chunkLen = mtu - 200; activeTransfers = new List(); } @@ -197,11 +197,8 @@ namespace Barotrauma.Networking foreach (FileTransferOut transfer in activeTransfers) { transfer.WaitTimer -= deltaTime; - for (int i = 0; i < 10; i++) - { - if (transfer.WaitTimer > 0.0f) { break; } - Send(transfer); - } + if (transfer.WaitTimer > 0.0f) { continue; } + Send(transfer); } if (numRemoved > 0 || endedTransfers.Count > 0) @@ -281,7 +278,7 @@ namespace Barotrauma.Networking if (transfer.SentOffset >= transfer.Data.Length) { transfer.SentOffset = transfer.KnownReceivedOffset; - transfer.WaitTimer = 0.5f; + transfer.WaitTimer = 1.0f; } peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable, compressPastThreshold: false); @@ -356,7 +353,7 @@ namespace Barotrauma.Networking matchingTransfer.SentOffset >= matchingTransfer.Data.Length) { matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; - matchingTransfer.WaitTimer = 0.5f; + matchingTransfer.WaitTimer = 1.0f; } if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) @@ -364,6 +361,7 @@ namespace Barotrauma.Networking matchingTransfer.Status = FileTransferStatus.Finished; } } + return; } FileTransferType fileType = (FileTransferType)inc.ReadByte(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 985bcdf3c..f8a129f51 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -91,13 +91,16 @@ namespace Barotrauma private void StartSubmarineVote(SubmarineInfo subInfo, VoteType voteType, Client sender) { + if (ActiveVote == null) + { + sender.SetVote(voteType, 2); + } var subVote = new SubmarineVote( sender, subInfo, voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0, voteType); StartOrEnqueueVote(subVote); - sender.SetVote(voteType, 2); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } @@ -127,13 +130,17 @@ namespace Barotrauma if (pendingVotes.Any()) { ActiveVote = pendingVotes.Dequeue(); + ActiveVote.VoteStarter?.SetVote(ActiveVote.VoteType, 2); } } public void StartTransferVote(Client starter, Client from, int transferAmount, Client to) { + if (ActiveVote == null) + { + starter.SetVote(VoteType.TransferMoney, 2); + } StartOrEnqueueVote(new TransferVote(starter, from, transferAmount, to)); - starter.SetVote(VoteType.TransferMoney, 2); GameMain.Server.UpdateVoteStatus(checkActiveVote: false); } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index a5861e794..a56b1996b 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.17.6.0 + 0.17.7.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index e3f0db038..08fa162ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -62,9 +62,9 @@ namespace Barotrauma private float enemycheckTimer; /// - /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). + /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. /// - public float ReportRange { get; set; } + public float ReportRange { get; set; } = float.PositiveInfinity; private float _aimSpeed = 1; public float AimSpeed @@ -166,7 +166,6 @@ namespace Barotrauma objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); SortTimer = Rand.Range(0f, sortObjectiveInterval); - ReportRange = Character.IsOnPlayerTeam ? float.PositiveInfinity : 1000; } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 4abcd54ee..0c46fca5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -269,6 +269,18 @@ namespace Barotrauma if (!character.AnimController.InWater || character.Submarine != null) { return; } if (CurrentPath == null || CurrentPath.Unreachable || CurrentPath.Finished) { return; } if (CurrentPath.CurrentIndex < 0 || CurrentPath.CurrentIndex >= CurrentPath.Nodes.Count - 1) { return; } + var lastNode = CurrentPath.Nodes.Last(); + Submarine targetSub = lastNode.Submarine; + if (targetSub != null) + { + float subSize = Math.Max(targetSub.Borders.Size.X, targetSub.Borders.Size.Y) / 2; + float margin = 500; + if (Vector2.DistanceSquared(character.WorldPosition, targetSub.WorldPosition) < MathUtils.Pow2(subSize + margin)) + { + // Don't skip nodes when close to the target submarine. + return; + } + } // Check if we could skip ahead to NextNode when the character is swimming and using waypoints outside. // Do this to optimize the old path before creating and evaluating a new path. // In general, this is to avoid behavior where: @@ -280,7 +292,7 @@ namespace Barotrauma { var waypoint = CurrentPath.Nodes[i]; float directDistance = Vector2.DistanceSquared(character.WorldPosition, waypoint.WorldPosition); - if (directDistance > (pathDistance * pathDistance) || Submarine.PickBody(host.SimPosition, waypoint.SimPosition, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + if (directDistance > MathUtils.Pow2(pathDistance) || !character.CanSeeTarget(waypoint)) { pathDistance -= CurrentPath.GetLength(startIndex: i - 1, endIndex: i); continue; @@ -336,6 +348,7 @@ namespace Barotrauma return Vector2.Zero; } Vector2 pos = host.WorldPosition; + Vector2 diff = currentPath.CurrentNode.WorldPosition - pos; bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; @@ -346,7 +359,7 @@ namespace Barotrauma } Ladder nextLadder = GetNextLadder(); var ladders = currentLadder ?? nextLadder; - bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && steering.Y > 1); + bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1); if (useLadders && character.SelectedConstruction != ladders.Item) { if (character.CanInteractWith(ladders.Item)) @@ -374,7 +387,6 @@ namespace Barotrauma } if (character.IsClimbing && useLadders) { - Vector2 diff = currentPath.CurrentNode.WorldPosition - pos; bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) { @@ -398,7 +410,7 @@ namespace Barotrauma else if (nextLadder != null && !nextLadderSameAsCurrent) { // Try to change the ladder (hatches between two submarines) - if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition)) + if (character.SelectedConstruction != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) { if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) { @@ -406,7 +418,7 @@ namespace Barotrauma } } } - if (isAboveFloor || nextLadderSameAsCurrent) + if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10) { NextNode(!doorsChecked); } @@ -484,7 +496,7 @@ namespace Barotrauma { return Vector2.Zero; } - return ConvertUnits.ToSimUnits(currentPath.CurrentNode.WorldPosition - pos); + return ConvertUnits.ToSimUnits(diff); } private void NextNode(bool checkDoors) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index f93ededde..bdc673490 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -212,10 +212,14 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { - if (sqrDistance > maxDistance * maxDistance) + if (character.Submarine == null || character.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { - // The target escaped from us. - return true; + // Can't lose the target in friendly outposts. + if (sqrDistance > maxDistance * maxDistance) + { + // The target escaped from us. + return true; + } } return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index c66746f9a..ba7a674fb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -101,7 +101,7 @@ namespace Barotrauma { if (InWater || !CanWalk) { - return TargetMovement.LengthSquared() > SwimSlowParams.MovementSpeed; + return TargetMovement.LengthSquared() > MathUtils.Pow2(SwimSlowParams.MovementSpeed); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index e524f3a87..d47bbf405 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -302,7 +302,7 @@ namespace Barotrauma } // used for talents/ability conditions - public Item SourceItem { get; } + public Item SourceItem { get; set; } public List GetMultipliedAfflictions(float multiplier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 5182ff0b6..57df4275c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -308,6 +308,10 @@ namespace Barotrauma public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; + public bool IsMale => info?.IsMale ?? false; + + public bool IsFemale => info?.IsFemale ?? false; + public string BloodDecalName => Params.BloodDecal; public bool CanSpeak @@ -557,10 +561,11 @@ namespace Barotrauma #if CLIENT CharacterHealth.SetHealthBarVisibility(value == null); #elif SERVER - if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet }) + if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0) { Wallet.Give(balance); grabbedWallet.Deduct(balance); + GameServer.Log($"{Name} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money); } #endif } @@ -3587,21 +3592,8 @@ namespace Barotrauma float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime; - AbilityAttackData attackData = new AbilityAttackData(attack, this); - if (attacker != null) - { - attackData.Attacker = attacker; - attacker.CheckTalents(AbilityEffectType.OnAttack, attackData); - CheckTalents(AbilityEffectType.OnAttacked, attackData); - attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.AttackMultiplier); - if (attacker.TeamID == TeamID) - { - attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.TeamAttackMultiplier); - } - } - + AbilityAttackData attackData = new AbilityAttackData(attack, this, attacker); IEnumerable attackAfflictions; - if (attackData.Afflictions != null) { attackAfflictions = attackData.Afflictions.Union(attack.Afflictions.Keys); @@ -4916,10 +4908,21 @@ namespace Barotrauma public Character Character { get; set; } public Character Attacker { get; set; } - public AbilityAttackData(Attack sourceAttack, Character character) + public AbilityAttackData(Attack sourceAttack, Character target, Character attacker) { SourceAttack = sourceAttack; - Character = character; + Character = target; + if (attacker != null) + { + Attacker = attacker; + attacker.CheckTalents(AbilityEffectType.OnAttack, this); + target.CheckTalents(AbilityEffectType.OnAttacked, this); + DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.AttackMultiplier); + if (attacker.TeamID == target.TeamID) + { + DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.TeamAttackMultiplier); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index df1cede68..1d384e608 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -130,17 +130,22 @@ namespace Barotrauma head = value; HeadSprite = null; AttachmentSprites = null; + IsMale = value.Preset?.TagSet?.Contains("Male".ToIdentifier()) ?? false; + IsFemale = value.Preset?.TagSet?.Contains("Female".ToIdentifier()) ?? false; } } } + public bool IsMale { get; private set; } + + public bool IsFemale { get; private set; } + public CharacterInfoPrefab Prefab => CharacterPrefab.Prefabs[SpeciesName].CharacterInfoPrefab; public class HeadPreset : ISerializableEntity { private readonly CharacterInfoPrefab characterInfoPrefab; public Identifier MenuCategory => TagSet.First(t => characterInfoPrefab.VarTags[characterInfoPrefab.MenuCategoryVar].Contains(t)); - public ImmutableHashSet TagSet { get; private set; } [Serialize("", IsPropertySaveable.No)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs index 35865bb05..4f853ba01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/ContentFile.cs @@ -71,8 +71,8 @@ namespace Barotrauma public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { - static Result fail(string error) - => Result.Failure(error); + static Result fail(string error, string? stackTrace = null) + => Result.Failure(error, stackTrace); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); @@ -99,10 +99,10 @@ namespace Barotrauma } catch (Exception e) { - return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}\n{e.StackTrace.CleanupStackTrace()}"); + return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e.StackTrace.CleanupStackTrace()); } } - + protected ContentFile(ContentPackage contentPackage, ContentPath path) { ContentPackage = contentPackage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs index 3523bb443..e59272553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentFile/GenericPrefabFile.cs @@ -34,7 +34,15 @@ namespace Barotrauma else if (MatchesSingular(elemName)) { T prefab = CreatePrefab(parentElement); - prefabs.Add(prefab, overriding); + try + { + prefabs.Add(prefab, overriding); + } + catch + { + prefab.Dispose(); //clean up before rethrowing, since some prefab types might lock resources + throw; + } } else if (MatchesPlural(elemName)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs index 586da6857..35e6b6436 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackage/ContentPackage.cs @@ -1,6 +1,6 @@ #nullable enable using Barotrauma.Extensions; -using Microsoft.Xna.Framework; +using Barotrauma.Steam; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -9,7 +9,6 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; -using Barotrauma.Steam; namespace Barotrauma { @@ -39,7 +38,7 @@ namespace Barotrauma public readonly DateTime? InstallTime; public readonly ImmutableArray Files; - public readonly ImmutableArray Errors; + public readonly ImmutableArray<(string error, string? stackTrace)> Errors; public async Task IsUpToDate() { @@ -92,7 +91,7 @@ namespace Barotrauma Errors = fileResults .OfType>() - .Select(f => f.Error) + .Select(f => (f.Error, f.StackTrace)) .ToImmutableArray(); HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer); @@ -304,5 +303,24 @@ namespace Barotrauma } return path == LocalModsDir; } + + public void LogErrors() + { + 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; + } + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs index add517a0c..533690a7a 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 in VanillaCorePackage.Errors) + foreach ((string error, string? stackTrace) in VanillaCorePackage.Errors) { - DebugConsole.ThrowError(error); + DebugConsole.ThrowError(error + (stackTrace == null ? string.Empty : '\n' + stackTrace)); } } @@ -469,8 +469,17 @@ namespace Barotrauma } var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName); - enabledCorePackage = findPackage(CorePackages, corePackageElement) ?? VanillaCorePackage!; - + var configEnabledCorePackage = findPackage(CorePackages, corePackageElement); + if (configEnabledCorePackage == null) + { + string packageStr = corePackageElement.GetAttributeString("name", null) ?? corePackageElement.GetAttributeStringUnrestricted("path", "UNKNOWN"); + DebugConsole.ThrowError($"Could not find the selected core package \"{packageStr}\". Switching to the \"{enabledCorePackage.Name}\" package."); + } + else + { + enabledCorePackage = configEnabledCorePackage; + } + var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName); if (regularPackagesElement != null) { @@ -499,5 +508,13 @@ namespace Barotrauma yield return new LoadProgress(1.0f); } + + public static void LogEnabledRegularPackageErrors() + { + foreach (var p in EnabledPackages.Regular) + { + p.LogErrors(); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index f9b027f33..077da45bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1562,17 +1562,24 @@ namespace Barotrauma commands.Add(new Command("money", "money [amount] [character]: Gives the specified amount of money to the crew when a campaign is active.", args => { if (args.Length == 0) { return; } - if (GameMain.GameSession?.GameMode is CampaignMode campaign) + + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) { return; } + Character targetCharacter = null; + + if (args.Length >= 2) { - if (int.TryParse(args[0], out int money)) - { - campaign.Bank.Give(money); - GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); - } - else - { - ThrowError($"\"{args[0]}\" is not a valid numeric value."); - } + targetCharacter = FindMatchingCharacter(args.Skip(1).ToArray()); + } + + if (int.TryParse(args[0], out int money)) + { + Wallet wallet = targetCharacter is null || GameMain.IsSingleplayer ? campaign.Bank : targetCharacter.Wallet; + wallet.Give(money); + GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console"); + } + else + { + ThrowError($"\"{args[0]}\" is not a valid numeric value."); } }, isCheat: true, getValidArgs: () => new [] { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs index 2b0cd4756..af71aa722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Decals/DecalManager.cs @@ -1,10 +1,8 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography; using System.Xml.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -33,11 +31,11 @@ namespace Barotrauma public static int GrimeSpriteCount { get; private set; } = 0; public static readonly PrefabCollection GrimeSprites = new PrefabCollection( - onAdd: (sprite, b) => GrimeSpriteCount = Math.Max(GrimeSpriteCount, sprite.IndexInFile+1), + onAdd: (sprite, b) => GrimeSpriteCount = Math.Max(GrimeSpriteCount, sprite.IndexInFile + 1), onRemove: (s) => GrimeSpriteCount = GrimeSprites.AllPrefabs .SelectMany(kvp => kvp.Value) - .Where(p => p != s).Select(p => p.IndexInFile+1).MaxOrNull() ?? 0, + .Where(p => p != s).Select(p => p.IndexInFile + 1).MaxOrNull() ?? 0, onSort: null, onAddOverrideFile: null, onRemoveOverrideFile: null); public static void LoadFromFile(DecalsFile configFile) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 11ed0fd52..9e3c000b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -102,7 +102,7 @@ namespace Barotrauma item.GetComponent() != null || item.GetComponent() != null) { - item.Indestructible = true; + item.InvulnerableToDamage = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs index e10894b20..1005729bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Wallet.cs @@ -210,6 +210,13 @@ namespace Barotrauma }; } + public string GetOwnerLogName() => Owner switch + { + Some { Value: var character } => character.Name, + None _ => "the bank", + _ => throw new ArgumentOutOfRangeException(nameof(Owner)) + }; + partial void SettingsChanged(Option balanceChanged, Option rewardChanged); private static int ClampBalance(int value) => Math.Clamp(value, 0, CampaignMode.MaxMoney); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 44a2cdf9b..43e206d8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -147,22 +147,23 @@ namespace Barotrauma var campaign = SinglePlayerCampaign.Load(subElement); campaign.LoadNewLevel(); GameMode = campaign; + InitOwnedSubs(submarineInfo, ownedSubmarines); break; #endif case "multiplayercampaign": CrewManager = new CrewManager(false); var mpCampaign = MultiPlayerCampaign.LoadNew(subElement); GameMode = mpCampaign; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - mpCampaign.LoadNewLevel(); + mpCampaign.LoadNewLevel(); + InitOwnedSubs(submarineInfo, ownedSubmarines); //save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance SaveUtil.SaveGame(saveFile); } break; } } - InitOwnedSubs(submarineInfo); } private void InitOwnedSubs(SubmarineInfo submarineInfo, List? ownedSubmarines = null) @@ -409,14 +410,16 @@ namespace Barotrauma if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false) { - foreach (Item item in Item.ItemList) + List items = new List(); + items.AddRange(Submarine.MainSubs[0].GetItems(alsoFromConnectedSubs: true)); + if (Submarine.MainSubs[1] != null) { - if (item.Submarine == Submarine.MainSubs[0] || - (Submarine.MainSubs[1] != null && item.Submarine == Submarine.MainSubs[1])) - { - Wire wire = item.GetComponent(); - if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; } - } + items.AddRange(Submarine.MainSubs[1].GetItems(alsoFromConnectedSubs: true)); + } + foreach (Item item in items) + { + Wire wire = item.GetComponent(); + if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 1319254d4..7a624ac27 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -228,9 +228,12 @@ namespace Barotrauma.Items.Components { if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection)) { - Vector2 rayDir = rayStart.NearlyEquals(sourcePos) ? Vector2.Zero : Vector2.Normalize(rayStart - sourcePos); - rayStartWorld = ConvertUnits.ToSimUnits(hullIntersection - rayDir * 5.0f); - if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; } + if (!item.CurrentHull.ConnectedGaps.Any(g => g.Open > 0.0f && Submarine.RectContains(g.Rect, hullIntersection))) + { + Vector2 rayDir = rayStart.NearlyEquals(sourcePos) ? Vector2.Zero : Vector2.Normalize(rayStart - sourcePos); + rayStartWorld = ConvertUnits.ToSimUnits(hullIntersection - rayDir * 5.0f); + if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 028e59a8e..df622c16c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -251,7 +251,7 @@ namespace Barotrauma return true; } #endif - + if (HiddenInGame) { return false; } if (character != null && character.IsOnPlayerTeam) { return IsPlayerTeamInteractable; @@ -1438,7 +1438,6 @@ namespace Barotrauma public bool HasAccess(Character character) { - if (HiddenInGame) { return false; } if (character.IsBot && IgnoreByAI(character)) { return false; } if (!IsInteractable(character)) { return false; } var itemContainer = GetComponent(); @@ -1834,10 +1833,29 @@ namespace Barotrauma Submarine prevSub = Submarine; var projectile = GetComponent(); - if (projectile?.StickTarget?.UserData is Limb limb && limb.character != null) + if (projectile?.StickTarget != null) { - Submarine = body.Submarine = limb.character.Submarine; - currentHull = limb.character.CurrentHull; + if (projectile?.StickTarget.UserData is Limb limb && limb.character != null) + { + Submarine = body.Submarine = limb.character.Submarine; + currentHull = limb.character.CurrentHull; + } + else if (projectile.StickTarget.UserData is Structure structure) + { + Submarine = body.Submarine = structure.Submarine; + currentHull = Hull.FindHull(WorldPosition, CurrentHull); + } + else if (projectile.StickTarget.UserData is Item targetItem) + { + Submarine = body.Submarine = targetItem.Submarine; + currentHull = targetItem.CurrentHull; + } + else if (projectile.StickTarget.UserData is Submarine) + { + //attached to a sub from the outside -> don't move inside the sub + Submarine = body.Submarine = null; + currentHull = null; + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 89ee34a92..8ab8ee918 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -133,6 +133,7 @@ namespace Barotrauma { displayRange *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius); Attack.DamageMultiplier *= 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage); + Attack.SourceItem ??= sourceItem; } Vector2 cameraPos = GameMain.GameScreen.Cam.Position; @@ -337,11 +338,17 @@ namespace Barotrauma } } + AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker); + if (attackData.Afflictions != null) + { + modifiedAfflictions.AddRange(attackData.Afflictions); + } + //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 dir = worldPosition - limb.WorldPosition; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier); + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier); damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index e531500bf..f4172ff8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -23,6 +23,10 @@ namespace Barotrauma public readonly Vector2 Noise; public readonly Color DirtColor; +#if CLIENT + public Sprite GrimeSprite; +#endif + public float ColorStrength { get; @@ -51,6 +55,9 @@ namespace Barotrauma PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f)); Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); +#if CLIENT + GrimeSprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{index % DecalManager.GrimeSpriteCount}"].Sprite; +#endif } public BackgroundSection(Rectangle rect, ushort index, float colorStrength, Color color, ushort rowIndex) @@ -68,6 +75,9 @@ namespace Barotrauma PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f)); DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X); +#if CLIENT + GrimeSprite = DecalManager.GrimeSprites[$"{nameof(GrimeSprite)}{index % DecalManager.GrimeSpriteCount}"].Sprite; +#endif } public bool SetColor(Color color) @@ -274,33 +284,17 @@ namespace Barotrauma get { return Submarine == null ? surface : surface + Submarine.Position.Y; } } - private float dirtiedVolume = 0.0f; - public float WaterVolume { get { return waterVolume; } set { - if (!MathUtils.IsValid(value)) return; + if (!MathUtils.IsValid(value)) { return; } waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress); if (waterVolume < Volume) { Pressure = rect.Y - rect.Height + waterVolume / rect.Width; } if (waterVolume > 0.0f) { update = true; - if (BackgroundSections != null) - { - float volumeMultiplier = Math.Clamp(waterVolume / Volume, 0f, 1f); - if (Math.Abs(volumeMultiplier - dirtiedVolume) > 0.075f) - { - RefreshSubmergedSections(new Rectangle(new Point(0, -rect.Height), new Point(rect.Width, (int)(rect.Height * volumeMultiplier)))); - dirtiedVolume = volumeMultiplier; - } - } - } - else - { - submergedSections.Clear(); - dirtiedVolume = 0.0f; } } } @@ -391,8 +385,6 @@ namespace Barotrauma private readonly HashSet pendingSectionUpdates = new HashSet(); - private readonly List submergedSections = new List(); - public int xBackgroundMax, yBackgroundMax; public bool SupportsPaintedColors @@ -664,7 +656,6 @@ namespace Barotrauma } BackgroundSections?.Clear(); - submergedSections?.Clear(); List fireSourcesToRemove = new List(FireSources); foreach (FireSource fireSource in fireSourcesToRemove) @@ -973,12 +964,6 @@ namespace Barotrauma } } - //0.016 increase every ~2000 frames = reaches full dirtiness in ~35 minutes - if (submergedSections.Count > 0 && Submarine != null && Submarine.Info.Type == SubmarineType.Player && Rand.Int(2000) == 1) - { - DirtySections(submergedSections, deltaTime); - } - if (waterVolume < Volume) { LethalPressure -= 10.0f * deltaTime; @@ -1451,17 +1436,6 @@ namespace Barotrauma } } - public void RefreshSubmergedSections(Rectangle waterArea) - { - if (BackgroundSections == null) { return; } - - submergedSections.Clear(); - foreach (var section in GetBackgroundSectionsViaContaining(waterArea)) - { - submergedSections.Add(section); - } - } - public bool DoesSectionMatch(int index, int row) { return index >= 0 && row >= 0 && BackgroundSections.Count > index && BackgroundSections[index] != null && BackgroundSections[index].RowIndex == row; @@ -1528,15 +1502,6 @@ namespace Barotrauma } } - public void DirtySections(List sections, float dirtyVal) - { - if (sections == null) { return; } - for (int i = 0; i < sections.Count; i++) - { - IncreaseSectionColorOrStrength(sections[i], sections[i].DirtColor, dirtyVal, false, false); - } - } - public void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired) { bool decalsCleaned = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 89c509288..501c9d163 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -37,10 +37,14 @@ namespace Barotrauma.RuinGeneration Indent = true, NewLineOnAttributes = true }; - + + IEnumerable packages = ContentPackageManager.LocalPackages; +#if DEBUG + packages = packages.Union(ContentPackageManager.VanillaCorePackage.ToEnumerable()); +#endif foreach (RuinGenerationParams generationParams in RuinParams) { - foreach (RuinConfigFile configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) + foreach (RuinConfigFile configFile in packages.SelectMany(p => p.GetFiles())) { if (configFile.Path != generationParams.ContentFile.Path) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 7a7fa2598..258e7262f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Reflection; -using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -13,16 +11,20 @@ namespace Barotrauma enum MapEntityCategory { Structure = 1, - Decorative = 2, - Machine = 4, - Equipment = 8, - Electrical = 16, - Material = 32, - Misc = 64, - Alien = 128, - Wrecked = 256, - ItemAssembly = 512, - Legacy = 1024 + Decorative = 2, + Machine = 4, + Medical = 8, + Weapon = 16, + Diving = 32, + Equipment = 64, + Fuel = 128, + Electrical = 256, + Material = 1024, + Alien = 2048, + Wrecked = 4096, + ItemAssembly = 8192, + Legacy = 16384, + Misc = 32768 } abstract partial class MapEntityPrefab : PrefabWithUintIdentifier diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 594adb116..295b92751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1540,8 +1540,10 @@ namespace Barotrauma List killedCharacters = new List(); List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)> selectedCharacters = new List<(HumanPrefab HumanPrefab, CharacterInfo CharacterInfo)>(); - foreach (HumanPrefab humanPrefab in outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient)) + var humanPrefabs = outpost.Info.OutpostGenerationParams.GetHumanPrefabs(Rand.RandSync.ServerAndClient); + foreach (HumanPrefab humanPrefab in humanPrefabs) { + if (humanPrefab is null) { continue; } var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.ServerAndClient), randSync: Rand.RandSync.ServerAndClient); if (location != null && location.KilledCharacterIdentifiers.Contains(characterInfo.GetIdentifier())) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 26c2f92e0..0a1356efd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -153,6 +153,7 @@ namespace Barotrauma { Name = TextManager.GetWithVariable("wreckeditemformat", "[name]", Name); } + Name = Name.Fallback(OriginalName); var tags = new HashSet(); string joinedTags = element.GetAttributeString("tags", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index b7edffbbb..84f6527c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -139,7 +139,7 @@ namespace Barotrauma.Networking } } - public bool HasPermissions = false; + public bool HasPermissions => Permissions != ClientPermissions.None; public VoipQueue VoipQueue { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index a86284f23..fcc0bed07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -38,6 +38,7 @@ namespace Barotrauma.Networking Wiring, ServerMessage, ConsoleUsage, + Money, Karma, Talent, Error, @@ -53,9 +54,10 @@ namespace Barotrauma.Networking { MessageType.Wiring, new Color(255, 157, 85) }, { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, + { MessageType.Money, Color.Green }, { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Talent, new Color(125, 125, 255) }, - { MessageType.Error, Color.Red }, + { MessageType.Error, Color.Red } }; private readonly Dictionary messageTypeName = new Dictionary @@ -68,6 +70,7 @@ namespace Barotrauma.Networking { MessageType.Wiring, "Wiring" }, { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, + { MessageType.Money, "Money" }, { MessageType.Karma, "Karma" }, { MessageType.Talent, "Talent" }, { MessageType.Error, "Error" } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 765a62374..d6f2ec544 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -1007,6 +1007,19 @@ namespace Barotrauma } } } + else if (attributeName == "move") + { + Vector2 moveAmount = subElement.GetAttributeVector2("move", Vector2.Zero); + if (entity is Structure structure) + { + structure.Move(moveAmount); + } + else if (entity is Item item) + { + item.Move(moveAmount); + } + continue; + } if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs index 551b48fdc..7292d5bf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs @@ -148,6 +148,8 @@ namespace Barotrauma public struct GraphicsSettings { + public static readonly Point MinSupportedResolution = new Point(1024, 540); + public static GraphicsSettings GetDefault() { GraphicsSettings gfxSettings = new GraphicsSettings diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index f7445abed..45aa60f26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -167,16 +167,9 @@ namespace Barotrauma var type = Type; if (type == ConditionType.Uncertain) { - if (AfflictionPrefab.Prefabs.ContainsKey(AttributeName)) - { - type = ConditionType.Affliction; - } - else - { - type = (target?.SerializableProperties?.ContainsKey(AttributeName) ?? false) - ? ConditionType.PropertyValue - : ConditionType.HasSpecifierTag; - } + type = AfflictionPrefab.Prefabs.ContainsKey(AttributeName) + ? ConditionType.Affliction + : ConditionType.PropertyValue; } if (checkContained) @@ -250,6 +243,14 @@ namespace Barotrauma } } return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + case ConditionType.HasSpecifierTag: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + if (!(target is Character { Info: { } characterInfo })) { return false; } + + return (Operator == OperatorType.Equals) == + SplitAttributeValue.All(v => characterInfo.Head.Preset.TagSet.Contains(v)); + } case ConditionType.SpeciesName: { if (target == null) { return Operator == OperatorType.NotEquals; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs index 8f843ba7c..f313f89fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Steam/Workshop.cs @@ -309,6 +309,11 @@ namespace Barotrauma.Steam XDocument fileListSrc = XMLExtensions.TryLoadXml(Path.Combine(itemDirectory, ContentPackage.FileListFileName)); string modName = fileListSrc.Root.GetAttributeString("name", item.Title).Trim(); + string[] modPathSplit = fileListSrc.Root.GetAttributeString("path", "") + .CleanUpPathCrossPlatform(correctFilenameCase: false).Split("/"); + string? modPathDirName = modPathSplit.Length > 1 && modPathSplit[0] == "Mods" + ? modPathSplit[1] + : null; string modVersion = fileListSrc.Root.GetAttributeString("modversion", ContentPackage.DefaultModVersion); Version gameVersion = fileListSrc.Root.GetAttributeVersion("gameversion", GameMain.Version); bool isCorePackage = fileListSrc.Root.GetAttributeBool("corepackage", false); @@ -316,7 +321,7 @@ namespace Barotrauma.Steam using (var copyIndicator = new CopyIndicator(copyIndicatorPath)) { - await CopyDirectory(itemDirectory, modName, itemDirectory, installDir); + await CopyDirectory(itemDirectory, modPathDirName ?? modName, itemDirectory, installDir); string fileListDestPath = Path.Combine(installDir, ContentPackage.FileListFileName); XDocument fileListDest = XMLExtensions.TryLoadXml(fileListDestPath); @@ -330,9 +335,9 @@ namespace Barotrauma.Steam new XAttribute("modversion", modVersion), new XAttribute("gameversion", gameVersion), new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime))); - if (modName.ToIdentifier() != itemTitle) + if ((modPathDirName ?? modName).ToIdentifier() != itemTitle) { - root.Add(new XAttribute("altnames", modName)); + root.Add(new XAttribute("altnames", modPathDirName ?? modName)); } if (!expectedHash.IsNullOrEmpty()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs index ff86da1d4..67cb2fbf0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/RichString.cs @@ -83,7 +83,7 @@ namespace Barotrauma if (Debugger.IsAttached) { Debugger.Break(); } } #endif - return Plain(lStr); + return Plain(lStr ?? string.Empty); } public static implicit operator RichString(string str) => (LocalizedString)str; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs index b4829e7ba..e36603dd7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextPack.cs @@ -149,22 +149,34 @@ namespace Barotrauma { StringBuilder sb = new StringBuilder(); - for (int i = 0; i < Texts.Count; i++) + XDocument doc = XMLExtensions.TryLoadXml(ContentFile.Path); + if (doc == null) { return; } + + List<(string key, string value)> texts = new List<(string key, string value)>(); + + foreach (var element in doc.Root.Elements()) { - Identifier key = Texts.Keys.ElementAt(i); - Texts.TryGetValue(key, out ImmutableArray infoList); + string text = element.ElementInnerText() + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace(""", "\"") + .Replace("'", "'"); + + texts.Add((element.Name.ToString(), text)); + } + + foreach ((string key, string value) in texts) + { + sb.Append(key); // ID + sb.Append('*'); + sb.Append(value); // Original + sb.Append('*'); + // Translated + sb.Append('*'); + // Comments + sb.AppendLine(); - for (int j = 0; j < infoList.Length; j++) - { - sb.Append(key); // ID - sb.Append('*'); - sb.Append(infoList[j]); // Original - sb.Append('*'); - // Translated - sb.Append('*'); - // Comments - sb.AppendLine(); - } } Barotrauma.IO.File.WriteAllText($"csv_{Language.ToString().ToLower()}_{index}.csv", sb.ToString()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/Result.cs index 71de5e5a3..b63e7a2bc 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) - => new Failure(error); + public static Failure Failure(TError error, string? stackTrace) + => new Failure(error, stackTrace); } public sealed class Success : Result @@ -33,11 +33,15 @@ namespace Barotrauma where TError: notnull { public readonly TError Error; + + public readonly string? StackTrace; + public override bool IsSuccess => false; - public Failure(TError error) + public Failure(TError error, string? stackTrace) { Error = error; + StackTrace = stackTrace; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 40f135e26..c4acca250 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma.IO { @@ -23,9 +24,15 @@ namespace Barotrauma.IO public static bool CanWrite(string path, bool isDirectory) { - path = System.IO.Path.GetFullPath(path).CleanUpPath(); - string localModsDir = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath(); - string workshopModsDir = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath(); + string getFullPath(string p) + => System.IO.Path.GetFullPath(p).CleanUpPath(); + + path = getFullPath(path); + string localModsDir = getFullPath(ContentPackage.LocalModsDir); + string workshopModsDir = getFullPath(ContentPackage.WorkshopModsDir); +#if CLIENT + string tempDownloadDir = getFullPath(ModReceiver.DownloadFolder); +#endif if (!isDirectory) { @@ -34,8 +41,15 @@ namespace Barotrauma.IO { return false; } - if (!path.StartsWith(workshopModsDir, StringComparison.OrdinalIgnoreCase) - && !path.StartsWith(localModsDir, StringComparison.OrdinalIgnoreCase) + + bool pathStartsWith(string prefix) + => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); + + if (!pathStartsWith(workshopModsDir) + && !pathStartsWith(localModsDir) +#if CLIENT + && !pathStartsWith(tempDownloadDir) +#endif && (extension == ".dll" || extension == ".exe" || extension == ".json")) { return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index e0b919969..b55e03a9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -13,7 +13,7 @@ using System.Text.RegularExpressions; namespace Barotrauma { - partial class SaveUtil + static class SaveUtil { private static readonly string LegacySaveFolder = Path.Combine("Data", "Saves"); private static readonly string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer"); diff --git a/Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub b/Barotrauma/BarotraumaShared/Submarines/PowerTestSub.sub deleted file mode 100644 index ea439c4cbff3c95a5d9fef07ca65eb3438396279..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8754 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 sQRy2uPTzh{EJO2Y&5=rW GlobalOnItemInstalled; public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } + + public static PublishedFileId[] GetSubscribedItems() + { + uint numSubscribed = NumSubscribedItems; + PublishedFileId[] ids = new PublishedFileId[numSubscribed]; + Internal.GetSubscribedItems(ids, numSubscribed); + return ids; + } } }

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ξ9Yx9~B-X^q*e!?y1WfwrAFM6KZi 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�YZt7fYHFdgbdF+lg5n;-bVk32< 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 O0000 \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml deleted file mode 100644 index db65d0370..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimFast.xml +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml deleted file mode 100644 index ab9573b83..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerSwimSlow.xml +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml deleted file mode 100644 index b46d3a4c0..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Animations/RedcrawlerWalk.xml +++ /dev/null @@ -1,23 +0,0 @@ - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml deleted file mode 100644 index 71035be33..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml deleted file mode 100644 index 8fcbeb57a..000000000 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Redcrawler.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/crawler.png deleted file mode 100644 index 31f9101d985a3eb14f0b4714690e19111d638aca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161093 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∋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